xmigra 1.5.1 → 1.6.0
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 +4 -4
- data/lib/xmigra/db_support/mssql.rb +43 -0
- data/lib/xmigra/db_support/psql.rb +45 -0
- data/lib/xmigra/declarative_migration.rb +160 -0
- data/lib/xmigra/declarative_support/table.rb +590 -0
- data/lib/xmigra/declarative_support.rb +158 -0
- data/lib/xmigra/impdecl_migration_adder.rb +249 -0
- data/lib/xmigra/migration.rb +10 -3
- data/lib/xmigra/migration_chain.rb +22 -8
- data/lib/xmigra/migration_conflict.rb +27 -0
- data/lib/xmigra/new_access_artifact_adder.rb +44 -0
- data/lib/xmigra/new_index_adder.rb +33 -0
- data/lib/xmigra/new_migration_adder.rb +10 -6
- data/lib/xmigra/permission_script_writer.rb +11 -5
- data/lib/xmigra/program.rb +231 -23
- data/lib/xmigra/schema_updater.rb +28 -5
- data/lib/xmigra/vcs_support/git.rb +189 -8
- data/lib/xmigra/vcs_support/svn.rb +107 -1
- data/lib/xmigra/version.rb +1 -1
- data/lib/xmigra.rb +47 -2
- data/test/git_vcs.rb +64 -4
- data/test/new_files.rb +14 -0
- data/test/runner.rb +49 -4
- data/test/structure_declarative.rb +811 -0
- data/test/utils.rb +17 -2
- metadata +10 -2
@@ -0,0 +1,590 @@
|
|
1
|
+
require 'xmigra/declarative_support'
|
2
|
+
|
3
|
+
module XMigra
|
4
|
+
module DeclarativeSupport
|
5
|
+
class Table
|
6
|
+
include ImpdeclMigrationAdder::SupportedDatabaseObject
|
7
|
+
for_declarative_tagged "!table"
|
8
|
+
|
9
|
+
def self.decldoc
|
10
|
+
<<END_OF_HELP
|
11
|
+
The "!table" tag declares a table within the database using two standard top-
|
12
|
+
level keys: a required "columns" key and an optional "constraints" key.
|
13
|
+
|
14
|
+
The value of the "columns" key is a sequence of mappings, each giving "name"
|
15
|
+
and "type". The value of the "type" key should be a type according to the
|
16
|
+
database system in use. The "nullable" key (with a default value of true) can
|
17
|
+
map to false to indicate that the column should not accept null values. The
|
18
|
+
key "primary key", whose value is interpreted as a Boolean, can be used to
|
19
|
+
indicate a primary key without using the more explicit "constraints" syntax.
|
20
|
+
Including a "default" key indicates a default constraint on the column, where
|
21
|
+
the value of the key is an expression to use for computing the default value
|
22
|
+
(which may be constrained by the database system in use).
|
23
|
+
|
24
|
+
The value of the "constraints" key is a mapping from constraint name to
|
25
|
+
constraint definition (itself a mapping). The constraint type can either be
|
26
|
+
explicit through use of the "type" key in the constraint definition or
|
27
|
+
implicit through a prefix used to start the constraint name (and no explicit
|
28
|
+
constraint type). The available constraint types are:
|
29
|
+
|
30
|
+
Explicit type Implicit prefix
|
31
|
+
------------- ---------------
|
32
|
+
primary key PK_
|
33
|
+
unique UQ_
|
34
|
+
foreign key FK_
|
35
|
+
check CK_
|
36
|
+
default DF_
|
37
|
+
|
38
|
+
Primary key and unique constraint definitions must have a "columns" key that
|
39
|
+
is a sequence of column names. Only one primary key constraint may be
|
40
|
+
specified, whether through use of "primary key" keys in column mappings or
|
41
|
+
explicitly in the "constraints" section. For foreign key constraint
|
42
|
+
definitions, the value of the "columns" key must be a mapping of referring
|
43
|
+
column name to referenced column name. Check constraint definitions must have
|
44
|
+
a "verify" key whose value is an SQL expression to be checked for all records.
|
45
|
+
Default constraints (when given explicitly) must have a "value" key giving
|
46
|
+
the expression (with possible limitations imposed by the database system in
|
47
|
+
use) for the default value and an indication of the constrained column: either
|
48
|
+
a "column" key giving explicit reference to a column or, if the constraint
|
49
|
+
name starts with the implicit prefix, the part of the constraint name after
|
50
|
+
the prefix.
|
51
|
+
|
52
|
+
When specifying SQL expressions in YAML, make sure to use appropriate quoting.
|
53
|
+
For example, where apostrophes delimit string literals in SQL and it might be
|
54
|
+
tempting to write a default expression with only one set of apostrophes around
|
55
|
+
it, YAML also uses apostrophes (or "single quotes") to mark a scalar value
|
56
|
+
(a string unless otherwise tagged), and so the apostrophes are consumed when
|
57
|
+
the YAML is parsed. Either use a triple-apostrophe (YAML consumes the first,
|
58
|
+
converts the pair of second and third into a single apostrophe, and does the
|
59
|
+
reverse sequence at the end of the scalar) or use a block scalar (literal or
|
60
|
+
folded), preferably chopped (i.e. "|-" or ">-"). The rule is: whatever YAML
|
61
|
+
parsing sees as the value of the scalar, that is the SQL expression to be
|
62
|
+
used.
|
63
|
+
|
64
|
+
Extended information may be added to any standard-structure mapping in the
|
65
|
+
declarative document by using any string key beginning with "X-" (the LATIN
|
66
|
+
CAPITAL LETTER X followed by a HYPHEN-MINUS). All other keys are reserved for
|
67
|
+
future expansion and may cause an error when generating implementing SQL.
|
68
|
+
END_OF_HELP
|
69
|
+
#'
|
70
|
+
end
|
71
|
+
|
72
|
+
class Column
|
73
|
+
SPEC_ATTRS = [:name, :type]
|
74
|
+
|
75
|
+
def initialize(col_spec)
|
76
|
+
@primary_key = !!col_spec['primary key']
|
77
|
+
@nullable = !!col_spec.fetch('nullable', true)
|
78
|
+
SPEC_ATTRS.each do |a|
|
79
|
+
instance_variable_set("@#{a}".to_sym, col_spec[a.to_s])
|
80
|
+
end
|
81
|
+
if default = col_spec['default']
|
82
|
+
@default_constraint = DefaultConstraint.new(
|
83
|
+
"DF_#{name}",
|
84
|
+
StructureReader.new({
|
85
|
+
'column'=>name,
|
86
|
+
'value'=>default
|
87
|
+
})
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_accessor *SPEC_ATTRS
|
93
|
+
attr_accessor :default_constraint
|
94
|
+
|
95
|
+
def primary_key?
|
96
|
+
@primary_key
|
97
|
+
end
|
98
|
+
def primary_key=(value)
|
99
|
+
@primary_key = value
|
100
|
+
end
|
101
|
+
|
102
|
+
def nullable?
|
103
|
+
@nullable
|
104
|
+
end
|
105
|
+
def nullable=(value)
|
106
|
+
@nullable = value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Constraint
|
111
|
+
SUBTYPES = []
|
112
|
+
def self.inherited(subclass)
|
113
|
+
SUBTYPES << subclass
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.each_type(&blk)
|
117
|
+
SUBTYPES.each(&blk)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.type_by_identifier(identifier)
|
121
|
+
SUBTYPES.find {|t| t.const_defined?(:IDENTIFIER) && t::IDENTIFIER == identifier}
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.bad_spec(message)
|
125
|
+
raise SpecificationError, message
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.deserialize(name, constr_spec)
|
129
|
+
constraint_type = constr_spec['type'] || implicit_type(name) || bad_spec(
|
130
|
+
"No type specified (or inferrable) for constraint #{name}"
|
131
|
+
)
|
132
|
+
constraint_type = Constraint.type_by_identifier(constraint_type) || bad_spec(
|
133
|
+
%Q{Unknown constraint type "#{constraint_type}" for constraint #{name}}
|
134
|
+
)
|
135
|
+
|
136
|
+
constraint_type.new(name, constr_spec)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.implicit_type(name)
|
140
|
+
return if name.nil?
|
141
|
+
Constraint.each_type.find do |type|
|
142
|
+
next unless type.const_defined?(:IMPLICIT_PREFIX)
|
143
|
+
break type::IDENTIFIER if name.start_with?(type::IMPLICIT_PREFIX)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def constraint_type
|
148
|
+
self.class::IDENTIFIER.gsub(' ', '_').to_sym
|
149
|
+
end
|
150
|
+
|
151
|
+
def initialize(name, constr_spec)
|
152
|
+
@name = name
|
153
|
+
end
|
154
|
+
|
155
|
+
attr_accessor :name
|
156
|
+
|
157
|
+
def only_on_column_at_creation?
|
158
|
+
false
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
def creation_name_sql
|
163
|
+
return "" if name.nil?
|
164
|
+
"CONSTRAINT #{name} "
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class ColumnListConstraint < Constraint
|
169
|
+
def initialize(name, constr_spec)
|
170
|
+
super(name, constr_spec)
|
171
|
+
@columns = get_and_validate_columns(constr_spec)
|
172
|
+
end
|
173
|
+
|
174
|
+
attr_reader :columns
|
175
|
+
|
176
|
+
def constrained_colnames
|
177
|
+
columns
|
178
|
+
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
def get_and_validate_columns(constr_spec)
|
182
|
+
(constr_spec.array_fetch('columns', ->(c) {c['name']}) || Constraint.bad_spec(
|
183
|
+
%Q{#{self.class::IDENTIFIER} constraint #{name} must specify columns}
|
184
|
+
)).tap do |cols|
|
185
|
+
unless cols.kind_of? Array
|
186
|
+
Constraint.bad_spec(
|
187
|
+
%Q{#{self.class::IDENTIFIER} constraint #{@name} expected "columns" to be a sequence (Array)}
|
188
|
+
)
|
189
|
+
end
|
190
|
+
if cols.uniq.length < cols.length
|
191
|
+
Constraint.bad_spec(
|
192
|
+
%Q{#{self.class::IDENTIFIER} constraint #{@name} has one or more duplicate columns}
|
193
|
+
)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
class PrimaryKey < ColumnListConstraint
|
200
|
+
IDENTIFIER = "primary key"
|
201
|
+
IMPLICIT_PREFIX = "PK_"
|
202
|
+
|
203
|
+
def creation_sql
|
204
|
+
creation_name_sql + "PRIMARY KEY (#{constrained_colnames.join(', ')})"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class UniquenessConstraint < ColumnListConstraint
|
209
|
+
IDENTIFIER = "unique"
|
210
|
+
IMPLICIT_PREFIX = "UQ_"
|
211
|
+
|
212
|
+
def creation_sql
|
213
|
+
creation_name_sql + "UNIQUE (#{constrained_colnames.join(', ')})"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class ForeignKey < ColumnListConstraint
|
218
|
+
IDENTIFIER = "foreign key"
|
219
|
+
IMPLICIT_PREFIX = "FK_"
|
220
|
+
|
221
|
+
def initialize(name, constr_spec)
|
222
|
+
super(name, constr_spec)
|
223
|
+
@referent = constr_spec['link to'] || Constraint.bad_spec(
|
224
|
+
%Q{Foreign key constraint #{@name} does not specify "link to" (referent)}
|
225
|
+
)
|
226
|
+
@update_rule = constr_spec['on update'].tap {|v| break v.upcase if v}
|
227
|
+
@delete_rule = constr_spec['on delete'].tap {|v| break v.upcase if v}
|
228
|
+
end
|
229
|
+
|
230
|
+
attr_accessor :referent, :update_rule, :delete_rule
|
231
|
+
|
232
|
+
def constrained_colnames
|
233
|
+
columns.keys
|
234
|
+
end
|
235
|
+
|
236
|
+
def referenced_colnames
|
237
|
+
columns.values
|
238
|
+
end
|
239
|
+
|
240
|
+
def creation_sql
|
241
|
+
"".tap do |result|
|
242
|
+
result << creation_name_sql
|
243
|
+
result << "FOREIGN KEY (#{constrained_colnames.join(', ')})"
|
244
|
+
result << "\n REFERENCES #{referent} (#{referenced_colnames.join(', ')})"
|
245
|
+
if update_rule
|
246
|
+
result << "\n ON UPDATE #{update_rule}"
|
247
|
+
end
|
248
|
+
if delete_rule
|
249
|
+
result << "\n ON DELETE #{delete_rule}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
protected
|
255
|
+
def get_and_validate_columns(constr_spec)
|
256
|
+
(constr_spec.raw_item('columns') || Constraint.bad_spec(
|
257
|
+
%Q{#{self.class::IDENTIFIER} constraint #{name} must specify columns}
|
258
|
+
)).tap do |cols|
|
259
|
+
unless cols.kind_of? Hash
|
260
|
+
Constraint.bad_spec(
|
261
|
+
%Q{Foreign key constraint #{@name} expected "columns" to be a mapping (Hash) referrer -> referent}
|
262
|
+
)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class CheckConstraint < Constraint
|
269
|
+
IDENTIFIER = 'check'
|
270
|
+
IMPLICIT_PREFIX = 'CK_'
|
271
|
+
|
272
|
+
def initialize(name, constr_spec)
|
273
|
+
super(name, constr_spec)
|
274
|
+
@expression = constr_spec['verify'] || Constraint.bad_spec(
|
275
|
+
%Q{Check constraint #{name} does not specify an expression to "verify"}
|
276
|
+
)
|
277
|
+
end
|
278
|
+
|
279
|
+
attr_accessor :expression
|
280
|
+
|
281
|
+
def creation_sql
|
282
|
+
creation_name_sql + "CHECK (#{expression})"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
class DefaultConstraint < Constraint
|
287
|
+
IDENTIFIER = 'default'
|
288
|
+
IMPLICIT_PREFIX = 'DF_'
|
289
|
+
|
290
|
+
def initialize(name, constr_spec)
|
291
|
+
super(name, constr_spec)
|
292
|
+
implicit_column = (
|
293
|
+
name[IMPLICIT_PREFIX.length..-1] if name.start_with?(IMPLICIT_PREFIX)
|
294
|
+
)
|
295
|
+
@column = constr_spec['column'] || implicit_column || Constraint.bad_spec(
|
296
|
+
%Q{Default constraint #{name} does not specify a "column"}
|
297
|
+
)
|
298
|
+
@expression = constr_spec['value'] || Constraint.bad_spec(
|
299
|
+
%Q{Default constraint #{name} does not specify an expression to use as a "value"}
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
attr_accessor :column, :expression
|
304
|
+
|
305
|
+
def only_on_column_at_creation?
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
def creation_sql
|
310
|
+
creation_name_sql + "DEFAULT #{expression} FOR #{column}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def initialize(name, structure)
|
315
|
+
structure = StructureReader.new(structure)
|
316
|
+
@name = name
|
317
|
+
constraints = {}
|
318
|
+
@columns_by_name = (structure.array_fetch('columns', ->(c) {c['name']}) || raise(
|
319
|
+
SpecificationError,
|
320
|
+
"No columns specified for table #{@name}"
|
321
|
+
)).inject({}) do |result, item|
|
322
|
+
column = Column.new(item)
|
323
|
+
result[column.name] = column
|
324
|
+
|
325
|
+
if !(col_default = column.default_constraint).nil?
|
326
|
+
constraints[col_default.name] = col_default
|
327
|
+
end
|
328
|
+
|
329
|
+
result
|
330
|
+
end
|
331
|
+
@primary_key = columns.select(&:primary_key?).tap do |cols|
|
332
|
+
break nil if cols.empty?
|
333
|
+
pk = PrimaryKey.new(
|
334
|
+
"PK_#{name.gsub('.', '_')}",
|
335
|
+
StructureReader.new({'columns'=>cols.map(&:name)})
|
336
|
+
)
|
337
|
+
break (constraints[pk.name] = pk)
|
338
|
+
end
|
339
|
+
@constraints = (structure['constraints'] || []).inject(constraints) do |result, name_spec_pair|
|
340
|
+
constraint = Constraint.deserialize(*name_spec_pair)
|
341
|
+
|
342
|
+
if result.has_key?(constraint.name)
|
343
|
+
raise SpecificationError, "Constraint #{constraint.name} is specified multiple times"
|
344
|
+
end
|
345
|
+
|
346
|
+
result[constraint.name] = constraint
|
347
|
+
|
348
|
+
# Link DefaultConstraints to their respective Columns
|
349
|
+
# because the constraint object is needed for column creation
|
350
|
+
if constraint.kind_of? DefaultConstraint
|
351
|
+
unless (col = get_column(constraint.column)).default_constraint.nil?
|
352
|
+
raise SpecificationError, "Default constraint #{constraint.name} attempts to constrain #{constraint.column} which already has a default constraint"
|
353
|
+
end
|
354
|
+
col.default_constraint = constraint
|
355
|
+
end
|
356
|
+
|
357
|
+
result
|
358
|
+
end
|
359
|
+
errors = []
|
360
|
+
@constraints.each_value do |constraint|
|
361
|
+
if constraint.kind_of? PrimaryKey
|
362
|
+
unless @primary_key.nil? || constraint.equal?(@primary_key)
|
363
|
+
raise SpecificationError, "Multiple primary keys specified"
|
364
|
+
end
|
365
|
+
@primary_key = constraint
|
366
|
+
end
|
367
|
+
if constraint.kind_of? ColumnListConstraint
|
368
|
+
unknown_cols = constraint.constrained_colnames.reject do |colname|
|
369
|
+
has_column?(colname)
|
370
|
+
end
|
371
|
+
unless unknown_cols.empty?
|
372
|
+
errors << "#{constraint.class::IDENTIFIER} constraint #{constraint.name} references unknown column(s): #{unknown_cols.join(', ')}"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
structure.each_unused_standard_key do |k|
|
378
|
+
errors << "Unrecognized standard key #{k.join('.')}"
|
379
|
+
end
|
380
|
+
|
381
|
+
unless errors.empty?
|
382
|
+
raise SpecificationError, errors.join("\n")
|
383
|
+
end
|
384
|
+
|
385
|
+
@extensions = structure.each_extension.inject({}) do |result, kv_pair|
|
386
|
+
key, value = kv_pair
|
387
|
+
result[key] = value
|
388
|
+
result
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
attr_accessor :name
|
393
|
+
attr_reader :constraints, :extensions
|
394
|
+
|
395
|
+
def columns
|
396
|
+
@columns_by_name.values
|
397
|
+
end
|
398
|
+
|
399
|
+
def get_column(name)
|
400
|
+
@columns_by_name[name]
|
401
|
+
end
|
402
|
+
|
403
|
+
def has_column?(name)
|
404
|
+
@columns_by_name.has_key? name
|
405
|
+
end
|
406
|
+
|
407
|
+
def add_default(colname, expression, constr_name=nil)
|
408
|
+
col = get_column(colname)
|
409
|
+
unless col.default_constraint.nil?
|
410
|
+
raise "#{colname} already has a default constraint"
|
411
|
+
end
|
412
|
+
constr_name ||= "DF_#{colname}"
|
413
|
+
if constraints[constr_name]
|
414
|
+
raise "Constraint #{constr_name} already exists"
|
415
|
+
end
|
416
|
+
constraints[constr_name] = col.default_constraint = DefaultConstraint.new(
|
417
|
+
constr_name,
|
418
|
+
StructureReader.new({
|
419
|
+
'column'=>colname,
|
420
|
+
'value'=>expression,
|
421
|
+
})
|
422
|
+
)
|
423
|
+
end
|
424
|
+
|
425
|
+
def creation_sql
|
426
|
+
"CREATE TABLE #{name} (\n" + \
|
427
|
+
table_creation_items.map {|item| " #{item.creation_sql}"}.join(",\n") + "\n" + \
|
428
|
+
");"
|
429
|
+
end
|
430
|
+
|
431
|
+
def table_creation_items
|
432
|
+
table_items = []
|
433
|
+
table_items.concat(columns
|
434
|
+
.map {|col| ColumnCreationFragment.new(self, col)}
|
435
|
+
)
|
436
|
+
table_items.concat(constraints.values
|
437
|
+
.reject(&:only_on_column_at_creation?)
|
438
|
+
)
|
439
|
+
end
|
440
|
+
|
441
|
+
def sql_to_effect_from(old_state)
|
442
|
+
delta = Delta.new(old_state, self)
|
443
|
+
parts = []
|
444
|
+
|
445
|
+
# Remove constraints
|
446
|
+
parts.concat remove_table_constraints_sql_statements(
|
447
|
+
delta.constraints_to_drop
|
448
|
+
)
|
449
|
+
|
450
|
+
# Add new columns
|
451
|
+
parts.concat add_table_columns_sql_statements(
|
452
|
+
delta.new_columns.lazy.map {|col| [col.name, col.type]}
|
453
|
+
).to_a
|
454
|
+
|
455
|
+
# Alter existing columns
|
456
|
+
parts.concat alter_table_columns_sql_statements(
|
457
|
+
delta.altered_column_pairs
|
458
|
+
).to_a
|
459
|
+
|
460
|
+
# Remove columns
|
461
|
+
parts.concat remove_table_columns_sql_statements(
|
462
|
+
delta.removed_columns.lazy.map(&:name)
|
463
|
+
).to_a
|
464
|
+
|
465
|
+
# Add constraints
|
466
|
+
parts.concat add_table_constraints_sql_statements(
|
467
|
+
delta.new_constraint_sql_clauses
|
468
|
+
).to_a
|
469
|
+
|
470
|
+
(extensions.keys + old_state.extensions.keys).uniq.sort.each do |ext_key|
|
471
|
+
case
|
472
|
+
when extensions.has_key?(ext_key) && !old_state.extensions.has_key?(ext_key)
|
473
|
+
parts << "-- TODO: New extension #{ext_key}"
|
474
|
+
when old_state.extensions.has_key?(ext_key) && !extensions.has_key?(ext_key)
|
475
|
+
parts << "-- TODO: Extension #{ext_key} removed"
|
476
|
+
else
|
477
|
+
parts << "-- TODO: Modification to extension #{ext_key}"
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
return parts.join("\n")
|
482
|
+
end
|
483
|
+
|
484
|
+
class ColumnCreationFragment
|
485
|
+
def initialize(table, column)
|
486
|
+
@table = table
|
487
|
+
@column = column
|
488
|
+
end
|
489
|
+
|
490
|
+
attr_reader :column
|
491
|
+
|
492
|
+
def creation_sql
|
493
|
+
@table.column_creation_sql_fragment(@column)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
class Delta
|
498
|
+
def initialize(old_state, new_state)
|
499
|
+
@constraints_to_drop = []
|
500
|
+
@new_constraint_sql_clauses = []
|
501
|
+
|
502
|
+
# Look for constraints from old_state that are removed and gather
|
503
|
+
# constraint creation SQL
|
504
|
+
old_constraint_sql = old_state.constraints.each_value.inject({}) do |result, constr|
|
505
|
+
if new_state.constraints.has_key? constr.name
|
506
|
+
result[constr.name] = constr.creation_sql
|
507
|
+
else
|
508
|
+
@constraints_to_drop << constr.name
|
509
|
+
end
|
510
|
+
|
511
|
+
result
|
512
|
+
end
|
513
|
+
|
514
|
+
# Look for constraints that are new to or altered in new_state
|
515
|
+
new_state.constraints.each_value do |constr|
|
516
|
+
if old_constraint_sql.has_key? constr.name
|
517
|
+
if old_constraint_sql[constr.name] != (crt_sql = constr.creation_sql)
|
518
|
+
@constraints_to_drop << constr.name
|
519
|
+
@new_constraint_sql_clauses << crt_sql
|
520
|
+
end
|
521
|
+
else
|
522
|
+
new_constraint_sql_clauses << constr.creation_sql
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Look for new and altered columns
|
527
|
+
@new_columns = []
|
528
|
+
@altered_column_pairs = []
|
529
|
+
new_state.columns.each do |col|
|
530
|
+
if !old_state.has_column? col.name
|
531
|
+
@new_columns << col
|
532
|
+
elsif new_state.column_alteration_occurs?(old_col = old_state.get_column(col.name), col)
|
533
|
+
@altered_column_pairs << [old_col, col]
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# Look for removed columns
|
538
|
+
@removed_columns = old_state.columns.reject {|col| new_state.has_column? col.name}
|
539
|
+
end
|
540
|
+
|
541
|
+
attr_reader :constraints_to_drop, :new_constraint_sql_clauses, :new_columns, :altered_column_pairs, :removed_columns
|
542
|
+
end
|
543
|
+
|
544
|
+
def column_creation_sql_fragment(column)
|
545
|
+
"#{column.name} #{column.type}".tap do |result|
|
546
|
+
if dc = column.default_constraint
|
547
|
+
result << " CONSTRAINT #{dc.name} DEFAULT #{dc.expression}"
|
548
|
+
end
|
549
|
+
result << " NOT NULL" unless column.nullable?
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
def column_alteration_occurs?(a, b)
|
554
|
+
[[a, b], [b, a]].map {|pair| alter_table_columns_sql_statements([pair])}.inject(&:!=)
|
555
|
+
end
|
556
|
+
|
557
|
+
def remove_table_constraints_sql_statements(constraint_names)
|
558
|
+
constraint_names.map do |constr|
|
559
|
+
"ALTER TABLE #{name} DROP CONSTRAINT #{constr};"
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def add_table_columns_sql_statements(column_name_type_pairs)
|
564
|
+
column_name_type_pairs.map do |col_name, col_type|
|
565
|
+
"ALTER TABLE #{name} ADD COLUMN #{col_name} #{col_type};"
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
def alter_table_columns_sql_statements(column_name_type_pairs)
|
570
|
+
raise(NotImplementedError, "SQL 92 does not provide a standard way to alter a column's type") unless column_name_type_pairs.empty?
|
571
|
+
end
|
572
|
+
|
573
|
+
def remove_table_columns_sql_statements(column_names)
|
574
|
+
column_names.map do |col_name|
|
575
|
+
"ALTER TABLE #{name} DROP COLUMN #{col_name};"
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def add_table_constraints_sql_statements(constraint_def_clauses)
|
580
|
+
constraint_def_clauses.map do |create_clause|
|
581
|
+
"ALTER TABLE #{name} ADD #{create_clause};"
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def destruction_sql
|
586
|
+
"DROP TABLE #{name};"
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|