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.
@@ -0,0 +1,148 @@
1
+ # encoding : utf-8
2
+
3
+ require 'mysql'
4
+
5
+ module Rigrate
6
+ class Mysql < Driver
7
+ attr_accessor :transaction_active
8
+
9
+ def initialize(uri)
10
+ default_opts = {
11
+ host: '127.0.0.1',
12
+ user: nil,
13
+ passwd: nil,
14
+ db: nil,
15
+ port: 3306,
16
+ socket: nil,
17
+ flag: 0
18
+ }
19
+
20
+ extract_from_db_path(uri).each do |k, v|
21
+ default_opts[k.to_sym] = v if default_opts.keys.include? k.to_sym
22
+ end
23
+
24
+ @db = ::Mysql.connect(*default_opts.values)
25
+ @transaction_active = false
26
+ end
27
+
28
+ def select(sql, *args)
29
+ target_tbl_name = extract_tbl_from_sql(sql)
30
+
31
+ ResultSet.new.tap do |rs|
32
+ stm = @db.prepare(sql)
33
+ rs.db = self
34
+ rs.target_tbl_name = target_tbl_name
35
+ rs.column_info = statement_fields(stm)
36
+ result = stm.execute(*args)
37
+ rs.rows = []
38
+ while row = result.fetch
39
+ new_row = Row.new(to_rb_row(row))
40
+ yield new_row if block_given?
41
+ rs.rows << new_row
42
+ end
43
+ end
44
+ end
45
+
46
+ def save(resultset)
47
+ resultset.db = self
48
+
49
+ resultset.save!
50
+ end
51
+
52
+ def update(sql, *args)
53
+ begin
54
+ stm = @db.prepare sql
55
+ args.each do |row|
56
+ if Rigrate::Row === row
57
+ row = row.data
58
+ end
59
+ stm.execute(*row)
60
+ end
61
+ rescue Exception => e
62
+ Rigrate.logger.error("execute SQL [#{sql}] ARGS [#{args.size}] -> #{e.backtrace.join('\n')}")
63
+ raise DriverError.new("execute error #{e.message}")
64
+ end
65
+ end
66
+ alias :insert :update
67
+ alias :delete :update
68
+
69
+ def primary_key(tbl_name)
70
+ tbl_name = tbl_name.to_s
71
+
72
+ db.list_fields(tbl_name).fetch_fields.select do |field|
73
+ field.is_pri_key?
74
+ end.map(&:name)
75
+ end
76
+
77
+ def statement_fields(stm)
78
+ cols = []
79
+
80
+ stm.result_metadata.fetch_fields.each do |field|
81
+ cols << Column.new(field.name, get_field_type(field.type))
82
+ end
83
+ end
84
+
85
+ def to_rb_row(mysql_row)
86
+ mysql_row.map do |field|
87
+ if ::Mysql::Time === field
88
+ field.to_s
89
+ else
90
+ field
91
+ end
92
+ end
93
+ end
94
+
95
+ def format_sql_args(args)
96
+ args.map do |arg|
97
+ if String === arg
98
+ "'#{arg}'"
99
+ elsif DateTime === arg
100
+ arg.strptime('%Y-%m-%d %H:%M:%S')
101
+ else
102
+ arg
103
+ end
104
+ end
105
+ end
106
+
107
+ def get_field_type(num)
108
+ ::Mysql::Field.constants.select do |cons|
109
+ cons if ::Mysql::Field.const_get(cons) == num
110
+ end
111
+ end
112
+
113
+ def extract_from_db_path(uri)
114
+ uri = URI.parse(uri)
115
+ args = {}
116
+
117
+ args[:host] = uri.host if uri.host
118
+ args[:user] = uri.user if uri.user
119
+ args[:passwd] = uri.password if uri.password
120
+ args[:port] = uri.port if uri.port
121
+ args[:scheme] = uri.scheme if uri.scheme
122
+ args[:db] = uri.path.sub('/','') if uri.path.size > 1
123
+
124
+ args
125
+ end
126
+
127
+ def transaction_active?
128
+ @transaction_active
129
+ end
130
+
131
+ def transaction
132
+ @db.autocommit false
133
+ @transaction_active = true
134
+ end
135
+
136
+ def commit
137
+ @db.commit
138
+ @db.autocommit true
139
+ @transaction_active = false
140
+ end
141
+
142
+ def rollback
143
+ @db.rollback
144
+ @db.autocommit true
145
+ @transaction_active = false
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,172 @@
1
+ # encoding : utf-8
2
+
3
+ require 'oci8'
4
+
5
+ module Rigrate
6
+ class Oracle < Driver
7
+ def initialize(uri)
8
+ key_params = [:username, :password, :sid, :privilege]
9
+ opts = params_pack(extract_from_db_path(uri), key_params)
10
+ @db = OCI8.new(*opts.values)
11
+ @db.autocommit = true
12
+ end
13
+
14
+ def select(sql, *args)
15
+ target_tbl_name = extract_tbl_from_sql(sql)
16
+ sql = convert_question_mark_to_symbol(sql, args.size)
17
+
18
+ ResultSet.new.tap do |rs|
19
+ cursor = @db.parse(sql)
20
+ rs.db = self
21
+ rs.target_tbl_name = target_tbl_name
22
+ rs.rows = []
23
+ cursor.exec(*args)
24
+ while row = cursor.fetch
25
+ new_row = Row.new(to_rb_row(row))
26
+ yield new_row if block_given?
27
+ rs.rows << new_row
28
+ end
29
+ rs.column_info = statement_fields(cursor)
30
+ end
31
+ end
32
+
33
+ def execute(sql, *args)
34
+ sql = convert_question_mark_to_symbol(sql, args.first.size)
35
+ begin
36
+ cursor = @db.parse(sql)
37
+ args.each do |row|
38
+ if Rigrate::Row === row
39
+ row = row.data
40
+ end
41
+ cursor.exec(*row)
42
+ end
43
+ rescue Exception => e
44
+ Rigrate.logger.error("execute SQL [#{sql}] ARGS [#{args.size}] -> #{e.backtrace.join('\n')}")
45
+ raise DriverError.new("execute error #{e.message}")
46
+ end
47
+ end
48
+ alias :insert :execute
49
+ alias :update :execute
50
+ alias :delete :execute
51
+
52
+ def statement_fields(cursor)
53
+ cols = []
54
+
55
+ cursor.column_metadata.each do |field|
56
+ cols << Column.new(field.name, field.data_type)
57
+ end
58
+
59
+ cols
60
+ end
61
+
62
+ def convert_question_mark_to_symbol(sql, param_size)
63
+ param_size.times do |idx|
64
+ sql.sub!('?', ":#{idx + 1}")
65
+ end
66
+
67
+ sql
68
+ end
69
+
70
+ # TODO add blob/clob support
71
+ def to_native_row(row, column_info)
72
+ column_info.each_with_index do |col_info, idx|
73
+ case col_info.type.to_s.downcase.to_sym
74
+ when :blob
75
+ new_val = OCI8::BLOB.new(@db, row[idx])
76
+ when :clob
77
+ new_val = OCI8::CLOB.new(@db, row[idx])
78
+ when :bclob
79
+ new_val = OCI8::NCLOB.new(@db, row[idx])
80
+ when :date
81
+ if row[idx]
82
+ if Time === row[idx]
83
+ new_val = row[idx]
84
+ elsif String === row[idx] && row[idx].size > 0
85
+ new_val = Time.parse(row[idx])
86
+ end
87
+ else
88
+ new_val = ''
89
+ end
90
+ else
91
+ new_val = row[idx]
92
+ end
93
+
94
+ if new_val.nil?
95
+ new_val = ''
96
+ end
97
+
98
+ row.[]=(idx, new_val, false)
99
+ end
100
+
101
+ row
102
+ end
103
+
104
+ def to_rb_row(row)
105
+ row.map do |field|
106
+ type = field.class
107
+ if [OCI8::BLOB, OCI8::CLOB, OCI8::NCLOB].include? type
108
+ field.read
109
+ elsif Time == type
110
+ field.to_s
111
+ else
112
+ field
113
+ end
114
+ end
115
+ end
116
+
117
+ # oracle://scott:tiger@foctest?privilege=:SYSOPER
118
+ def extract_from_db_path(uri)
119
+ uri = URI.parse(uri)
120
+ args = {}
121
+
122
+ args[:username] = uri.user if uri.user
123
+ args[:password] = uri.password if uri.password
124
+ args[:sid] = uri.host if uri.host
125
+ URI::decode_www_form(uri.query.to_s).to_h.each do |key, val|
126
+ args[key] = val
127
+ end
128
+ args
129
+ end
130
+
131
+ def primary_key(tbl_name)
132
+ str =<<SQL
133
+ select a.column_name
134
+ from user_cons_columns a, user_constraints b
135
+ where a.constraint_name = b.constraint_name
136
+ and b.constraint_type = 'P'
137
+ and a.table_name = '#{tbl_name}'
138
+ SQL
139
+
140
+ result = []
141
+ @db.exec(str) do |row|
142
+ result << row.first
143
+ end
144
+
145
+ result
146
+ end
147
+
148
+ def params_pack(hash, keys)
149
+ keys.each do |key|
150
+ hash.delete(key) unless hash.keys.include? key
151
+ end
152
+
153
+ hash
154
+ end
155
+
156
+ def transaction_active?
157
+ ! @db.autocommit?
158
+ end
159
+
160
+ def transaction
161
+ @db.autocommit = false
162
+ end
163
+
164
+ def commit
165
+ @db.commit
166
+ end
167
+
168
+ def rollback
169
+ @db.rollbak
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,389 @@
1
+ # encoding : utf-8
2
+
3
+ module Rigrate
4
+ class ResultSet
5
+ attr_accessor :db, :target_tbl_name
6
+ attr_accessor :rows
7
+ attr_accessor :column_info
8
+
9
+ # join two table by given field { :jc => :job_code }
10
+ def join(source_rs, key_fields = {})
11
+ if key_fields.size <= 0
12
+ raise ResultSetError.new("must specify the join condition.")
13
+ end
14
+
15
+ # convert condition key and value to string
16
+ key_fields = key_fields.inject({}) do |h, (k, v)|
17
+ h[k.to_s.upcase] = v.to_s.upcase
18
+ h
19
+ end
20
+
21
+ origin_rs_idx = column_idx(*key_fields.keys)
22
+ source_rs_idx = source_rs.column_idx(*key_fields.values)
23
+
24
+ ResultSet.new.tap do |rs|
25
+ # remove duplicate column header, base on column name
26
+ addtion_column_info = source_rs.column_info.dup.delete_if do |col|
27
+ key_fields.values.include? col.name.upcase
28
+ end
29
+
30
+ rs.column_info = @column_info + addtion_column_info
31
+
32
+ rs.rows = @rows.inject([]) do |new_rows, row|
33
+ origin_rs_key_values = row.values(*origin_rs_idx)
34
+
35
+ selected_source_rs_row = source_rs.rows.select do |r|
36
+ r.values(*source_rs_idx) == origin_rs_key_values
37
+ end
38
+
39
+ # remove duplicate data columns
40
+ selected_source_rs_row.map! do |r|
41
+ data = []
42
+ r.data.each_with_index do |value, idx|
43
+ data << value unless source_rs_idx.include? idx
44
+ end
45
+
46
+ Row.new data, RowStatus::NEW
47
+ end
48
+ # this is a left join.
49
+ if selected_source_rs_row.size > 0
50
+ selected_source_rs_row.each do |t_row|
51
+ new_rows << Row.new(row.data + t_row.data, RowStatus::NEW)
52
+ end
53
+ else
54
+ new_rows << row.dup.fill_with_nil(addtion_column_info.size)
55
+ end
56
+
57
+ new_rows
58
+ end
59
+ end
60
+ end
61
+
62
+ # union two result set , columns defination will not change
63
+ # default is union all style
64
+ def union(target, opts = {})
65
+ src_col_size = column_info.size
66
+ target_col_size = target.column_info.size
67
+
68
+ # TODO need type checking?
69
+
70
+ if src_col_size > target_col_size
71
+ target.rows = target.rows.map do |row|
72
+ row.fill_with_nil(src_col_size - target_col_size)
73
+ end
74
+ elsif src_col_size < target_col_size
75
+ target.rows = target.rows.map { |row| row[0...src_col_size] }
76
+ end
77
+
78
+ @rows += target.rows
79
+
80
+ self
81
+ end
82
+
83
+ def -(target, opts = {})
84
+ src_col_size = column_info.size
85
+ target_col_size = target.column_info.size
86
+
87
+ # checking type?
88
+ #columns size must equal
89
+ if src_col_size != target_col_size
90
+ raise ResultSetError.new('minus must be used between column size equaled.')
91
+ end
92
+ @rows.reject! do |row|
93
+ target.include? row
94
+ end
95
+
96
+ self
97
+ end
98
+ alias :minus :-
99
+
100
+ #
101
+ # There are three modes
102
+ # :echo is left to right ResultSet, and delete right which is not in left
103
+ # +When condition is nil+
104
+ # find the primary key as condition
105
+ # 1. if primary key can't find in ResultSet then delete all record in right side, and insert
106
+ # all rows to left side
107
+ # +When condition is not nil+
108
+ # 1. using condition to update existing row in right side
109
+ # 2. and insert rows which not included in right side
110
+ # 3. then delete rows not in left side
111
+ #
112
+ # :contribute is left to right, and KEEP the file even it not in left
113
+ # +When condition is nil+
114
+ # find the primary key as condition
115
+ # 1. if primary key can't find in Result then insert all rows to left side
116
+ # +When condition is not nil+
117
+ # 1. using condition to update existing row in right side
118
+ # 2. and insert rows not included in right side
119
+ #
120
+ # :sync will keep left and right the same +TODO+
121
+ # just keep two side the same
122
+ #
123
+ def migrate_from(src_rs, condition = nil, opts = {})
124
+ Rigrate.logger.info("start migration : source rs [#{src_rs.size}] ->target rs [#{rows.size}]")
125
+ mode = opts[:mode]
126
+ condition = eval "{#{condition.to_s.upcase}}" unless condition.nil?
127
+
128
+ if condition
129
+ src_cols_idx = src_rs.column_idx(*condition.keys)
130
+ tg_cols_idx = column_idx(*condition.values)
131
+ else
132
+ # delete line -> src_cols_idx = src_rs.column_idx(*src_rs.default_migration_condition)
133
+ # suppose all primary key of target resultset can be found in src result, and in same column idx
134
+ tg_cols_idx = column_idx(*default_migration_condition)
135
+ src_cols_idx = tg_cols_idx
136
+ end
137
+ @rows = handle_rows(src_rs.rows, mode, src_cols_idx, tg_cols_idx)
138
+ save!(condition)
139
+ end
140
+
141
+ # condition {:name => :c_name, :age => :age}
142
+ # insert or update or delete
143
+ def handle_rows (src_rows_data, mode, src_cols_idx = nil, tg_cols_idx = nil)
144
+ new_rows_data = []
145
+
146
+ # condition parameter is null , so delete all ROWS. and then copy the source rs
147
+ if src_cols_idx.to_a.size <= 0 && tg_cols_idx.to_a.size <= 0
148
+ # TODO check the size
149
+ if mode == :echo
150
+ # delete all the dest rs data
151
+ @rows.each do |row|
152
+ new_rows_data << Row.new(row.data, RowStatus::DELETE)
153
+ end
154
+ end
155
+ # :echo and :contribute mode
156
+ format_rows(src_rows_data, width).map do |row|
157
+ new_rows_data << Row.new(row.data, RowStatus::NEW)
158
+ end
159
+ else
160
+ if mode == :echo
161
+ new_rows_data += delete_not_exist_rows(@rows, src_rows_data, tg_cols_idx, src_cols_idx)
162
+ end
163
+ # :echo and :contribute
164
+ new_rows_data += op_two_rows(@rows, tg_cols_idx, src_rows_data, src_cols_idx)
165
+ end
166
+
167
+ new_rows_data
168
+ end
169
+
170
+ def op_two_rows(target_rows, target_cols_idx, source_rows, src_cols_idx)
171
+ result = []
172
+ source_rows.each do |s_row|
173
+ s_values = s_row.values(*src_cols_idx)
174
+ fetched = false
175
+
176
+ target_rows.each do |t_row|
177
+ if s_values == t_row.values(*target_cols_idx)
178
+ fetched = true
179
+ if s_row.data != t_row.data
180
+ result << Row.new(s_row.data, RowStatus::UPDATED)
181
+ else
182
+ result << Row.new(s_row.data, RowStatus::ORIGIN)
183
+ end
184
+ end
185
+ end
186
+ result << Row.new(s_row.data, RowStatus::NEW) unless fetched
187
+ end
188
+
189
+ result
190
+ end
191
+
192
+ def delete_not_exist_rows(target_rows, source_rows, target_cols_idx, src_cols_idx)
193
+ result = []
194
+ target_rows.each do |t_row|
195
+ t_values = t_row.values(*target_cols_idx)
196
+ fetched = false
197
+
198
+ source_rows.each do |s_row|
199
+ break fetched = true if s_row.values(*src_cols_idx) == t_values
200
+ end
201
+ result << Row.new(t_row.data, RowStatus::DELETE) unless fetched
202
+ end
203
+
204
+ result
205
+ end
206
+
207
+ def save!(condition = nil)
208
+ begin
209
+ # convert all to native row
210
+ @rows.map do |row|
211
+ convert_to_native_row(row)
212
+ end
213
+
214
+ @db.transaction if Rigrate.config[:strict]
215
+ condition = condition.values if condition
216
+ handle_delete!(condition)
217
+ handle_insert!
218
+ handle_update!(condition)
219
+ @db.commit if @db.transaction_active?
220
+ rescue Exception => e
221
+ Rigrate.logger.error("saving resultset [#{rows.size}] error: #{e.message} #{e.backtrace}")
222
+ raise e
223
+ @db.rollback if @db.transaction_active?
224
+ end
225
+ end
226
+
227
+ def handle_insert!()
228
+ sql = get_sql(:insert)
229
+
230
+ op_rows = @rows.select do |row|
231
+ row.status == RowStatus::NEW
232
+ end
233
+
234
+ Rigrate.logger.info("start insert [#{op_rows.size}] rows using sql [#{sql}]")
235
+ @db.insert sql, *op_rows if op_rows.size > 0
236
+ end
237
+
238
+ def handle_update!(condition = nil)
239
+ key_fields = (condition || primary_key)
240
+ sql = get_sql(:update, key_fields)
241
+ param_fields = column_info.reject do |col|
242
+ key_fields.include? col.name
243
+ end.map { |col| col.name }
244
+
245
+ op_rows = rows.select do |row|
246
+ row.status == RowStatus::UPDATED
247
+ end
248
+
249
+ key_idx = column_idx(*key_fields)
250
+ params_idx = column_idx(*param_fields)
251
+ formated_rows = op_rows.map do |row|
252
+ key_values = row.values(*key_idx)
253
+ params_values = row.values(*params_idx)
254
+
255
+ params_values + key_values
256
+ end
257
+ Rigrate.logger.info("start update [#{op_rows.size}] rows using sql [#{sql}]")
258
+ @db.update sql, *formated_rows if formated_rows.size > 0
259
+ end
260
+
261
+ def handle_delete!(condition = nil)
262
+ temp_primary_key = nil
263
+ temp_primary_key = primary_key if primary_key.size > 0
264
+ condi_fields = condition || temp_primary_key || column_info.map{ |col| col.name }
265
+
266
+ sql = get_sql(:delete, condi_fields)
267
+
268
+ op_rows = @rows.select do |row|
269
+ row.status == RowStatus::DELETE
270
+ end
271
+
272
+ params_idx = column_idx(*condi_fields)
273
+ formated_rows = op_rows.map do |row|
274
+ row.values(*params_idx)
275
+ end
276
+
277
+ Rigrate.logger.info("start delete [#{op_rows.size}] rows using sql [#{sql}]")
278
+ @db.delete sql, *formated_rows if formated_rows.size > 0
279
+ end
280
+
281
+ def get_sql(type, condition = nil)
282
+ case type
283
+ when :insert
284
+ params_str = column_info.map(&:name).join(',')
285
+ values_str = Array.new(column_info.size){'?'}.join(',')
286
+
287
+ "insert into #{target_tbl_name} (#{params_str}) values (#{values_str})"
288
+ when :update
289
+ condi_fields = condition || primary_key
290
+ params_str = condi_fields.map do |col|
291
+ "#{col}=?"
292
+ end.join(' and ')
293
+
294
+ upd_fields = column_info.reject do |col|
295
+ condi_fields.include? col.name
296
+ end
297
+ setting_str = upd_fields.map do |col|
298
+ "#{col.name}=?"
299
+ end.join(',')
300
+
301
+ "update #{target_tbl_name} set #{setting_str} where #{params_str}"
302
+ when :delete
303
+ params_str = condition.map do |col|
304
+ "#{col}=?"
305
+ end.join(' and ')
306
+ raise ResultSetError.new("can't get the delete sql") if params_str.to_s.size <= 0
307
+
308
+ "delete from #{target_tbl_name} where #{params_str}"
309
+ end
310
+ end
311
+
312
+ # convert source resulset rows to specify width
313
+ def format_rows(src_rows, tg_width, filled = nil)
314
+ r_length = src_rows.first.size
315
+
316
+ if r_length > tg_width
317
+ src_rows.map! do |row|
318
+ row.data = row[0..tg_width]
319
+ end
320
+ elsif r_length < tg_width
321
+ src_rows.map! do |row|
322
+ row.fill_with_nil(tg_width - r_length)
323
+ end
324
+ end
325
+
326
+ src_rows
327
+ end
328
+
329
+ def column_values(row, cols)
330
+ cols.map do |col|
331
+ row[col]
332
+ end
333
+ end
334
+
335
+ def column_idx(*names)
336
+ names.inject([]) do |idxes, name|
337
+ column_info.each_with_index do |col, idx|
338
+ idxes << idx if col.name.to_s.downcase == name.to_s.downcase
339
+ end
340
+
341
+ idxes
342
+ end
343
+ end
344
+
345
+ def include?(p_row)
346
+ @rows.each do |row|
347
+ return true if row.data == p_row.data
348
+ end
349
+
350
+ false
351
+ end
352
+
353
+ def default_migration_condition
354
+ flag = true
355
+ cols = @column_info.map { |col| col.name }
356
+ primary_key.each do |key|
357
+ flag = false unless cols.include? key
358
+ end
359
+
360
+ primary_key if flag
361
+ end
362
+
363
+ def convert_to_native_row(row)
364
+ @db.to_native_row(row, @column_info)
365
+ end
366
+
367
+ def size
368
+ @rows.size
369
+ end
370
+
371
+ def primary_key
372
+ @primary_key ||= @db.primary_key(@target_tbl_name)
373
+ end
374
+
375
+ private
376
+
377
+ def fill_with_nil(rows, num)
378
+ fill_row = Array.new(num)
379
+
380
+ rows.map do |row|
381
+ row + fill_row
382
+ end
383
+ end
384
+
385
+ def width
386
+ @column_info.size
387
+ end
388
+ end
389
+ end