db_suit_rails 0.4.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,435 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Author: M. Sakano (Wise Babel Ltd)
4
+
5
+ require 'rubygems'
6
+ require 'active_support/all'
7
+ require 'db_suit_rails/sql_skelton/utils' # class SqlSkelton is defined.
8
+ require 'db_suit_rails/db_suit_rails_error'
9
+ require 'db_suit_rails/sql_skelton/fkey'
10
+
11
+ # =class SqlSkelton::ColIndex
12
+ #
13
+ # ==Summary
14
+ #
15
+ # Class for mapping between the original and modified SQL tables/columns
16
+ #
17
+ # ==Description
18
+ #
19
+ # This holds the information of old and new names for each column and
20
+ # whether it is referenced or not, and if it is, from which old table/column.
21
+ #
22
+ # When you specify something, it is always the old table or column name.
23
+ #
24
+ # ==Example
25
+ #
26
+ # ci = SqlSkelton::ColIndex.new(tables) # Columns are not set.
27
+ # ci.update!(tables[0], column1)
28
+ # ci.update!(tables[0], 'tax_id')
29
+ # ci.update!(tables[0], 'job_id')
30
+ #
31
+ # # Foreign key (you could have done it in the sentnce above).
32
+ # fk_tax = SqlSkelton::Fkey.new(tables[0], 'tax_id', tables[1], 'eid')
33
+ # ci.update!(tables[0], 'tax_id', fkey: fk_tax)
34
+ #
35
+ # # Manually sets
36
+ # ci.set_newcolval_with(oldtbl, oldcol, new_value)
37
+ # new_col_name = newcolval(oldtbl, oldcol)
38
+ #
39
+ # fk_job = SqlSkelton::Fkey.new(tables[0], 'job_id', tables[2], 'fid')
40
+ # ci.set_fkeyval_with(tables[0], 'job_id', fk_job)
41
+ # ci.fkey?( tables[0], 'job_id') # => true
42
+ # fk_job_refer = fkeyval(tables[0], 'job_id')
43
+ # ci.update!(tables[0], 'job_id') # fkey option is unnecessary.
44
+ #
45
+ #
46
+ class SqlSkelton::ColIndex
47
+
48
+ # Constant SuffixId is defined.
49
+ include Object.const_get(parent_name)::Utils # parent_name defined in active_support/all
50
+
51
+ # List of the reserved Field names.
52
+ # Note it is also a bad idea to use a name identical with a method name of Method (check with ActiveRecord.singleton_methods)
53
+ # @see http://www.rubymagic.org/posts/ruby-and-rails-reserved-words
54
+ ReservedColumnNames = %w(created_at created_on updated_at updated_on deleted_at lock_version type id position parent_id lft rgt quote_value)
55
+
56
+ # Hash of Hash of Hash:
57
+ # { Old-Table-Name =>
58
+ # { :order => [colname1, colname2, ...], # Holding the order of columns
59
+ # Old-Column-Name =>
60
+ # { :name => New_colname, :fkey => Fkey|nil } } }
61
+ attr_reader :colmaps
62
+
63
+ # Constructor
64
+ #
65
+ # Table list is NOT mandatory to specify.
66
+ #
67
+ # @param oldtbls [Array,String] Old table name(s)
68
+ def initialize(oldtbls=[])
69
+ @colmaps = {}
70
+
71
+ [oldtbls].flatten.map{|i| remove_sqlprefix(i)}.uniq.each do |ec|
72
+ push(ec)
73
+ end
74
+ end
75
+
76
+ # Whether empty
77
+ #
78
+ def empty?()
79
+ @colmaps.empty?
80
+ end
81
+
82
+ # Whether a old-name table is registered?
83
+ #
84
+ # @param oldtbl [String] Old table name
85
+ def table_exist?(oldtbl)
86
+ @colmaps.has_key?(remove_sqlprefix(oldtbl))
87
+ end
88
+
89
+ # Whether a old-name column is registered?
90
+ #
91
+ # @param oldtbl [String] Old table name
92
+ # @param oldcol [String] Old column name
93
+ def column_registered?(oldtbl, oldcol)
94
+ oldtbl = remove_sqlprefix(oldtbl)
95
+ table_exist?(oldtbl) && @colmaps[oldtbl].has_key?(oldcol)
96
+ end
97
+
98
+
99
+ # Gets a stored newcol name
100
+ #
101
+ # @param oldtbl [String] Old table name
102
+ # @param oldcol [String] Candidate new column name
103
+ # @return [String] Stored new column name
104
+ def newcolval(oldtbl, oldcol)
105
+ oldtbl = remove_sqlprefix(oldtbl)
106
+ @colmaps[oldtbl][oldcol][:name]
107
+ end
108
+
109
+ # Sets a newcol name
110
+ #
111
+ # @param oldtbl [String] Old table name
112
+ # @param oldcol [String] Candidate new column name
113
+ # @param value [String] new column name to set
114
+ # @return [String] Stored new column name
115
+ def set_newcolval_with(oldtbl, oldcol, value)
116
+ oldtbl = remove_sqlprefix(oldtbl)
117
+ @colmaps[oldtbl][oldcol][:name] = value
118
+ end
119
+
120
+
121
+ # Returns a foreign key (SqlSkelton::Fkey object) or nil
122
+ #
123
+ # @param oldtbl [String] Old table name
124
+ # @param oldcol [String] Old column name
125
+ def fkeyval(oldtbl, oldcol)
126
+ oldtbl = remove_sqlprefix(oldtbl)
127
+ column_registered?(oldtbl, oldcol) || (return nil)
128
+ @colmaps[oldtbl][oldcol][:fkey]
129
+ end
130
+
131
+ # Returns a foreign key (SqlSkelton::Fkey object) or nil
132
+ #
133
+ # @param oldtbl [String] Old table name
134
+ # @param oldcol [String] Old column name
135
+ # @param fkey [SqlSkelton::Fkey, NilClass] Foreign key
136
+ def set_fkeyval_with(oldtbl, oldcol, fkey)
137
+ oldtbl = remove_sqlprefix(oldtbl)
138
+ @colmaps[oldtbl][oldcol][:fkey] = fkey
139
+ end
140
+
141
+ # Whether a old-name column is a foreign key?
142
+ #
143
+ # @param oldtbl [String] Old table name
144
+ # @param oldcol [String] Old column name
145
+ def fkey?(oldtbl, oldcol)
146
+ oldtbl = remove_sqlprefix(oldtbl)
147
+ !!(fkeyval(oldtbl, oldcol))
148
+ end
149
+
150
+
151
+ # Push a new column (and/or table)
152
+ #
153
+ # Make sure to use this method to add a record in @colmaps .
154
+ # Or, alternatively, you can use {#update!}, which calls this method,
155
+ # and sets newcol automatically.
156
+ #
157
+ # If the record already exists, this raises an error.
158
+ #
159
+ # @param oldtbl [String] Old table name
160
+ # @param oldcol [String] Old column name
161
+ # @param newcol [String] New column name
162
+ # @param fkey [SqlSkelton::Fkey, NilClass] Foreign key or (Default) nil
163
+ # @return [self]
164
+ def push(oldtbl, oldcol=nil, newcol=nil, fkey: nil)
165
+ oldtbl = remove_sqlprefix(oldtbl)
166
+ if table_exist?(oldtbl)
167
+ oldcol || (raise DbSuitRailsError, "ERROR: Failed to push old-table (#{oldtbl}) (with no column specified) as it already exists.")
168
+ else
169
+ @colmaps[oldtbl] = { :order => [] }
170
+ # (raise "ERROR: Failed to push column (#{oldcol}) as old-table (#{oldtbl}) does not exist.")
171
+ end
172
+
173
+ !oldcol && (return self)
174
+
175
+ column_registered?(oldtbl, oldcol) && (raise DbSuitRailsError, "ERROR: Failed to push old-column (#{oldcol}) in old-table (#{oldtbl}), as it exists.")
176
+
177
+ @colmaps[oldtbl][oldcol] = { :name => newcol, :fkey => fkey }
178
+ @colmaps[oldtbl][:order].push(oldcol)
179
+ self
180
+ end
181
+
182
+ # Update a column (or all the columns)
183
+ #
184
+ # If oldcol is :all, all the column are updated, in which case
185
+ # fkey option is ignored.
186
+ #
187
+ # This updates a newcol name only if fkey is set or given or
188
+ # if the existing one is empty or nil.
189
+ #
190
+ # If force: option is true, regardless of fkey and current newcol name,
191
+ # newcol is updated.
192
+ #
193
+ # @param oldtbl [String] Old table name
194
+ # @param oldcol [String, Symbol] Old column name, or :all
195
+ # @param fkey [SqlSkelton::Fkey, NilClass] Foreign key or (Default) nil
196
+ # @param force forcibly updates.
197
+ # @return [self]
198
+ def update(oldtbl, oldcol, fkey: nil, force: false)
199
+ oldtbl = remove_sqlprefix(oldtbl)
200
+ if :all == oldcol
201
+ oldcolnames(oldtbl).each do |ec_col|
202
+ update(oldtbl, ec_col, force: force)
203
+ end
204
+ return self
205
+ end
206
+
207
+ column_registered?(oldtbl, oldcol) || (raise DbSuitRailsError, "ERROR: Failed to update column (#{oldcol}) in old-table (#{oldtbl}), which may not exist.")
208
+
209
+ cur_fkey = (fkey || fkeyval(oldtbl, oldcol))
210
+
211
+ newcol = newcolval(oldtbl, oldcol)
212
+ if !newcol || newcol.empty?
213
+ set_newcolval_with(oldtbl, oldcol, mk_newcolname(oldtbl, oldcol))
214
+
215
+ ## Foreign key has not been taken into account. Therefore it is now.
216
+ cur_fkey && (return update(oldtbl, oldcol, fkey: fkey))
217
+ return self
218
+ end
219
+
220
+ ## If force, attempts to update newcol name regardless the other conditions.
221
+ force && set_newcolval_with(oldtbl, oldcol, mk_newcolname(oldtbl, oldcol))
222
+
223
+ cur_fkey || (return self)
224
+ set_fkeyval_with(oldtbl, oldcol, cur_fkey) ## Maybe unchanged.
225
+
226
+ case oldcol
227
+ when /^id$/i
228
+ newn = unique_colname(oldtbl, oldcol, 'id' + SuffixId + '_id', useprefix: true)
229
+ set_newcolval_with(oldtbl, oldcol, newn)
230
+ return self
231
+ when /_id$/i
232
+ # New column name must end in _id if a foreign key.
233
+ # And in fact the name-root has to be the singular form of the table name
234
+ # which references, AND its colum name must be 'id' (as in 2 characters).
235
+ return self if /_id$/i =~ newcol
236
+
237
+ # Old column name ends in _id, but the new one does not.
238
+ # Because foreign key is defined, it should.
239
+ # In other words, newcol must be identical to oldcol, as long as available
240
+ set_newcolval_with(oldtbl, oldcol, unique_colname(oldtbl, oldcol))
241
+ return self
242
+ else
243
+ # Does nothing.
244
+ return self
245
+ end
246
+ end
247
+
248
+
249
+ # Updates a column or pushes it if it is not registered, yet.
250
+ #
251
+ # Basically, this method is a wrapper for {#update} and {#push}
252
+ #
253
+ # This updates a newcol name regardless the conditions (force option is set).
254
+ #
255
+ # If oldcol is :all, all the column are updated, in which case
256
+ # fkey option is ignored.
257
+ #
258
+ # @param oldtbl [String] Old table name
259
+ # @param oldcol [String, Symbol] Old column name or :all . Must not be nil.
260
+ # @param fkey [SqlSkelton::Fkey, NilClass] Foreign key or (Default) nil
261
+ # @return [self]
262
+ #
263
+ # @see SqlSkelton::ColIndex.update
264
+ def update!(oldtbl, oldcol, fkey: nil)
265
+ raise DbSuitRailsError, "ERROR: oldtbl (#{oldtbl.inspect}) must be String." if ! oldtbl
266
+ oldtbl = remove_sqlprefix(oldtbl)
267
+
268
+ if !oldcol
269
+ table_exist?(oldtbl) || push(oldtbl, oldcol, fkey: fkey)
270
+ warn "WARNING: non-nil fkey is ignored when the specified column is nil in ColIndex#update!." if fkey
271
+ return self
272
+ end
273
+
274
+ ## Note:
275
+ ## Somehow, the following sentence followed by simply update(..., force: true) does not work.
276
+ ## and so the rescue sentence seems essential.
277
+ #
278
+ # column_registered?(oldtbl, oldcol) || push(oldtbl, oldcol, fkey: fkey)
279
+
280
+ begin
281
+ return update(oldtbl, oldcol, fkey: fkey, force: true)
282
+ rescue DbSuitRailsError
283
+ end
284
+ push(oldtbl, oldcol, fkey: fkey)
285
+ update(oldtbl, oldcol, force: true)
286
+ end
287
+
288
+
289
+ # Wrapper of {#update!}
290
+ #
291
+ # It returns the new column-name.
292
+ # Also, it does NOT update the record, if the new file is already registered.
293
+ #
294
+ # @param oldtbl [String] Old table name
295
+ # @param oldcol [String, Symbol] Old column name or :all
296
+ # @param fkey [SqlSkelton::Fkey, NilClass] Foreign key or (Default) nil
297
+ # @return [String]
298
+ #
299
+ # @see SqlSkelton::ColIndex.update
300
+ def updated_col!(oldtbl, oldcol, fkey: nil)
301
+ oldtbl = remove_sqlprefix(oldtbl)
302
+ if ! column_registered?(oldtbl, oldcol)
303
+ push(oldtbl, oldcol, fkey: fkey)
304
+ update(oldtbl, oldcol, force: true)
305
+ end
306
+
307
+ newcolval(oldtbl, oldcol)
308
+ end
309
+
310
+
311
+ # Returns an Array of Hash of column names { :old => old, :new => new }
312
+ #
313
+ # Returned array is sorted.
314
+ #
315
+ # @param oldtbl [String] Old table name
316
+ # @return [Array[Hash]]
317
+ def paircolnames(oldtbl)
318
+ oldtbl = remove_sqlprefix(oldtbl)
319
+ table_exist?(oldtbl) || (raise DbSuitRailsError, "ERROR: Old-table (#{oldtbl}) does not exist.")
320
+ arret = []
321
+ @colmaps[oldtbl].select{ |k, v| defined? k.gsub }.each_pair do |k, v|
322
+ # (:order => [Array]) is excluded by hash#select
323
+
324
+ arret.push( { :old => k, :new => v[:name] } )
325
+ end
326
+ arret.sort { |a,b|
327
+ @colmaps[oldtbl][:order].find_index(a[:old]) <=>
328
+ @colmaps[oldtbl][:order].find_index(b[:old])
329
+ }
330
+ end
331
+
332
+
333
+ # Returns an Array of old column names.
334
+ #
335
+ # @param oldtbl [String] Old table name
336
+ # @param exclude [String] Old column name, the corresponding value of which either becomes nil in, or if compact option is true, is excluded from, the result.
337
+ # @param compact [Boolean] Read only when exclude option is non-nil. If true (Def), the result is compacted; else the excluded value becomes nil.
338
+ # @return [Array[Hash]]
339
+ def oldcolnames(oldtbl, exclude: nil, compact: true)
340
+ oldtbl = remove_sqlprefix(oldtbl)
341
+ old_or_newcolnames(:old, oldtbl, exclude: exclude, compact: compact)
342
+ end
343
+
344
+
345
+ # Returns an Array of new column names.
346
+ #
347
+ # @param (see #oldcolnames)
348
+ def newcolnames(oldtbl, exclude: nil, compact: true)
349
+ oldtbl = remove_sqlprefix(oldtbl)
350
+ old_or_newcolnames(:new, oldtbl, exclude: exclude, compact: compact)
351
+ end
352
+
353
+
354
+ # Returns an Array of mixed column names == (new OR old)
355
+ #
356
+ # @param (see #oldcolnames)
357
+ def mixcolnames(oldtbl, exclude: nil, compact: true)
358
+ oldtbl = remove_sqlprefix(oldtbl)
359
+ newcol = newcolnames(oldtbl, exclude: exclude, compact: compact)
360
+ oldcol = oldcolnames(oldtbl, exclude: exclude, compact: compact)
361
+
362
+ mixcol = []
363
+ newcol.each_with_index do |newc, i|
364
+ mixcol[i] = (newc || oldcol[i])
365
+ end
366
+ mixcol
367
+ end
368
+
369
+
370
+ #######################################################
371
+ private
372
+ #######################################################
373
+
374
+ # Gets a unique new column name.
375
+ #
376
+ # If the CANDIDATE fails, it searches for CANDIDATE1, CANDIDATE2, ... etc.
377
+ # Or, n1_CANDIDATE, n2_CANDIDATE1, ... etc, if useprefix option is true.
378
+ #
379
+ # @param oldtbl [String] Old table name
380
+ # @param oldcol [String] Old column name
381
+ # @param col_root [String, NilClass] Candidate new column name. If unspecified, copied from oldcol
382
+ # @param useprefix [Boolean] If true, add a prefix as opposed to suffix.
383
+ # @return [String] Unique new column name
384
+ def unique_colname(oldtbl, oldcol, col_root=nil, useprefix: false)
385
+ oldtbl = remove_sqlprefix(oldtbl)
386
+ col_root ||= oldcol
387
+ case col_root
388
+ when oldtbl+'count', *ReservedColumnNames
389
+ col_root += SuffixId
390
+ end
391
+ existing_cols = mixcolnames(oldtbl, exclude: oldcol)
392
+
393
+ # unique_name_cand defined in Utils module.
394
+ newcol = unique_name_cand(col_root, useprefix: useprefix){ |cand| ! existing_cols.include?(cand) }
395
+ newcol && (return(newcol))
396
+
397
+ raise "ERROR: Failed to find a new column name for candidate column-name(#{col_root} in (old) Table(#{oldtbl})."
398
+ end
399
+
400
+ # Gets a non-existing new column name
401
+ #
402
+ # This is a low-level method, and does not take into account {#fkey?}.
403
+ # In practice, use {#update!} etc.
404
+ #
405
+ # @param oldtbl [String] Old table name
406
+ # @param oldcol [String] Old column name (Template for the new column name)
407
+ # @param useprefix [Boolean] If true, add a prefix as opposed to suffix.
408
+ # @return [String] Unique new column name
409
+ def mk_newcolname(oldtbl, oldcol, useprefix: false)
410
+ oldtbl = remove_sqlprefix(oldtbl)
411
+ case oldcol
412
+ when /^id$/i
413
+ return unique_colname(oldtbl, oldcol, 'id' + SuffixId, useprefix: useprefix)
414
+ when /_id$/i
415
+ newcol_cand = (fkey?(oldtbl, oldcol) ? oldcol : (oldcol + SuffixId))
416
+ return unique_colname(oldtbl, oldcol, newcol_cand, useprefix: useprefix)
417
+ else
418
+ return oldcol
419
+ end
420
+ end
421
+
422
+ # Core routine for {#oldcolnames} and {#newcolnames}
423
+ #
424
+ # @param old_or_new [Symbol] :old or :new.
425
+ # @param (see #oldcolnames)
426
+ def old_or_newcolnames(old_or_new, oldtbl, exclude: nil, compact: true)
427
+ oldtbl = remove_sqlprefix(oldtbl)
428
+ pairs = paircolnames(oldtbl)
429
+ arret = pairs.map{|i| (exclude == i[:old]) ? nil : i[old_or_new]}
430
+ return arret if !(exclude && compact)
431
+ arret.delete_at( pairs.find_index{ |val| exclude == val[:old] } )
432
+ arret
433
+ end
434
+
435
+ end
@@ -0,0 +1,61 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Author: M. Sakano (Wise Babel Ltd)
4
+
5
+ require 'active_support/all'
6
+ require 'db_suit_rails/sql_skelton/utils' # class SqlSkelton is defined.
7
+
8
+ # =class SqlSkelton::Fkey
9
+ #
10
+ # ==Summary
11
+ #
12
+ # Class for mapping a foreign key relation
13
+ #
14
+ # ==Description
15
+ #
16
+ # Three methods only. To access it, via child or parent.
17
+ #
18
+ class SqlSkelton::Fkey
19
+
20
+ # Method remove_sqlprefix() is defined.
21
+ include Object.const_get(parent_name)::Utils # parent_name defined in active_support/all
22
+
23
+ # Access the child via
24
+ #
25
+ # fkey.child[:tbl] # => Child table name
26
+ # fkey.child[:col] # => Child column name
27
+ attr_reader :child
28
+
29
+ # Access the parent via
30
+ #
31
+ # fkey.parent[:tbl] # => Parent table name
32
+ # fkey.parent[:col] # => Parent column name
33
+ attr_reader :parent
34
+
35
+ # Set up the basic parameters.
36
+ #
37
+ # @param tbl_child [String] Table referenced to
38
+ # @param col_child [String] Column referenced to
39
+ # @param tbl_parent [String] Table referenced from
40
+ # @param col_parent [String] Column referenced from
41
+ def initialize(tbl_child, col_child, tbl_parent, col_parent)
42
+ tbl_child = remove_sqlprefix(tbl_child)
43
+ tbl_parent = remove_sqlprefix(tbl_parent)
44
+ @child = { :tbl => tbl_child, :col => col_child }
45
+ @parent = { :tbl => tbl_parent, :col => col_parent }
46
+ if !(@child[:tbl] && @child[:col] && @parent[:tbl] && @parent[:col])
47
+ msg = sprintf('child(tbl|col) = (%s | %s) parent(tbl|col) = (%s | %s)', @child[:tbl].inspect, @child[:col].inspect, @parent[:tbl].inspect, @parent[:col].inspect)
48
+ raise DbSuitRailsFkeyError, 'None of the table/column should be nil.: '+msg
49
+ end
50
+ end
51
+
52
+ # Equality implementation
53
+ #
54
+ def ==(obj)
55
+ begin
56
+ (@child == obj.child) && (@parent == obj.parent)
57
+ rescue
58
+ false
59
+ end
60
+ end
61
+ end