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.
@@ -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