rigrate 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +160 -0
- data/Rakefile +9 -0
- data/bin/rigrate +69 -0
- data/lib/rigrate.rb +97 -0
- data/lib/rigrate/data_source.rb +43 -0
- data/lib/rigrate/error.rb +11 -0
- data/lib/rigrate/interface.rb +20 -0
- data/lib/rigrate/interface/driver.rb +41 -0
- data/lib/rigrate/interface/mysql.rb +148 -0
- data/lib/rigrate/interface/oracle.rb +172 -0
- data/lib/rigrate/interface/result_set.rb +389 -0
- data/lib/rigrate/interface/row.rb +69 -0
- data/lib/rigrate/interface/sqlite.rb +106 -0
- data/lib/rigrate/migration.rb +89 -0
- data/lib/rigrate/parser.rb +270 -0
- data/rigrate.gemspec +21 -0
- data/test/test_datasource.rb +66 -0
- data/test/test_driver.rb +45 -0
- data/test/test_driver_mysql.rb +87 -0
- data/test/test_driver_oracle.rb +109 -0
- data/test/test_driver_sqlite.rb +85 -0
- data/test/test_helper.rb +9 -0
- data/test/test_migration.rb +108 -0
- data/test/test_parser.rb +262 -0
- data/test/test_resultset.rb +300 -0
- data/test/test_rigrate.rb +6 -0
- data/test/test_rigrate_row.rb +51 -0
- metadata +124 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding : utf-8
|
2
|
+
|
3
|
+
module Rigrate
|
4
|
+
# start
|
5
|
+
module RowStatus
|
6
|
+
NEW = :NEW
|
7
|
+
UPDATED = :UPDATED
|
8
|
+
DELETE = :DELETE
|
9
|
+
ORIGIN = :ORIGIN
|
10
|
+
|
11
|
+
def test; end
|
12
|
+
end
|
13
|
+
# end
|
14
|
+
|
15
|
+
class Row
|
16
|
+
attr_accessor :data
|
17
|
+
attr_accessor :status
|
18
|
+
attr_accessor :fields
|
19
|
+
|
20
|
+
def initialize(data = [], status = RowStatus::ORIGIN)
|
21
|
+
self.data = data
|
22
|
+
self.status = status
|
23
|
+
end
|
24
|
+
|
25
|
+
def values(*idxes)
|
26
|
+
idxes.map do |idx|
|
27
|
+
self[idx]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# why need in this way?
|
32
|
+
# TODO need rework
|
33
|
+
def +(t_row)
|
34
|
+
t_row.data.each { |item| @data << item }
|
35
|
+
@status = RowStatus::UPDATED
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(t_row)
|
41
|
+
@data == t_row.data && @status == t_row.status
|
42
|
+
end
|
43
|
+
|
44
|
+
def fill_with_nil(num)
|
45
|
+
@data += Array.new(num)
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](idx)
|
51
|
+
@data[idx]
|
52
|
+
end
|
53
|
+
|
54
|
+
def []=(idx, val, status_force = true)
|
55
|
+
@data[idx] = val
|
56
|
+
@status = RowStatus::UPDATED if status_force
|
57
|
+
end
|
58
|
+
|
59
|
+
def size
|
60
|
+
@data.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def <<(item)
|
64
|
+
@data << item
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding : utf-8
|
2
|
+
|
3
|
+
require 'sqlite3'
|
4
|
+
|
5
|
+
module Rigrate
|
6
|
+
class Sqlite < Driver
|
7
|
+
def initialize(url = nil)
|
8
|
+
url ||= "sqlite://memory"
|
9
|
+
|
10
|
+
opts = extract_conn_param(URI.parse(url))
|
11
|
+
file = extract_db_path(url)
|
12
|
+
|
13
|
+
@db = ::SQLite3::Database.new(file, opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def select(sql, *args)
|
17
|
+
target_tbl_name = extract_tbl_from_sql(sql)
|
18
|
+
|
19
|
+
ResultSet.new.tap do |rs|
|
20
|
+
stm = @db.prepare sql, *args
|
21
|
+
|
22
|
+
rs.db = self
|
23
|
+
rs.target_tbl_name = target_tbl_name
|
24
|
+
rs.column_info = statement_fields(stm.columns, stm.types)
|
25
|
+
rs.rows = []
|
26
|
+
stm.execute.each do |row|
|
27
|
+
new_row = Row.new(to_rb_row(row.to_a))
|
28
|
+
yield new_row if block_given?
|
29
|
+
rs.rows << new_row
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def save(resultset)
|
35
|
+
resultset.db = self
|
36
|
+
|
37
|
+
resultset.save!
|
38
|
+
end
|
39
|
+
|
40
|
+
def update(sql, *args)
|
41
|
+
begin
|
42
|
+
stm = @db.prepare sql
|
43
|
+
args.each do |row|
|
44
|
+
if Rigrate::Row === row
|
45
|
+
row = row.data
|
46
|
+
end
|
47
|
+
stm.execute(*row)
|
48
|
+
end
|
49
|
+
rescue Exception => e
|
50
|
+
Rigrate.logger.error("execute SQL [#{sql}] ARGS [#{args.size}] -> #{e.backtrace.join('\n')}")
|
51
|
+
raise DriverError.new("execute error #{e.message}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
alias :insert :update
|
55
|
+
alias :delete :update
|
56
|
+
|
57
|
+
def primary_key(tbl_name)
|
58
|
+
(@db.table_info(tbl_name).select do |col_hash|
|
59
|
+
col_hash["pk"] == 1
|
60
|
+
end).map do |col_hash|
|
61
|
+
col_hash["name"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_rb_row(row)
|
66
|
+
row.map do |field|
|
67
|
+
if field.nil?
|
68
|
+
field.to_s
|
69
|
+
else
|
70
|
+
field
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def statement_fields(names, types)
|
76
|
+
cols = []
|
77
|
+
names.each_with_index do |name, idx|
|
78
|
+
cols << Column.new(name, types[idx])
|
79
|
+
end
|
80
|
+
|
81
|
+
cols
|
82
|
+
end
|
83
|
+
|
84
|
+
def extract_db_path(path)
|
85
|
+
result = ":memory:"
|
86
|
+
|
87
|
+
if path =~ /sqlite:\/\/(.*)/
|
88
|
+
if $1 == 'memory'
|
89
|
+
result = ":memory:"
|
90
|
+
else
|
91
|
+
result = $1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
result
|
96
|
+
end
|
97
|
+
|
98
|
+
def transaction
|
99
|
+
@db.transaction
|
100
|
+
end
|
101
|
+
|
102
|
+
def commit
|
103
|
+
@db.commit
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding : utf-8
|
2
|
+
|
3
|
+
module Rigrate
|
4
|
+
module Migration
|
5
|
+
def union(rs_first_str, rs_second_str)
|
6
|
+
if String === rs_first_str
|
7
|
+
rs_first = instance_eval rs_first_str
|
8
|
+
else
|
9
|
+
rs_first = rs_first_str
|
10
|
+
end
|
11
|
+
rs_second = instance_eval rs_second_str
|
12
|
+
|
13
|
+
if ResultSet === rs_first && ResultSet === rs_second
|
14
|
+
Rigrate.logger.info("start union two ResultSet rs [#{rs_first.size}] <---> rs [#{rs_second.size}]")
|
15
|
+
return rs_first.union rs_second
|
16
|
+
else
|
17
|
+
raise Exception.new('rs_first or rs_second is not a resultset')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def minus(rs_first_str, rs_second_str)
|
22
|
+
if String === rs_first_str
|
23
|
+
rs_first = instance_eval rs_first_str
|
24
|
+
else
|
25
|
+
rs_first = rs_first_str
|
26
|
+
end
|
27
|
+
rs_second = instance_eval rs_second_str
|
28
|
+
|
29
|
+
if ResultSet === rs_first && ResultSet === rs_second
|
30
|
+
Rigrate.logger.info("start minus two ResultSet rs [#{rs_first.size}] <---> rs [#{rs_second.size}]")
|
31
|
+
return rs_first.minus rs_second
|
32
|
+
else
|
33
|
+
raise Exception.new('rs_first or rs_second is not a resultset')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def join(rs_first_str, rs_second_str, condition = nil)
|
38
|
+
if String === rs_first_str
|
39
|
+
rs_first = instance_eval rs_first_str
|
40
|
+
else
|
41
|
+
rs_first = rs_first_str
|
42
|
+
end
|
43
|
+
condition = eval("{#{condition}}") unless condition.nil?
|
44
|
+
rs_second = instance_eval rs_second_str
|
45
|
+
|
46
|
+
if ResultSet === rs_first && ResultSet === rs_second
|
47
|
+
Rigrate.logger.info("start join two ResultSet rs [#{rs_first.size}] <---> rs [#{rs_second.size}]")
|
48
|
+
return rs_first.join(rs_second, condition)
|
49
|
+
else
|
50
|
+
raise Exception.new('rs_first or rs_second is not a resultset')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# migration mode
|
55
|
+
def migrate(rs_first_str, rs_second_str, condition = nil, mode = nil)
|
56
|
+
if String === rs_first_str
|
57
|
+
rs_source = instance_eval rs_first_str
|
58
|
+
else
|
59
|
+
rs_source = rs_first_str
|
60
|
+
end
|
61
|
+
rs_target = instance_eval rs_second_str
|
62
|
+
|
63
|
+
default_opts = { :mode => Rigrate.config[:mode] }
|
64
|
+
if mode
|
65
|
+
default_opts[:mode] = instance_eval("#{mode}")
|
66
|
+
end
|
67
|
+
|
68
|
+
if ResultSet === rs_source && ResultSet === rs_target
|
69
|
+
return rs_target.migrate_from(rs_source, condition, default_opts)
|
70
|
+
else
|
71
|
+
raise Exception.new('rs_target or rs_source is not a resultset.')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self_eval(rb_str)
|
76
|
+
instance_eval(rb_str)
|
77
|
+
end
|
78
|
+
|
79
|
+
def data_source(name, conn_str)
|
80
|
+
name = name.to_s
|
81
|
+
ds = DataSource.new(conn_str)
|
82
|
+
variable_name = "@#{name}".to_sym unless name.start_with? "@"
|
83
|
+
instance_variable_set variable_name, ds
|
84
|
+
instance_eval("def #{name}=(x); #{variable_name}=x; end;\
|
85
|
+
def #{name}; #{variable_name}; end")
|
86
|
+
end
|
87
|
+
alias :ds :data_source
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
# encoding : utf-8
|
2
|
+
|
3
|
+
module Rigrate
|
4
|
+
class Parser
|
5
|
+
include Migration
|
6
|
+
|
7
|
+
module TokenType
|
8
|
+
FROM = :FROM_TAG
|
9
|
+
TO = :TO_TAG
|
10
|
+
UNION = :UNION_TAG
|
11
|
+
JOIN = :JOIN_TAG
|
12
|
+
MINUS = :MINUS_TAG
|
13
|
+
ON = :ON_TAG
|
14
|
+
USING = :USING_TAG
|
15
|
+
RUBY_STR = :RUBY_STR_TAG
|
16
|
+
end
|
17
|
+
|
18
|
+
module StringType
|
19
|
+
DOUBLE_QUOTE = :DQ_STR
|
20
|
+
SINGLE_QUOTE = :SQ_STR
|
21
|
+
end
|
22
|
+
|
23
|
+
module LexStatus
|
24
|
+
INIT = :INIT_STATUS
|
25
|
+
IN_KEYWORD = :IN_KEYWORD_STATUS
|
26
|
+
IN_RUBY_CODE= :IN_RUBY_CODE_STATUS
|
27
|
+
IN_RUBY_STR = :IN_RUBY_STR_STATUS
|
28
|
+
IN_COMMENT = :IN_COMMENT_STATUS
|
29
|
+
end
|
30
|
+
|
31
|
+
Token = Struct.new(:type, :value)
|
32
|
+
attr_accessor :tokens
|
33
|
+
|
34
|
+
##
|
35
|
+
# load script str -> tokens
|
36
|
+
# TODO there is a bug need to fixed, when comment str include FROM key word will praser error.
|
37
|
+
def lex(str)
|
38
|
+
status = LexStatus::INIT
|
39
|
+
@tokens = []
|
40
|
+
t_token = ''
|
41
|
+
t_sub_token = ''
|
42
|
+
string_type = StringType::DOUBLE_QUOTE
|
43
|
+
char_arr = str.chars
|
44
|
+
loop do
|
45
|
+
c = char_arr.shift
|
46
|
+
|
47
|
+
if c == nil
|
48
|
+
token = Token.new TokenType::RUBY_STR, t_token
|
49
|
+
@tokens << token
|
50
|
+
break
|
51
|
+
end
|
52
|
+
|
53
|
+
if status == LexStatus::IN_KEYWORD && c =~ /\s/
|
54
|
+
if is_a_token?(t_token)
|
55
|
+
@tokens << (Token.new get_token_type(t_token), t_token)
|
56
|
+
t_token = ''
|
57
|
+
t_sub_token = ''
|
58
|
+
status = LexStatus::INIT
|
59
|
+
next
|
60
|
+
else
|
61
|
+
status = LexStatus::IN_RUBY_CODE
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if status != LexStatus::IN_RUBY_CODE && status != LexStatus::IN_RUBY_STR
|
66
|
+
(status = LexStatus::INIT && next) if c =~ /\s/
|
67
|
+
end
|
68
|
+
|
69
|
+
t_token << c
|
70
|
+
t_sub_token << c
|
71
|
+
|
72
|
+
if status == LexStatus::IN_RUBY_CODE ||
|
73
|
+
status == LexStatus::IN_KEYWORD
|
74
|
+
if c == '"'
|
75
|
+
string_type = StringType::DOUBLE_QUOTE
|
76
|
+
status = LexStatus::IN_RUBY_STR
|
77
|
+
next
|
78
|
+
elsif c == "'"
|
79
|
+
string_type = StringType::SINGLE_QUOTE
|
80
|
+
status = LexStatus::IN_RUBY_STR
|
81
|
+
next
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
if status == LexStatus::IN_RUBY_STR
|
86
|
+
is_matched = false
|
87
|
+
if (string_type == StringType::DOUBLE_QUOTE && c == '"') ||
|
88
|
+
(string_type == StringType::SINGLE_QUOTE && c == "'")
|
89
|
+
is_matched = true
|
90
|
+
end
|
91
|
+
|
92
|
+
if is_matched && t_token[-1] != "\\"
|
93
|
+
status = LexStatus::IN_RUBY_CODE
|
94
|
+
elsif c =~ /\s/
|
95
|
+
t_sub_token = ''
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if status == LexStatus::IN_RUBY_CODE && c =~ /\s/
|
100
|
+
if is_a_token? t_sub_token
|
101
|
+
token = Token.new TokenType::RUBY_STR, t_token.sub(/#{t_sub_token}$/, '')
|
102
|
+
@tokens << token
|
103
|
+
token = Token.new get_token_type(t_sub_token), t_sub_token
|
104
|
+
@tokens << token
|
105
|
+
|
106
|
+
status = LexStatus::INIT
|
107
|
+
t_token = ''
|
108
|
+
end
|
109
|
+
|
110
|
+
t_sub_token = ''
|
111
|
+
end
|
112
|
+
|
113
|
+
if status == LexStatus::INIT
|
114
|
+
status = LexStatus::IN_KEYWORD
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
def parsing
|
122
|
+
full_parse tokens.dup
|
123
|
+
end
|
124
|
+
|
125
|
+
#private
|
126
|
+
|
127
|
+
def full_parse(tks)
|
128
|
+
while tks.size > 0
|
129
|
+
parse_rs_or_migrate_exp(tks)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_rs_or_migrate_exp(tks)
|
134
|
+
token = tks.shift
|
135
|
+
|
136
|
+
if token.type == TokenType::RUBY_STR
|
137
|
+
v1 = self_eval token.value
|
138
|
+
else
|
139
|
+
tks.unshift token
|
140
|
+
v1 = parse_migrate_exp(tks)
|
141
|
+
end
|
142
|
+
|
143
|
+
v1
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_migrate_exp(tks)
|
147
|
+
token = tks.shift
|
148
|
+
|
149
|
+
if token.type == TokenType::FROM
|
150
|
+
v1 = parse_operate_exp(tks)
|
151
|
+
|
152
|
+
sub_token = tks.shift
|
153
|
+
if sub_token.type == TokenType::TO
|
154
|
+
v2 = parse_rs_exp(tks)
|
155
|
+
v3 = parse_condition_exp(tks)
|
156
|
+
v4 = parse_using_exp(tks)
|
157
|
+
migrate(v1, v2, v3, v4)
|
158
|
+
|
159
|
+
# sub_token_1 = tks.shift
|
160
|
+
# if not sub_token_1.nil?
|
161
|
+
# if sub_token_1.type == TokenType::ON
|
162
|
+
# cond = tks.shift
|
163
|
+
# migrate(v1, v2, cond)
|
164
|
+
# else
|
165
|
+
# tks.unshift sub_token_1
|
166
|
+
# migrate(v1, v2)
|
167
|
+
# end
|
168
|
+
# else
|
169
|
+
# migrate(v1, v2)
|
170
|
+
# end
|
171
|
+
else
|
172
|
+
raise ParserError.new("Syntax Error: need TO expression")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_operate_exp(tks)
|
178
|
+
v1 = parse_rs_exp(tks)
|
179
|
+
|
180
|
+
while true
|
181
|
+
token = tks.shift
|
182
|
+
if token.type != TokenType::UNION &&
|
183
|
+
token.type != TokenType::JOIN &&
|
184
|
+
token.type != TokenType::MINUS
|
185
|
+
tks.unshift token
|
186
|
+
break
|
187
|
+
end
|
188
|
+
|
189
|
+
v2 = parse_rs_exp(tks)
|
190
|
+
if token.type == TokenType::UNION
|
191
|
+
v1 = union(v1, v2)
|
192
|
+
elsif token.type == TokenType::MINUS
|
193
|
+
v1 = minus(v1, v2)
|
194
|
+
elsif token.type == TokenType::JOIN
|
195
|
+
sub_token = tks.shift
|
196
|
+
|
197
|
+
if not sub_token.nil?
|
198
|
+
if sub_token.type == TokenType::ON
|
199
|
+
cond = tks.shift
|
200
|
+
v1 = join(v1, v2, cond.value)
|
201
|
+
else
|
202
|
+
tks.unshift sub_token
|
203
|
+
v1 = join(v1, v2)
|
204
|
+
end
|
205
|
+
else
|
206
|
+
v1 = join(v1, v2)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
v1
|
212
|
+
end
|
213
|
+
|
214
|
+
def parse_condition_exp(tks)
|
215
|
+
token = tks.shift
|
216
|
+
return if token.nil?
|
217
|
+
|
218
|
+
if token.type == TokenType::ON
|
219
|
+
v1 = parse_rs_exp(tks)
|
220
|
+
|
221
|
+
if v1.nil?
|
222
|
+
raise ParserError.new("ON expression should end with a [RUBY_STR]")
|
223
|
+
end
|
224
|
+
else
|
225
|
+
tks.unshift token
|
226
|
+
return
|
227
|
+
end
|
228
|
+
|
229
|
+
v1
|
230
|
+
end
|
231
|
+
|
232
|
+
def parse_using_exp(tks)
|
233
|
+
token = tks.shift
|
234
|
+
return if token.nil?
|
235
|
+
|
236
|
+
if token.type == TokenType::USING
|
237
|
+
v1 = parse_rs_exp(tks)
|
238
|
+
|
239
|
+
if v1.nil?
|
240
|
+
raise ParserError.new("USING expression should end with a [RUBY_STR]")
|
241
|
+
end
|
242
|
+
else
|
243
|
+
tks.unshift token
|
244
|
+
return
|
245
|
+
end
|
246
|
+
|
247
|
+
v1
|
248
|
+
end
|
249
|
+
|
250
|
+
# TODO return value should change
|
251
|
+
def parse_rs_exp(tks)
|
252
|
+
token = tks.shift
|
253
|
+
return if token.nil?
|
254
|
+
|
255
|
+
if token.type == TokenType::RUBY_STR
|
256
|
+
return token.value
|
257
|
+
else
|
258
|
+
raise ParserError.new("Invalid Syntax")
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def is_a_token?(str)
|
263
|
+
TokenType.constants.include? str.strip.upcase.to_sym
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_token_type(str)
|
267
|
+
TokenType.const_get(str.strip.upcase)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|