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,47 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rake'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{db_suit_rails}
7
+ s.version = "0.4.1"
8
+ # s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ %w(mk_sqlskelton copy_nline).each do |f|
10
+ s.executables << f
11
+ end
12
+ s.bindir = 'bin'
13
+ s.authors = ["Masa Sakano"]
14
+ s.date = %q{2018-03-31}
15
+ s.summary = %q{Database conversion to suit Ruby-on-Rails}
16
+ s.description = %q{Database conversion software to make it suit Ruby-on-Rails.}
17
+ # s.email = %q{abc@example.com}
18
+ s.extra_rdoc_files = [
19
+ # "LICENSE",
20
+ "README.en.rdoc",
21
+ ]
22
+ s.license = 'MIT'
23
+ s.files = FileList['.gitignore','lib/**/*.rb','[A-Z]*','test/**/*.rb', '*.gemspec', 'bin'].to_a.delete_if{ |f|
24
+ ret = false
25
+ arignore = IO.readlines('.gitignore')
26
+ arignore.map{|i| i.chomp}.each do |suffix|
27
+ if File.fnmatch(suffix, File.basename(f))
28
+ ret = true
29
+ break
30
+ end
31
+ end
32
+ ret
33
+ }
34
+ s.files.reject! { |fn| File.symlink? fn }
35
+ s.add_runtime_dependency 'rails'
36
+ # s.add_development_dependency "bourne", [">= 0"]
37
+ s.homepage = %q{https://www.wisebabel.com}
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+
40
+ # s.require_paths = ["lib"] # Default "lib"
41
+ s.required_ruby_version = '>= 2.0'
42
+ s.test_files = Dir['test/**/*.rb']
43
+ s.test_files.reject! { |fn| File.symlink? fn }
44
+ # s.requirements << 'libmagick, v6.0' # Simply, info to users.
45
+ # s.rubygems_version = %q{1.3.5} # This is always set automatically!!
46
+ end
47
+
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class DbSuitRailsError < StandardError
4
+ end
5
+
6
+ class DbSuitRailsFkeyError < StandardError
7
+ end
@@ -0,0 +1,1108 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Author: M. Sakano (Wise Babel Ltd)
4
+
5
+ require 'csv'
6
+ require 'active_support/all'
7
+ require 'db_suit_rails/db_suit_rails_error'
8
+ require 'db_suit_rails/sql_skelton/tbl_index'
9
+ require 'db_suit_rails/sql_skelton/col_index'
10
+
11
+ # =class SqlSkelton
12
+ #
13
+ # ==Summary
14
+ #
15
+ # Class for mapping between the original and modified SQL tables/columns
16
+ #
17
+ # ==Description
18
+ #
19
+ # ==Note
20
+ #
21
+ # "constraint names" are preserved, as they are guaranteed to be unique.
22
+ # Index made by CREATE INDEX is not taken into account;
23
+ # it may take a form like `TABLE_COLUMN_index`.
24
+ #
25
+ # -- Name: password_resets_email_index; Type: INDEX; Schema: public; Owner: seller
26
+ # CREATE INDEX password_resets_email_index ON password_resets USING btree (email);
27
+ #
28
+ class SqlSkelton
29
+
30
+ # Default mapping CSV file between the client table/column names and their couterparts in the server.
31
+ DefMappingCsvname = 'mapping.csv'
32
+
33
+ # Default parameters for SQL script for mapping between the client table names and their couterparts (Table and column names)
34
+ #
35
+ # This defines with the contents
36
+ # 1. the table name (key :tblname4tbl => "db_tbl_mappings" (Def)) that contains
37
+ # the mapping information for the table names for old and new data,
38
+ # as well as the column names in it, and
39
+ # 2. the table name (key :tblname4col => "db_col_mappings" (Def)) that contains
40
+ # the mapping information for the column names, where the former table is
41
+ # referred to with a foreign-key column (Def: "db_tbl_mapping_id").
42
+ #
43
+ # The prefixes for the column names (:colprefix4tbl => "tbl_" and :colprefix4col => "col_") and
44
+ # the suffixes for them (:colsuffix4from and :colsuffix4to) are also defined.
45
+ #
46
+ # Default parameters for SQL script for mapping between the client table names and their couterparts (Table and column names)
47
+ DefMappingSql = {
48
+ :tblname4tbl => 'db_tbl_mappings',
49
+ :tblname4col => 'db_col_mappings',
50
+ :colprefix4tbl => 'tbl', # => Column names: tbl.+
51
+ :colprefix4col => 'col', # => Column names: col.+
52
+ :colsuffix4from => '_from', # => Column names: tbl_from, col_from
53
+ :colsuffix4to => '_to', # => Column names: tbl_to, col_to
54
+ }
55
+
56
+ # Maximum number of bytes allowed for a column name in PostgreSQL
57
+ MaxColnameBytes = 63
58
+
59
+ # Output SQL file
60
+ attr_reader :outfile
61
+
62
+ # Output Mapping CSV file
63
+ attr_reader :mappingcsv
64
+
65
+ # Output Mapping SQL information (Hash-keys: :filename, :tblname, :sqlname)
66
+ attr_reader :mappingsql
67
+
68
+ # TblIndex instance for mapping database table names
69
+ attr_reader :tbl_index
70
+
71
+ # ColIndex instance for mapping database column names
72
+ attr_reader :col_index
73
+
74
+ # Constraint-Index instance for listing the constraint name
75
+ attr_reader :csrt_index
76
+
77
+
78
+ # Makes template mappingsql
79
+ #
80
+ # @return [Hash]
81
+ def mk_tmpl_mappingsql
82
+ hs = {
83
+ :filename => DefMappingCsvname.sub(/\.csv$/, ''),
84
+ :tblname => {
85
+ :tbl => DefMappingSql[:tblname4tbl],
86
+ :col => DefMappingSql[:tblname4col],
87
+ },
88
+ :colname => {
89
+ :tbl => {
90
+ :from => DefMappingSql[:colprefix4tbl] + DefMappingSql[:colsuffix4from],
91
+ :to => DefMappingSql[:colprefix4tbl] + DefMappingSql[:colsuffix4to],
92
+ },
93
+ :col => {
94
+ :from => DefMappingSql[:colprefix4col] + DefMappingSql[:colsuffix4from],
95
+ :to => DefMappingSql[:colprefix4col] + DefMappingSql[:colsuffix4to],
96
+ },
97
+ }
98
+ }
99
+ end
100
+ private :mk_tmpl_mappingsql
101
+
102
+
103
+ # Set up the basic parameters.
104
+ #
105
+ # For mappingsql Hash, the following is the specification:
106
+ #
107
+ # {
108
+ # :filename => String(Filename to output),
109
+ # :tblname => {
110
+ # :tbl => String(Table-name for table-name mapping), # the Foreign-key points to this.
111
+ # :col => String(Table-name for column-name mapping)
112
+ # },
113
+ # :colname => {
114
+ # :tbl => {
115
+ # :from => String(Column-name for the table-names of from-database),
116
+ # :to => String(Column-name for the table-names of to-database),
117
+ # },
118
+ # :col => {
119
+ # :from => String(Column-name for the column-names of from-database),
120
+ # :to => String(Column-name for the column-names of to-database),
121
+ # },
122
+ # }
123
+ # }
124
+ #
125
+ # @param infile [String, IO] Original SQL
126
+ # @param outfile [String, IO] Output filename (Def: INFILE_ROOT_ror.sql / out.sql)
127
+ # @param mappingcsv [String] Output mapping-CSV file (Def: mapping.csv)
128
+ # @param mappingsql [Hash] Output mapping-SQL file info (keys: :filename, :tblname[:tbl, :col], :colname[:tbl|:col => :from|:to])
129
+ def initialize(infile, outfile=nil, mappingcsv: DefMappingCsvname, mappingsql: {})
130
+ @outfile = outfile
131
+ if defined?(infile.read)
132
+ @strall = infile.read
133
+ @outfile ||= '_rails_db.sql'
134
+ else
135
+ @strall = File.read(infile)
136
+ @outfile ||= File.basename(infile, '.sql') + '_rails_db.sql'
137
+ end
138
+
139
+ @mappingcsv = mappingcsv
140
+ @mappingsql = mk_tmpl_mappingsql().merge(mappingsql)
141
+
142
+ @tbl_index = TblIndex.new
143
+ @col_index = ColIndex.new
144
+ @csrt_index = {} # { 'PRIMARY' => {'old_table' => []}, 'UNIQUE' => {} }
145
+ end
146
+
147
+ # Run
148
+ #
149
+ # @param dryrun [Boolean] Dryrun if True
150
+ # @param iow [IO,String] Output IO/Filename. In default, the instance variable is used.
151
+ # @param outmapping [String] Output CSV file.
152
+ # @param outmappingsql [Hash] Output SQL file info. See {#initialize} for specification.
153
+ # @param delete_primary [Boolean] Delete primary key definitions and add a new one.
154
+ # @param delete_sequence [Boolean] Delete all the Sequences.
155
+ # @param delete_trigger [Boolean] Delete all the triggers.
156
+ # @return [Array<String>] [output.sql, mapping.csv, nil or mapping.sql]
157
+ def run(dryrun: false, iow: @outfile, outmapping: @mappingcsv, outmappingsql: @mappingsql, delete_primary: true, delete_sequence: true, delete_trigger: true)
158
+ # @return [Array<String>] [output.sql, mapping.sql]
159
+ # is_dryrun = dryrun
160
+ read(stage: :refactoring, delete_primary: delete_primary, delete_sequence: delete_sequence, delete_trigger: delete_trigger)
161
+ read(stage: :indexing, delete_primary: delete_primary, delete_sequence: delete_sequence, delete_trigger: delete_trigger)
162
+ read(stage: :final, delete_primary: delete_primary, delete_sequence: delete_sequence, delete_trigger: delete_trigger)
163
+ if !dryrun
164
+ outsql = write_sql(iow: iow)
165
+ outmap = write_mapping(outfile: outmapping)
166
+ outmapsql = write_mappingsql(csvfile: outmap, mappingsql: outmappingsql)
167
+ end
168
+
169
+ return [outsql, outmap, outmapsql]
170
+ end
171
+
172
+ # Write SQL
173
+ #
174
+ # @param iow [IO,String] Output IO/Filename. In default, the instance variable is used.
175
+ # @param instr [String, NilClass] String to examine (Default: @strall)
176
+ # @return [String, IO] Output.sql
177
+ def write_sql(iow: @outfile, instr: @strall)
178
+ instr ||= @strall
179
+ close_iow = false
180
+ if defined? iow.sync
181
+ iow_out = iow
182
+ else
183
+ iow_out = open((iow || @outfile), 'w')
184
+ close_iow = true
185
+ end
186
+
187
+ begin
188
+ iow_out.print instr
189
+ ensure
190
+ iow_out.close if close_iow
191
+ end
192
+
193
+ return [@outfile, @mappingcsv]
194
+ end
195
+
196
+ # Write mapping CSV file
197
+ #
198
+ # The format is as follows:
199
+ #
200
+ # Old_Table,New_Table,Old_Column,New_Column
201
+ #
202
+ # Note this table is obviously not normalised.
203
+ #
204
+ # @param outfile [String] Output file.
205
+ # @param tbl_index [TblIndex] Table mapping source
206
+ # @param col_index [ColIndex] Column mapping source
207
+ # @return [String] mapping.csv
208
+ def write_mapping(outfile: @mappingcsv, tbl_index: @tbl_index, col_index: @col_index)
209
+
210
+ CSV.open(outfile, "w") do |csv|
211
+ col_index.colmaps.each_pair do |ea_tbl, ea_hscol|
212
+ ea_hscol[:order].each do |ea_col|
213
+ csv << [ea_tbl, tbl_index.newtblval(ea_tbl), ea_col, ea_hscol[ea_col][:name]]
214
+ end
215
+ end
216
+ end
217
+
218
+ return outfile
219
+ end
220
+
221
+
222
+ # Write mapping SQL file
223
+ #
224
+ # Note the tables are normalised.
225
+ #
226
+ # @param csvfile [String] Mapping CSV filename.
227
+ # @param mappingsql [Hash] Output SQL file info. See {#initialize} for specification.
228
+ # @return [String, NilClass] mapping.sql
229
+ def write_mappingsql(csvfile: @mappingcsv, mappingsql: @mappingsql)
230
+
231
+ return nil if ! mappingsql[:filename]
232
+
233
+ tmptbl = 'cloud_db_tbl_col_mappings'
234
+ tbl_ref_id_name = self.class.colname4id(mappingsql[:tblname][:tbl])
235
+
236
+ # @param mappingsql [Hash] Output mapping-SQL file info (keys: :filename, :tblname[:tbl, :col], :colname[:tbl|:col => :from|:to])
237
+ open(mappingsql[:filename], "w"){ |iow|
238
+ iow.print <<EOD
239
+ -- Creates a pair of normalised mapping tables for table and column names.
240
+
241
+ CREATE TABLE #{tmptbl} (
242
+ id SERIAL PRIMARY KEY,
243
+ #{mappingsql[:colname][:tbl][:from]} varchar(#{MaxColnameBytes}),
244
+ #{mappingsql[:colname][:tbl][:to]} varchar(#{MaxColnameBytes}),
245
+ #{mappingsql[:colname][:col][:from]} varchar(#{MaxColnameBytes}),
246
+ #{mappingsql[:colname][:col][:to]} varchar(#{MaxColnameBytes}),
247
+ UNIQUE (#{mappingsql[:colname][:tbl][:from]}, #{mappingsql[:colname][:col][:from]}),
248
+ UNIQUE (#{mappingsql[:colname][:tbl][:to]}, #{mappingsql[:colname][:col][:to]})
249
+ );
250
+
251
+ COPY cloud_db_tbl_col_mappings (#{mappingsql[:colname][:tbl][:from]}, #{mappingsql[:colname][:tbl][:to]}, #{mappingsql[:colname][:col][:from]}, #{mappingsql[:colname][:col][:to]}) FROM STDIN WITH (FORMAT 'csv', DELIMITER ',');
252
+ EOD
253
+
254
+ iow.print File.read(csvfile)
255
+
256
+ iow.print <<EOD
257
+ \\.
258
+
259
+ -- Normalising the tables.
260
+
261
+ CREATE TABLE IF NOT EXISTS #{mappingsql[:tblname][:tbl]} (
262
+ id SERIAL PRIMARY KEY,
263
+ #{mappingsql[:colname][:tbl][:from]} varchar(#{MaxColnameBytes}) UNIQUE NOT NULL,
264
+ #{mappingsql[:colname][:tbl][:to]} varchar(#{MaxColnameBytes}) UNIQUE NOT NULL
265
+ );
266
+
267
+ CREATE TABLE IF NOT EXISTS #{mappingsql[:tblname][:col]} (
268
+ id SERIAL PRIMARY KEY,
269
+ #{tbl_ref_id_name} integer REFERENCES #{mappingsql[:tblname][:tbl]} ON DELETE RESTRICT,
270
+ #{mappingsql[:colname][:col][:from]} varchar(#{MaxColnameBytes}) NOT NULL,
271
+ #{mappingsql[:colname][:col][:to]} varchar(#{MaxColnameBytes}) NOT NULL,
272
+ UNIQUE (#{tbl_ref_id_name}, #{mappingsql[:colname][:col][:from]}, #{mappingsql[:colname][:col][:to]})
273
+ );
274
+
275
+ INSERT INTO #{mappingsql[:tblname][:tbl]} (#{mappingsql[:colname][:tbl][:from]}, #{mappingsql[:colname][:tbl][:to]})
276
+ SELECT DISTINCT #{mappingsql[:colname][:tbl][:from]}, #{mappingsql[:colname][:tbl][:to]}
277
+ FROM #{tmptbl}
278
+ ORDER BY #{mappingsql[:colname][:tbl][:from]};
279
+
280
+ INSERT INTO #{mappingsql[:tblname][:col]} (#{tbl_ref_id_name}, #{mappingsql[:colname][:col][:from]}, #{mappingsql[:colname][:col][:to]})
281
+ SELECT t.id, tc.#{mappingsql[:colname][:col][:from]}, tc.#{mappingsql[:colname][:col][:to]}
282
+ FROM #{tmptbl} tc
283
+ INNER JOIN #{mappingsql[:tblname][:tbl]} t ON tc.#{mappingsql[:colname][:tbl][:from]} = t.#{mappingsql[:colname][:tbl][:from]}
284
+ ORDER BY t.id, tc.id;
285
+
286
+ -- Drop the un-normalized table.
287
+ DROP TABLE #{tmptbl};
288
+
289
+ EOD
290
+ }
291
+ return mappingsql[:filename]
292
+ end
293
+
294
+
295
+ # Add primary key
296
+ #
297
+ # Returns a String to setup a new primary key 'id'
298
+ #
299
+ # @param oldtbl [String] old table name
300
+ # @param newcol [String] new column name to add
301
+ # @param newtype [String] type for newcol
302
+ # @return [Array<SqlSkelton::Fkey, String, NilClass>] [Array[Fkey]|NilClass, updated_string]
303
+ def add_primary_key(oldtbl, newcol='id', newtype='bigint')
304
+ retstr = "ALTER TABLE #{oldtbl} ADD COLUMN #{newcol} #{newtype};\n"
305
+ # retstr << "ALTER TABLE #{oldtbl} ADD PRIMARY KEY #{newcol};\n"
306
+ retstr << "ALTER TABLE ONLY #{oldtbl} ADD CONSTRAINT #{@csrt_index['PRIMARY'][oldtbl]} PRIMARY KEY (#{newcol});\n"
307
+
308
+ return retstr
309
+ end
310
+
311
+ # Read column names
312
+ #
313
+ # Returns a 2-component array. The 1st element can be nil if no REFERENCES is
314
+ # found, and the 2nd element is the updated (or not-updated) text.
315
+ #
316
+ # 1. To check out "FOREIGN KEY" statement, the second parameter of oldtbl
317
+ # (for the table name of the current table) must be given.
318
+ # In that case, the 1st element of the returned array is
319
+ # an array of {SqlSkelton::Fkey}.
320
+ # 2. To check out inline "REFERENCES", the third parameter is mandatory.
321
+ #
322
+ # @param strin [String] String to evaluate
323
+ # @param oldtbl [String] old table name
324
+ # @param oldcol [String] old column name
325
+ # @return [Array<SqlSkelton::Fkey, String, NilClass>] [Array[Fkey]|NilClass, updated_string]
326
+ def get_foreign_keys(strin, oldtbl, oldcol=nil)
327
+ case strin
328
+ when /(FOREIGN\s+KEY\s*\(\s*)([\w\s,]+)(\)\s*REFERENCES\s+)([\w.]+)(\s*\(\s*)([\w\s,]+)(\))/i
329
+ ## FOREIGN KEY (b, c) REFERENCES other_table (c1, c2)
330
+ oldtbl || (raise "ERROR: oldtbl must be given for checking FOREIGN KEY: strin= #{strin}")
331
+ oldtbl_prt = $4
332
+ retstr = $` + $1
333
+ strnewcol, _, aroldcol = convert_multi_cols(oldtbl, $2, retall: true)
334
+ retstr << strnewcol << $3
335
+
336
+ strnewcol_prt, _, aroldcol_prt = convert_multi_cols(oldtbl_prt, $6, retall: true) # "_prt" for "Parent"
337
+
338
+ fkeys = []
339
+ aroldcol.each_with_index do |e_oldcol, i|
340
+ fkeys.push((self.class)::Fkey.new(oldtbl, e_oldcol, oldtbl_prt, aroldcol_prt[i]))
341
+ @col_index.update!(oldtbl, e_oldcol, fkey: fkeys[-1])
342
+ end
343
+
344
+ retstr << @tbl_index.updated_tbl!(oldtbl_prt) << $5 << strnewcol_prt << $7 << $'
345
+
346
+ return [fkeys, retstr]
347
+
348
+ when /\b(REFERENCES\s+)([\w.]+)((\s*\(\s*)(\w+)(\s*\)))?(\s*,\s*)?(--.*)?$/i
349
+ ## REFERENCES products (product_no),
350
+ oldtbl_prt = $2
351
+ newtbl_prt = @tbl_index.updated_tbl!(oldtbl_prt)
352
+ retstr = $` + $1 + newtbl_prt
353
+
354
+ if $3
355
+ fkey = (self.class)::Fkey.new(oldtbl, oldcol, oldtbl_prt, $5)
356
+ @col_index.update!(oldtbl, oldcol, fkey: fkey)
357
+ @col_index.update!(oldtbl_prt, $5)
358
+ newcol_prt = @col_index.updated_col!(oldtbl_prt, $5)
359
+ retstr << $4 << newcol_prt << $6 << ($7 || '') << ($8 || '')
360
+ else
361
+ retstr << $7 << $8
362
+ end
363
+ return [[fkey], retstr]
364
+
365
+ else
366
+ return [nil, strin]
367
+ end
368
+ end
369
+
370
+
371
+ # Read column names
372
+ #
373
+ # @param oldtbl [String] old table name
374
+ # @param eline [String] A line input
375
+ # @param stage [Symbol] (:refactoring|:indexing|:final)
376
+ # @param delete_primary [Boolean] Delete primary key definitions and add a new one.
377
+ # @param delete_sequence [Boolean] Delete all the Sequences.
378
+ # @param delete_trigger [Boolean] Delete all the triggers.
379
+ # @return [Array] [hsflag, revised_line, (',')] A comma is returned if the comman at the previous line should be deleted.
380
+ def read_col(oldtbl, eline, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
381
+ case eline
382
+ when /^\s*\)\s*;/
383
+ ## End of CREATE TABLE
384
+ return [get_hsflag_in_read($'), eline]
385
+ when /^(\s*)(--.*)?$/
386
+ ## Comment line
387
+ return [get_hsflag_in_read($', in_create: oldtbl), eline]
388
+ when /^(\s*)(\w+)/
389
+ word_pre, word1st, str2nd = $1, $2, $'
390
+
391
+ case stage
392
+ when :refactoring
393
+ return [get_hsflag_in_read(str2nd, in_create: oldtbl), eline]
394
+ end
395
+
396
+ ## id integer NOT NULL REFERENCES products (product_no),
397
+ # spaces = $1
398
+ # post_match = $'
399
+ case word1st.upcase
400
+ when 'FOREIGN'
401
+ ## FOREIGN KEY (b, c) REFERENCES other_table (c1, c2)
402
+ _, retstr = get_foreign_keys(eline, oldtbl)
403
+ return [get_hsflag_in_read(str2nd, in_create: oldtbl), retstr]
404
+ when 'CONSTRAINT', 'CHECK', 'PRIMARY', 'UNIQUE', 'EXCLUDE', 'DEFERRABLE', 'INITIALLY'
405
+ comma = nil
406
+ if delete_primary && :final == stage
407
+ eline2 = eline.sub(/\bPRIMARY KEY\s*\([^)]*\) *(,?)/i, ' ')
408
+ comma = ',' if (eline != eline2) && ($1.empty?)
409
+ eline = eline2
410
+ end
411
+ hsflag = get_hsflag_in_read(str2nd, in_create: oldtbl)
412
+ return [hsflag, eline, comma]
413
+ else
414
+ ## Column is found.
415
+ newcol = @col_index.updated_col!(oldtbl, word1st)
416
+ str1st = word_pre + newcol
417
+ if delete_primary && :final == stage
418
+ str2nd.sub!(/\bSERIAL(\s+PRIMARY\s+KEY)\b/i, 'int\1')
419
+ str2nd.sub!(/\s*\bPRIMARY KEY\b\s*/i, ' ')
420
+ end
421
+
422
+ ## Checks out the foreign key constraint
423
+ _, retstr = get_foreign_keys(str2nd, oldtbl, word1st)
424
+ return [get_hsflag_in_read(retstr, in_create: oldtbl), str1st + retstr]
425
+ end
426
+ else
427
+ raise "ERROR: Unsupported format of the line: #{eline}"
428
+ end
429
+ end
430
+
431
+
432
+ # Handle a table and many columns, returns the updated string.
433
+ #
434
+ # @param oldtbl [String] old table name
435
+ # @param oldcolsstr [String] comma-separated old column names
436
+ # @param retall [Boolean] if true, returns an [String, ArrayNew, ArrayOld] (Def: F, and String only)
437
+ # @return [String] revised string
438
+ def convert_multi_cols(oldtbl, oldcolsstr, retall: false)
439
+ oldcolsstr = oldcolsstr.sub(/^(\s*)/, '')
440
+ str_pre = $1
441
+ oldcolsstr.sub!(/(\s*)$/, '')
442
+ str_post = $1
443
+
444
+ aroldcol = oldcolsstr.split(/[\s,]+/)
445
+ arnewcol = aroldcol.map{ |i| @col_index.updated_col!(oldtbl, i) }
446
+
447
+ retstr = str_pre + arnewcol.join(', ') + str_post
448
+
449
+ retall ? (return [retstr, arnewcol, aroldcol]) : (return retstr)
450
+ end
451
+
452
+
453
+
454
+ # Returns an array of non-comments and comments
455
+ #
456
+ # If the returned array has an even number of elements,
457
+ # the next line must be inside a comment.
458
+ #
459
+ # @param instr [String] String to examine
460
+ # @param ar_beg [Array] The previous array (for recursive uses). Even number of elements.
461
+ # @return [Array] [Non-Comment1, Comment1, Non-Comment2, ...]
462
+ def split_onoff_comments(instr, ar_beg: [])
463
+ if /\/\*/ !~ instr
464
+ return ar_beg+[instr] # odd number of elements
465
+ end
466
+
467
+ ar_beg.push($`) # odd number of elements
468
+
469
+ rest = $'
470
+ if /\*\// !~ rest
471
+ ## It is inside an open-ended comment.
472
+ return ar_beg+['/*'+rest] # even number of elements
473
+ end
474
+
475
+ ## It contains a Comment, but the comment is closed.
476
+ ## Inspects further, recursively.
477
+ ar_beg.push('/*'+$'+'*/') # even number of elements
478
+ return not_in_comment($', ar_beg: ar_beg) # Either even or odd
479
+ end
480
+
481
+
482
+ # Search for the end of a comment in a given string
483
+ #
484
+ # Returns a two-element Array. If the-end-of-comment is not found,
485
+ # the 2nd element is nil, and 1st element is equal to instr.
486
+ #
487
+ # @param instr [String] String to examine
488
+ # @return [Array<String>] [Comment, Rest|nil]
489
+ def get_end_comment(instr)
490
+ if /(\*\/)/ !~ instr
491
+ return [instr, nil]
492
+ end
493
+ return [$`+$1, $']
494
+ end
495
+
496
+
497
+ # Gets hsflag for read() to reset it.
498
+ #
499
+ # If the later part of the line ends in an open-ended comment,
500
+ # this routine handles it.
501
+ #
502
+ # @param instr [String] String to examine
503
+ # @param command [String] Current command, e.g., ALTER
504
+ # @param hskwd [Object] Any keyword you want to preset.
505
+ # @return [Hash] hsflag
506
+ def get_hsflag_in_read(instr='', command='', **hskwd)
507
+ hsflag = {
508
+ :in_create => nil, # Inside CREATE
509
+ :in_comment => false, # multiple-line comment
510
+ :in_sentence => false, # multiple line
511
+ :from_stdin => false, # During COPY statement
512
+ :tbl_cur => nil, # Current Table to process
513
+ }
514
+ hsflag.merge!(hskwd)
515
+
516
+ if hsflag[:in_comment]
517
+ ## Inside a comment
518
+ return hsflag
519
+ end
520
+
521
+ if hsflag[:in_sentence]
522
+ hsflag[:in_sentence] = false if /(?<!\\)(?:\\\\)*;/ =~ instr ## '\;' is ignored.
523
+ end
524
+
525
+ # ar_onoff_comment = split_onoff_comments(instr)
526
+ # if ar_onoff_comment.size.even?
527
+ # ## It is in an open-ended comment.
528
+ # hsflag[:in_comment] = true
529
+ # ar_onoff_comment.pop
530
+ # end
531
+
532
+ # if /;\s*(--.*)?$/ !~ ar_onoff_comment[-1]
533
+ # ## The sentence is open-ended
534
+ # hsflag[:in_sentence] = command
535
+ # end
536
+
537
+ hsflag
538
+ end
539
+
540
+
541
+ # Add a new constraint index
542
+ #
543
+ # @param oldtbl [String] old table name
544
+ # @param constraint [String] unique name for the constraint
545
+ # @param kind_in [String] String, including spaces. eg, "PRIMARY KEY "
546
+ # @return [Array] [Index(eg. UNIQUE, PRIMARY), Index-Number-in-Array]
547
+ def push_csrt_index(oldtbl, constraint, kind_in=:unknown)
548
+ kind = (defined?(kind_in.strip) ? kind_in.strip.upcase.split[0] : kind_in) # UNIQUE, PRIMARY (single word only)
549
+
550
+ @csrt_index.has_key?(kind) || (@csrt_index[kind] = {})
551
+ @csrt_index[kind].has_key?(oldtbl) || (@csrt_index[kind][oldtbl] = [])
552
+
553
+ ind = @csrt_index[kind][oldtbl].find_index(constraint)
554
+ if ind
555
+ return [kind, ind]
556
+ else
557
+ @csrt_index[kind][oldtbl].push(constraint)
558
+ return [kind, @csrt_index[kind][oldtbl].size-1]
559
+ end
560
+ end
561
+
562
+
563
+ # Meta routines to run a child method
564
+ #
565
+ # @param id_str [String] String identifier (CREATE|ALTER)
566
+ # @param (see #read_alter)
567
+ # @return [Array] [Hash(hsflag), String(to_add), String|NilClass]
568
+ def read_child(id_str, *rest, **kwd)
569
+ #def read_child(id_str, *rest, stage: :refactoring)
570
+ case id_str
571
+ when 'ALTER'
572
+ return read_alter( *rest, **kwd)
573
+ when 'COPY'
574
+ return read_copy( *rest, **kwd)
575
+ when 'CREATE'
576
+ return read_create(*rest, **kwd)
577
+ when 'NAME'
578
+ return read_name( *rest, **kwd)
579
+ when 'SELECT'
580
+ return read_select(*rest, **kwd)
581
+ else
582
+ raise
583
+ end
584
+ end
585
+
586
+
587
+ # Sub-routine to read 'ALTER' statement.
588
+ #
589
+ # The third parameter in the returned array is, if nil, next should be invoked in the caller,
590
+ # else the loop parameter should be replaced and redo be invoked.
591
+ #
592
+ # @param ma_last [MatchData] Last match
593
+ # @param indices [Hash] Hash of indices (key: :oldtbl, :object etc) to indicate what object is indicated by which index in ma_last.
594
+ # @param stage [Symbol] (:refactoring|:indexing|:final)
595
+ # @param delete_primary [Boolean] Delete primary key definitions and add a new one.
596
+ # @param delete_sequence [Boolean] Delete all the Sequences.
597
+ # @param delete_trigger [Boolean] Delete all the triggers.
598
+ # @return [Array] [Hash(hsflag), String(to_add), String|NilClass]
599
+ def read_alter(ma_last, indices={}, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
600
+ oldtbl = ma_last[indices[:oldtbl]]
601
+ eline = ma_last.post_match
602
+
603
+ case stage
604
+ when :refactoring
605
+ to_add = ma_last[0] + eline
606
+ if /.*;/ =~ eline
607
+ hsflag = get_hsflag_in_read(eline)
608
+ else
609
+ hsflag = get_hsflag_in_read(eline, 'ALTER', in_sentence: 'ALTER', tbl_cur: oldtbl) ###### NOTE: Check get_hsflag_in_read() ########
610
+ to_add.chomp!
611
+ end
612
+
613
+ return [hsflag, to_add, nil]
614
+ end
615
+
616
+ newtbl = @tbl_index.updated_tbl!(oldtbl)
617
+
618
+ case stage
619
+ when :indexing
620
+ to_add = ma_last[0]
621
+
622
+ when :final
623
+ to_add = ma_last[1] + newtbl
624
+
625
+ else
626
+ raise
627
+ end
628
+
629
+ case eline
630
+ when /^(\s+ADD\s+CONSTRAINT\s+)([\w.]+)([\w\s]+)(\()([\w\s,]+)(\))/i
631
+ # $1 $2 $3 $4 $5 $6
632
+ ## ADD CONSTRAINT shop_b01unique UNIQUE (office_id, file_path, file_name, file_row);
633
+ ## ADD CONSTRAINT shop_client_pkey PRIMARY KEY (office_id, client_id);
634
+
635
+ com_option = $1 # ADD CONSTRAINT
636
+ constraint_id = $2 # shop_client_pkey
637
+ constraint_kind = $3 # PRIMARY KEY
638
+ spacer = $4 # (
639
+ constraint_key = $5 # office_id, client_id
640
+ tail_part = $6 + $' # );
641
+
642
+ push_csrt_index(oldtbl, constraint_id, constraint_kind) # Add in @csrt_index
643
+
644
+ to_add << com_option << constraint_id
645
+ case stage
646
+ when :indexing
647
+ to_add << constraint_kind << spacer << constraint_key
648
+ when :final
649
+ if delete_primary && /\bPRIMARY\s+KEY\b/i =~ constraint_kind
650
+ if constraint_key.strip.split(',').size <= 1
651
+ to_add = '-- ' + to_add
652
+ else
653
+ constraint_kind.sub!(/\bPRIMARY\s+KEY\b/i, 'UNIQUE')
654
+ end
655
+ end
656
+
657
+ strnewcol = convert_multi_cols(oldtbl, constraint_key, retall: false)
658
+ to_add << constraint_kind << spacer << strnewcol
659
+ else
660
+ raise
661
+ end
662
+
663
+ to_add << tail_part
664
+
665
+ return [get_hsflag_in_read($'), to_add, nil]
666
+
667
+ when /^(\s+ALTER\s+COLUMN\s+)([\w.]+)([\w\s]+\bnextval\(')(#{Regexp.quote(oldtbl)}_id_seq)(')/i
668
+ # $1 $2 $3 $4 $5
669
+ ## ALTER TABLE ONLY shop_b05 ALTER COLUMN id SET DEFAULT nextval('shop_b05_id_seq'::regclass);
670
+ newcol = @col_index.updated_col!(oldtbl, $2)
671
+ newseq = @tbl_index.updated_tbl!($4)
672
+
673
+ case stage
674
+ when :indexing
675
+ to_add << $&
676
+ when :final
677
+ to_add << $1 << newcol << $3 << newseq << $5
678
+ else
679
+ raise
680
+ end
681
+
682
+ to_add << $'
683
+
684
+ hsflag = get_hsflag_in_read($', in_sentence: 'ALTER', tbl_cur: oldtbl)
685
+ return [hsflag, to_add, nil]
686
+
687
+ else
688
+ to_add << eline
689
+ hsflag = get_hsflag_in_read(eline, in_sentence: 'ALTER', tbl_cur: oldtbl)
690
+ return [hsflag, to_add, nil]
691
+
692
+ end # case eline
693
+
694
+ end # def read_alter(ma_last, stage: :refactoring)
695
+
696
+
697
+ # Sub-routine to read 'COPY' statement.
698
+ #
699
+ # @param (see #read_alter)
700
+ # @return (see #read_alter)
701
+ def read_copy(ma_last, indices={}, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
702
+ oldtbl = ma_last[indices[:oldtbl]]
703
+ strcols= ma_last[indices[:oldcols]]
704
+ eline = ma_last.post_match
705
+
706
+ hsflag = get_hsflag_in_read(from_stdin: true)
707
+
708
+ case stage
709
+ when :refactoring
710
+ return [hsflag, ma_last[0] + eline, nil]
711
+ end
712
+
713
+ ## Note: ##
714
+ ## Everything that may need to be modified (from old table/column names to new ones)
715
+ ## for the returned string is included in the argument ma_last.
716
+
717
+ newtbl = @tbl_index.updated_tbl!(oldtbl)
718
+ strnewcol = convert_multi_cols(oldtbl, strcols, retall: false)
719
+
720
+ case stage
721
+ when :indexing
722
+ return [hsflag, ma_last[0] + eline, nil]
723
+
724
+ when :final
725
+ to_add = ma_last[1] + newtbl + ma_last[indices[:oldcols]-1] + strnewcol + ma_last[indices[:tail]] + eline
726
+ return [hsflag, to_add, nil]
727
+
728
+ else
729
+ raise
730
+ end
731
+ end # def read_copy(ma_last, indices={}, stage: :refactoring)
732
+
733
+
734
+ # Sub-routine to read 'CREATE' statement.
735
+ #
736
+ # @param (see #read_alter)
737
+ # @return (see #read_alter)
738
+ def read_create(ma_last, indices={}, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
739
+ oldtbl = ma_last[indices[:oldtbl]] # May not be a table name (see below (case-when-else clause))
740
+ object = ma_last[indices[:object]].upcase
741
+ eline = ma_last.post_match
742
+
743
+ case object
744
+ when 'SEQUENCE'
745
+ hsflag = get_hsflag_in_read(eline)
746
+ when 'TABLE'
747
+ if /(\s+\(\s*(--.*)?$)?/i !~ eline
748
+ raise DbSuitRailsError, "ERROR: Unsupported format: #{(ma_last[0]+eline).chomp}"
749
+ end
750
+ hsflag = get_hsflag_in_read($', in_create: oldtbl) # , tbl_cur: oldtbl)
751
+ else
752
+ # oldtbl is NOT a table name but a trigger name etc.
753
+ oldtbl = nil
754
+ end
755
+
756
+ case stage
757
+ when :refactoring
758
+ to_add = ma_last[0] + eline
759
+ hsflag ||= get_hsflag_in_read(eline)
760
+ return [hsflag, to_add, nil]
761
+ end
762
+
763
+ ## Only if SEQUENCE or TABLE (namely if hsflag is defined already), oldtbl is a genuine table name.
764
+ ## Note this is needed here to do indexing, if necessary.
765
+ to_add = (oldtbl ? (ma_last[1] + @tbl_index.updated_tbl!(oldtbl)) : ma_last[0])
766
+
767
+ case stage
768
+ when :indexing
769
+ to_add = ma_last[0] # resets.
770
+
771
+ when :final
772
+ if delete_primary && (object == 'TABLE')
773
+ ## Adds a new PRIMARY KEY named "id"
774
+ ## SERIAL from 1 to 2147483647 (cf. BIGSERIAL: 1 to 9223372036854775807)
775
+ eline += " id SERIAL PRIMARY KEY,\n"
776
+ end
777
+ else
778
+ raise
779
+ end
780
+
781
+ if hsflag # Either SEQUENCE or TABLE
782
+ to_add << eline
783
+ return [hsflag, to_add, nil]
784
+ end
785
+
786
+ case object
787
+ when 'TRIGGER'
788
+ ## CREATE TRIGGER tg01 BEFORE INSERT OR UPDATE ON shop_office_c FOR EACH ROW EXECUTE PROCEDURE tg_ins_upd_trriger();
789
+ if /^([\w+\s]+\bON\s+)([\w.]+)(\s+FOR\s+)/i !~ eline
790
+ raise DbSuitRailsError, "ERROR: Unsupported format: #{(ma_last[0]+eline).chomp}"
791
+ end
792
+
793
+ newtbl = @tbl_index.updated_tbl!($2)
794
+
795
+ case stage
796
+ when :indexing
797
+ to_add << $&
798
+ when :final
799
+ to_add << $1 << newtbl << $3
800
+ if delete_trigger
801
+ to_add = '-- ' + to_add
802
+ end
803
+ else
804
+ raise
805
+ end
806
+
807
+ to_add << $'
808
+
809
+ return [get_hsflag_in_read($'), to_add, nil]
810
+
811
+ when 'INDEX'
812
+ ## CREATE INDEX password_resets_email_index ON password_resets USING btree (email);
813
+ if /^(\s+ON\s+)([\w.]+)(\s+USING\b[\w\s]+\(\s*)(\w+)(\s*\)\s*;)/i !~ eline
814
+ # $1 $2 $3 $4 $5
815
+ raise DbSuitRailsError, "ERROR: Unsupported format: #{(ma_last[0]+eline).chomp}"
816
+ end
817
+
818
+ newtbl = @tbl_index.updated_tbl!($2)
819
+ newcol = @col_index.updated_col!($2, $4)
820
+
821
+ case stage
822
+ when :indexing
823
+ to_add << $&
824
+ when :final
825
+ to_add << $1 << newtbl << $3 << newcol << $5
826
+ else
827
+ raise
828
+ end
829
+
830
+ to_add << $'
831
+
832
+ return [get_hsflag_in_read($'), to_add, nil]
833
+
834
+ else
835
+
836
+ raise DbSuitRailsError, "ERROR: Unsupported format: #{(ma_last[0]+eline).chomp}"
837
+ end
838
+ end
839
+
840
+
841
+ # Sub-routine to read 'NAME' (comment-line).
842
+ #
843
+ # @param (see #read_alter)
844
+ # @return (see #read_alter)
845
+ def read_name(ma_last, indices={}, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
846
+ oldtbl = ma_last[indices[:oldtbl]] # May not be a table name, depending on the type.
847
+ oldcol = ma_last[indices[:second]] # May be nil
848
+ eline = ma_last.post_match
849
+ hsflag = get_hsflag_in_read() # Because it is in a comment line anyway.
850
+
851
+ case stage
852
+ when :refactoring
853
+ to_add = ma_last[0] + eline
854
+ return [hsflag, to_add, nil]
855
+ end
856
+
857
+ ## Note: ##
858
+ ## Everything that may need to be modified (from old table/column names to new ones)
859
+ ## for the returned string is included in the argument ma_last.
860
+ ## They depend on the type, which is examined below.
861
+
862
+ if /^(\s*;\s*Type:\s+)(TABLE|SEQUENCE|DEFAULT|CONSTRAINT|TRIGGER)(\b(?:[\w\s.]*);)/i !~ eline
863
+ # $1 $2 $3
864
+ ## -- Name: shop_b01; Type: TABLE; Schema: public; Owner: seller
865
+ ## -- Data for Name: migrations; Type: TABLE DATA; Schema: public; Owner: seller
866
+ ## -- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: seller
867
+ ## -- Name: migrations migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: seller
868
+ ## -- Name: shop_b01 shop_b01unique; Type: CONSTRAINT; Schema: public; Owner: seller
869
+ ## -- Name: shop_b01 id; Type: DEFAULT; Schema: public; Owner: seller
870
+ ## -- Name: shop_office_c tg01; Type: TRIGGER; Schema: public; Owner: seller
871
+ #### The following is skipped, deliberately.
872
+ ## -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
873
+ ## -- Name: tg_ins_upd_trriger(); Type: FUNCTION; Schema: public; Owner: seller
874
+ return [hsflag, ma_last[0] + eline, nil]
875
+ end
876
+
877
+ case $2.upcase
878
+ when 'TABLE', 'SEQUENCE', 'DEFAULT'
879
+ newtbl = @tbl_index.updated_tbl!(oldtbl)
880
+ newcol = @col_index.updated_col!(oldtbl, oldcol) if oldcol
881
+
882
+ when 'CONSTRAINT', 'TRIGGER'
883
+ newtbl = @tbl_index.updated_tbl!(oldtbl)
884
+ newcol = oldcol ## Constraint/Trigger identifier etc.
885
+
886
+ else
887
+ return [get_hsflag_in_read(), ma_last[0] + eline, nil]
888
+ end
889
+
890
+ case stage
891
+ when :indexing
892
+ to_add = ma_last[0]
893
+ when :final
894
+ tmpstr = (newcol ? (ma_last[indices[:second]-1] + newcol) : '')
895
+ to_add = ma_last[1] + newtbl + tmpstr
896
+ else
897
+ raise "ERROR: stage=(#{stage.inspect})"
898
+ end
899
+
900
+ to_add << eline
901
+
902
+ return [hsflag, to_add, nil]
903
+
904
+ end # def read_name(ma_last, indices={}, stage: :refactoring)
905
+
906
+
907
+ # Sub-routine to read 'SELECT' statement.
908
+ #
909
+ # @param (see #read_alter)
910
+ # @return (see #read_alter)
911
+ def read_select(ma_last, indices={}, stage: :refactoring, delete_primary: true, delete_sequence: true, delete_trigger: true)
912
+ oldtbl = ma_last[indices[:oldtbl]]
913
+ eline = ma_last.post_match
914
+ hsflag = get_hsflag_in_read(eline)
915
+
916
+ case stage
917
+ when :refactoring
918
+ return [hsflag, ma_last[0] + eline, nil]
919
+ end
920
+
921
+ ## Note: ##
922
+ ## Everything that may need to be modified (from old table/column names to new ones)
923
+ ## for the returned string is included in the argument ma_last.
924
+
925
+ newtbl = @tbl_index.updated_tbl!(oldtbl)
926
+
927
+ case stage
928
+ when :indexing
929
+ return [hsflag, ma_last[0] + eline, nil]
930
+
931
+ when :final
932
+ to_add = ma_last[1] + newtbl + ma_last[indices[:tail]] + eline
933
+ return [hsflag, to_add, nil]
934
+
935
+ else
936
+ raise
937
+ end
938
+ end # def read_select(ma_last, indices={}, stage: :refactoring)
939
+
940
+
941
+ RexSqls = {
942
+ 'COPY' => /^(\s*(COPY)\s+)([\w.]+)(\s+\()([\w\s,]+)(\)\s+FROM\s+stdin(?:\s+WITH \([^)]*\))?\s*;)/i,
943
+ # $1(long)$2 $3 $4 $5 $6
944
+ 'SELECT' => /^(\s*(SELECT)\s+[\w.]+\.setval\(')(\w+)(')/i,
945
+ # $1(long)$2 $3 $4
946
+ 'NAME' => /^(\s*--+\s*(?:Data for\s+)?(Name):\s+)([\w.]+)(?:(\s*)([\w.]+))?/i,
947
+ # $1(long) $2 $3 $4 $5
948
+ 'CREATE' => /^(\s*(CREATE)\s+(TABLE|SEQUENCE|TRIGGER|INDEX)(\s+))([\w.]+)/i,
949
+ # $1(long)$2 $3 $4 $5
950
+ 'ALTER' => /^(\s*(ALTER)\s+(TABLE|SEQUENCE)(\s+ONLY)?(\b\s*))([\w.]+)/i,
951
+ # $1(long)$2 $3 $4 $5 $6
952
+ }
953
+
954
+ MatIndices = {
955
+ 'COPY' => {:oldtbl => 3, :oldcols => 5, :tail => 6},
956
+ 'SELECT' => {:oldtbl => 3, :tail => 4},
957
+ 'NAME' => {:oldtbl => 3, :second => 5},
958
+ 'CREATE' => {:oldtbl => 5, :object => 3},
959
+ 'ALTER' => {:oldtbl => 6},
960
+ }
961
+
962
+ # Main routine to read the input file and modifies it internally.
963
+ #
964
+ # Returns modified @strall (and set if specified) and sets @tbl_index and @col_index
965
+ #
966
+ # This assumes several formats, such as:
967
+ #
968
+ # /^CREATE TABLE migrations \(
969
+ # *[\w\s]+,
970
+ # *[\w\s]+,
971
+ # *[\w\s]+
972
+ # \);/
973
+ #
974
+ # In short, this script does not process a sentence straddling over multiple lines as one
975
+ # in most cases.
976
+ #
977
+ # Three stages are available:
978
+ #
979
+ # 1. :refactoring for refactroing the input string, where split lines of ALTER are connected,
980
+ # 2. :indexing to take into account the foreign keys, if there are any,
981
+ # 3. :final for producing the final output string.
982
+ #
983
+ # You should run this method 3 times each with a different stage parameter
984
+ # specified in this order.
985
+ #
986
+ # Note at the moment in :final all the variables are still attempted to be updated,
987
+ # which is a bit of waste.
988
+ #
989
+ # @param stage [Symbol] (:refactoring|:indexing|:final)
990
+ # @param instr [String, NilClass] String to examine (Default: @strall)
991
+ # @param setstr [Boolean, NilClass] if True (Default unless instr is specified), the result is written to the instance variable.
992
+ # @param delete_primary [Boolean] Delete primary key definitions and add a new one.
993
+ # @param delete_sequence [Boolean] Delete all the Sequences.
994
+ # @param delete_trigger [Boolean] Delete all the triggers.
995
+ # @return [String]
996
+ def read(stage: :refactoring, instr: nil, setstr: nil, delete_primary: true, delete_sequence: true, delete_trigger: true)
997
+
998
+ setstr = true if (!instr && setstr.nil?)
999
+
1000
+ # @see get_hsflag_in_read()
1001
+ hsflag = get_hsflag_in_read()
1002
+
1003
+ strret = ''
1004
+
1005
+ (instr || @strall).each_line do |eline|
1006
+ ret_readsql = catch(:readsql){
1007
+
1008
+ if hsflag[:in_comment]
1009
+ str_comment, eline = get_end_comment(eline)
1010
+ strret += str_comment
1011
+ eline ? redo : next
1012
+ end
1013
+
1014
+ if hsflag[:from_stdin]
1015
+ strret += eline
1016
+ hsflag[:from_stdin] = false if /^\\\./ =~ eline
1017
+ next
1018
+ end
1019
+
1020
+ if hsflag[:in_create]
1021
+ ## Inside CREATE TABLE
1022
+ hsflag, str_revised, comma = read_col(hsflag[:in_create], eline, stage: stage, delete_primary: delete_primary, delete_sequence: delete_sequence, delete_trigger: delete_trigger)
1023
+ case stage
1024
+ when :refactoring, :indexing
1025
+ strret += eline
1026
+ next
1027
+ when :final
1028
+ if comma == ','
1029
+ ## After PRIMARY KEY is deleted, the comman in the previous line should now be deleted.
1030
+ strret.sub!(/,\s*\Z/, '')
1031
+ end
1032
+ strret += str_revised
1033
+ next
1034
+ else
1035
+ raise "ERROR: stage=(#{stage.inspect})"
1036
+ end
1037
+ end
1038
+
1039
+ if hsflag[:in_sentence] && /ALTER/i =~ hsflag[:in_sentence]
1040
+ case stage
1041
+ when :refactoring
1042
+ strret.chomp! # To connect this sentence to the previous one.
1043
+ strret += eline
1044
+ hsflag2 = get_hsflag_in_read(eline, in_sentence: hsflag[:in_sentence])
1045
+ if ! hsflag2[:in_sentence]
1046
+ hsflag[:in_sentence] = hsflag2[:in_sentence]
1047
+ end
1048
+ next
1049
+
1050
+ # if /.*;/ =~ eline
1051
+ # strret += $&
1052
+ # eline = $'
1053
+ # hsflag[:in_sentence] = nil
1054
+ # hsflag.has_key?(:tbl_cur) && (hsflag[:tbl_cur] = nil)
1055
+ # redo
1056
+ # else
1057
+ # strret += eline
1058
+ # next
1059
+ # end
1060
+ else
1061
+ raise DbSuitRailsError, "ERROR: stage must be specified as :refactoring first while setstr option is set as true (or refactoring of an ALTER line somehow has failed...): #{eline.chomp}"
1062
+ end
1063
+ end
1064
+
1065
+ ## In_sentence like inside ALTER or CREATE SEQUENCE
1066
+ if hsflag[:in_sentence]
1067
+ strret += eline
1068
+ hsflag = get_hsflag_in_read(eline, in_sentence: hsflag[:in_sentence])
1069
+ next
1070
+ end
1071
+
1072
+ ## Main routine
1073
+ RexSqls.each_pair do |ea_key, ea_rex|
1074
+ matched = ea_rex.match(eline)
1075
+ if matched
1076
+ hsflag, to_add, remaining = read_child(ea_key, matched, MatIndices[ea_key], stage: stage, delete_primary: delete_primary, delete_sequence: delete_sequence, delete_trigger: delete_trigger)
1077
+ strret += to_add
1078
+ throw :readsql, remaining
1079
+ end
1080
+ end
1081
+
1082
+ strret += eline
1083
+ next
1084
+ } # ret_readsql = catch(:readsql){
1085
+
1086
+ eline = (ret_readsql || next) # == Variable remaining (in the loop)
1087
+ redo
1088
+
1089
+ end # (instr || @strall).each_line do |eline|
1090
+
1091
+ @strall = strret if setstr
1092
+
1093
+ return strret
1094
+ end # def read(update_index: true)
1095
+
1096
+
1097
+ # Returns the column name to referencing as a foreign key the primary key of the given table.
1098
+ #
1099
+ # Following the Rails convention. Requires ActiveSupport of Rails.
1100
+ #
1101
+ # @param tblname [String]
1102
+ # @return [String]
1103
+ def self.colname4id(tblname)
1104
+ tblname.singularize + '_id'
1105
+ end
1106
+
1107
+ end # class SqlSkelton
1108
+