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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/ChangeLog +41 -0
- data/Makefile +22 -0
- data/README.en.rdoc +118 -0
- data/Rakefile +9 -0
- data/bin/copy_nline +141 -0
- data/bin/mk_sqlskelton +158 -0
- data/db_suit_rails.gemspec +47 -0
- data/lib/db_suit_rails/db_suit_rails_error.rb +7 -0
- data/lib/db_suit_rails/sql_skelton.rb +1108 -0
- data/lib/db_suit_rails/sql_skelton/col_index.rb +435 -0
- data/lib/db_suit_rails/sql_skelton/fkey.rb +61 -0
- data/lib/db_suit_rails/sql_skelton/tbl_index.rb +310 -0
- data/lib/db_suit_rails/sql_skelton/utils.rb +56 -0
- data/test/sql_skelton/test_col_index.rb +224 -0
- data/test/sql_skelton/test_fkey.rb +48 -0
- data/test/sql_skelton/test_tbl_index.rb +145 -0
- data/test/test_sql_skelton.rb +245 -0
- metadata +83 -0
@@ -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,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
|
+
|