rigrate 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|