ridgepole 1.0.1 → 3.1.5
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/CHANGELOG.md +234 -99
- data/README.md +131 -26
- data/bin/ridgepole +16 -7
- data/lib/ridgepole/cli/config.rb +8 -15
- data/lib/ridgepole/client.rb +3 -2
- data/lib/ridgepole/default_limit.rb +13 -9
- data/lib/ridgepole/delta.rb +211 -17
- data/lib/ridgepole/diff.rb +207 -19
- data/lib/ridgepole/dsl_parser/context.rb +56 -3
- data/lib/ridgepole/dsl_parser/table_definition.rb +49 -21
- data/lib/ridgepole/dsl_parser.rb +24 -8
- data/lib/ridgepole/dumper.rb +7 -3
- data/lib/ridgepole/execute_expander.rb +34 -8
- data/lib/ridgepole/ext/abstract_adapter/disable_table_options.rb +0 -2
- data/lib/ridgepole/ext/schema_dumper/disable_sort_columns.rb +28 -0
- data/lib/ridgepole/ext/schema_dumper/foreign_keys.rb +68 -0
- data/lib/ridgepole/external_sql_executer.rb +1 -12
- data/lib/ridgepole/logger.rb +1 -0
- data/lib/ridgepole/version.rb +1 -1
- data/lib/ridgepole.rb +1 -1
- metadata +102 -33
- data/.rspec +0 -3
- data/.rubocop.yml +0 -53
- data/.simplecov +0 -6
- data/Appraisals +0 -22
- data/Gemfile +0 -6
- data/Rakefile +0 -13
- data/docker-compose.yml +0 -26
- data/gemfiles/activerecord_5.1.gemfile +0 -7
- data/gemfiles/activerecord_5.2.gemfile +0 -8
- data/gemfiles/activerecord_6.0.gemfile +0 -7
- data/gemfiles/activerecord_6.1.gemfile +0 -7
- data/gemfiles/activerecord_7.0.gemfile +0 -7
- data/lib/ridgepole/ext/schema_dumper.rb +0 -56
- data/ridgepole.gemspec +0 -47
data/lib/ridgepole/diff.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Ridgepole
|
|
4
4
|
class Diff
|
|
5
5
|
PRIMARY_KEY_OPTIONS = %i[id limit default null precision scale collation unsigned].freeze
|
|
6
|
+
REGEX_COLUMN_IDENTIFIER_QUOTATION_CHARS = '"`'
|
|
6
7
|
|
|
7
8
|
def initialize(options = {})
|
|
8
9
|
@options = options
|
|
@@ -28,12 +29,15 @@ module Ridgepole
|
|
|
28
29
|
if (from_attrs = from.delete(table_name))
|
|
29
30
|
@logger.verbose_info("# #{table_name}")
|
|
30
31
|
|
|
31
|
-
unless
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
unless @options[:drop_table_only]
|
|
33
|
+
unless hash_deep_equal?(from_attrs, to_attrs)
|
|
34
|
+
attrs_delta = diff_inspect(from_attrs, to_attrs)
|
|
35
|
+
@logger.verbose_info(attrs_delta) unless attrs_delta.empty?
|
|
36
|
+
end
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
scan_change(table_name, from_attrs, to_attrs, delta)
|
|
39
|
+
end
|
|
40
|
+
elsif !@options[:drop_table_only]
|
|
37
41
|
delta[:add] ||= {}
|
|
38
42
|
delta[:add][table_name] = to_attrs
|
|
39
43
|
end
|
|
@@ -41,7 +45,7 @@ module Ridgepole
|
|
|
41
45
|
|
|
42
46
|
scan_relation_info(relation_info)
|
|
43
47
|
|
|
44
|
-
if !@options[:merge] && @options[:force_drop_table]
|
|
48
|
+
if !@options[:merge] && (@options[:force_drop_table] || @options[:drop_table_only])
|
|
45
49
|
from.each do |table_name, from_attrs|
|
|
46
50
|
next unless target?(table_name)
|
|
47
51
|
|
|
@@ -99,8 +103,12 @@ module Ridgepole
|
|
|
99
103
|
|
|
100
104
|
scan_options_change(table_name, from[:options], to[:options], table_delta)
|
|
101
105
|
scan_definition_change(from[:definition], to[:definition], from[:indices], table_name, from[:options], table_delta)
|
|
106
|
+
apply_column_renames_to_indices(from[:indices], table_delta.dig(:definition, :rename))
|
|
102
107
|
scan_indices_change(from[:indices], to[:indices], to[:definition], table_delta, from[:options], to[:options])
|
|
103
108
|
scan_foreign_keys_change(from[:foreign_keys], to[:foreign_keys], table_delta, @options)
|
|
109
|
+
scan_check_constraints_change(from[:check_constraints], to[:check_constraints], table_delta)
|
|
110
|
+
scan_exclusion_constraints_change(from[:exclusion_constraints], to[:exclusion_constraints], table_delta)
|
|
111
|
+
scan_unique_constraints_change(from[:unique_constraints], to[:unique_constraints], table_delta)
|
|
104
112
|
|
|
105
113
|
unless table_delta.empty?
|
|
106
114
|
delta[:change] ||= {}
|
|
@@ -150,6 +158,14 @@ module Ridgepole
|
|
|
150
158
|
end
|
|
151
159
|
end
|
|
152
160
|
|
|
161
|
+
if Ridgepole::ConnectionAdapters.postgresql?
|
|
162
|
+
if @options[:postgresql_change_table_comment] && (from[:comment] != to[:comment])
|
|
163
|
+
from.delete(:comment)
|
|
164
|
+
to_comment = to.delete(:comment)
|
|
165
|
+
table_delta[:table_comment] = to_comment
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
153
169
|
if @options[:dump_without_table_options]
|
|
154
170
|
from.delete(:options)
|
|
155
171
|
from.delete(:charset)
|
|
@@ -182,8 +198,8 @@ module Ridgepole
|
|
|
182
198
|
to = to.except(*PRIMARY_KEY_OPTIONS)
|
|
183
199
|
|
|
184
200
|
unless from == to
|
|
185
|
-
@logger.
|
|
186
|
-
|
|
201
|
+
@logger.verbose_info(<<-MSG)
|
|
202
|
+
# Table option changes are ignored on `#{table_name}`.
|
|
187
203
|
from: #{from}
|
|
188
204
|
to: #{to}
|
|
189
205
|
MSG
|
|
@@ -227,6 +243,7 @@ module Ridgepole
|
|
|
227
243
|
from = (from || {}).dup
|
|
228
244
|
to = (to || {}).dup
|
|
229
245
|
definition_delta = {}
|
|
246
|
+
column_comments = {}
|
|
230
247
|
|
|
231
248
|
scan_column_rename(from, to, definition_delta)
|
|
232
249
|
|
|
@@ -242,10 +259,14 @@ module Ridgepole
|
|
|
242
259
|
next if ignore_column
|
|
243
260
|
|
|
244
261
|
if from_attrs
|
|
245
|
-
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
changed_attrs = build_attrs_if_changed(to_attrs, from_attrs)
|
|
263
|
+
if changed_attrs
|
|
264
|
+
if comment_only_change?(from_attrs, to_attrs)
|
|
265
|
+
column_comments[column_name] = to_attrs[:options][:comment]
|
|
266
|
+
else
|
|
267
|
+
definition_delta[:change] ||= {}
|
|
268
|
+
definition_delta[:change][column_name] = changed_attrs
|
|
269
|
+
end
|
|
249
270
|
end
|
|
250
271
|
else
|
|
251
272
|
definition_delta[:add] ||= {}
|
|
@@ -300,6 +321,7 @@ module Ridgepole
|
|
|
300
321
|
end
|
|
301
322
|
|
|
302
323
|
table_delta[:definition] = definition_delta unless definition_delta.empty?
|
|
324
|
+
table_delta[:column_comments] = column_comments unless column_comments.empty?
|
|
303
325
|
end
|
|
304
326
|
|
|
305
327
|
def scan_column_rename(from, to, definition_delta)
|
|
@@ -321,6 +343,19 @@ module Ridgepole
|
|
|
321
343
|
end
|
|
322
344
|
end
|
|
323
345
|
|
|
346
|
+
def apply_column_renames_to_indices(indices, renames)
|
|
347
|
+
return unless indices && renames
|
|
348
|
+
|
|
349
|
+
# renames: { new_column_name => old_column_name }
|
|
350
|
+
rename_map = renames.invert # { old_column_name => new_column_name }
|
|
351
|
+
|
|
352
|
+
indices.each_value do |attrs|
|
|
353
|
+
next unless attrs[:column_name].is_a?(Array)
|
|
354
|
+
|
|
355
|
+
attrs[:column_name] = attrs[:column_name].map { |col| rename_map[col] || col }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
324
359
|
def scan_indices_change(from, to, to_columns, table_delta, _from_table_options, to_table_options)
|
|
325
360
|
from = (from || {}).dup
|
|
326
361
|
to = (to || {}).dup
|
|
@@ -389,15 +424,17 @@ module Ridgepole
|
|
|
389
424
|
|
|
390
425
|
def normalize_column_options!(attrs, primary_key = false)
|
|
391
426
|
opts = attrs[:options]
|
|
392
|
-
opts[:null] = true if !opts.key?(:null) && !primary_key
|
|
427
|
+
opts[:null] = true if !opts.key?(:null) && !primary_key && attrs[:type] != :virtual
|
|
393
428
|
default_limit = Ridgepole::DefaultsLimit.default_limit(attrs[:type], @options)
|
|
394
429
|
opts.delete(:limit) if opts[:limit] == default_limit
|
|
395
430
|
|
|
396
431
|
# XXX: MySQL only?
|
|
397
|
-
|
|
432
|
+
# Generated/virtual columns cannot have DEFAULT values in MySQL.
|
|
433
|
+
# cf. https://github.com/ridgepole/ridgepole/issues/482
|
|
434
|
+
opts[:default] = nil if !opts.key?(:default) && !primary_key && attrs[:type] != :virtual
|
|
398
435
|
|
|
399
436
|
if Ridgepole::ConnectionAdapters.mysql?
|
|
400
|
-
opts[:unsigned] = false
|
|
437
|
+
opts[:unsigned] = false if !opts.key?(:unsigned) && attrs[:type] != :virtual
|
|
401
438
|
|
|
402
439
|
if attrs[:type] == :integer && opts[:limit]
|
|
403
440
|
min = Ridgepole::DefaultsLimit.default_limit(:integer, @options)
|
|
@@ -408,7 +445,7 @@ module Ridgepole
|
|
|
408
445
|
end
|
|
409
446
|
end
|
|
410
447
|
|
|
411
|
-
if opts[:size] &&
|
|
448
|
+
if opts[:size] && %i[text blob binary].include?(attrs[:type])
|
|
412
449
|
case opts.delete(:size)
|
|
413
450
|
when :tiny
|
|
414
451
|
attrs[:type] = :blob if attrs[:type] == :binary
|
|
@@ -419,9 +456,6 @@ module Ridgepole
|
|
|
419
456
|
opts[:limit] = 4_294_967_295
|
|
420
457
|
end
|
|
421
458
|
end
|
|
422
|
-
|
|
423
|
-
# Workaround for Active Record 7.0
|
|
424
|
-
opts.delete(:precision) if attrs[:type] == :datetime && opts[:precision].nil?
|
|
425
459
|
end
|
|
426
460
|
end
|
|
427
461
|
|
|
@@ -429,6 +463,9 @@ module Ridgepole
|
|
|
429
463
|
# XXX: MySQL only?
|
|
430
464
|
opts[:using] = :btree unless opts.key?(:using)
|
|
431
465
|
opts[:unique] = false unless opts.key?(:unique)
|
|
466
|
+
|
|
467
|
+
# 'algorithm: concurrently' is required for index creation but not stored in PostgreSQL DB metadata, so remove 'algorithm' to prevent diff
|
|
468
|
+
opts.delete(:algorithm)
|
|
432
469
|
end
|
|
433
470
|
|
|
434
471
|
def columns_all_include?(expected_columns, actual_columns, table_options)
|
|
@@ -444,6 +481,23 @@ module Ridgepole
|
|
|
444
481
|
to = (to || {}).dup
|
|
445
482
|
foreign_keys_delta = {}
|
|
446
483
|
|
|
484
|
+
# Converts conventionally named (i.e., the combination of a singular table name with `_id` suffix)
|
|
485
|
+
# foreign key column names to void.
|
|
486
|
+
#
|
|
487
|
+
# Example:
|
|
488
|
+
# in `foreign_key_name_or_tables`, it converts the foreign key column name to nil
|
|
489
|
+
# - ["my_table", "referenced_table", "referenced_table_id"] => ["my_table", "referenced_table", nil]
|
|
490
|
+
# in `to_attrs`, it drops `column` option value
|
|
491
|
+
# - {to_table: "referenced_table", options: {column: "referenced_table_id"}} => {to_table: "referenced_table", options: {}}
|
|
492
|
+
to = to.to_h do |foreign_key_name_or_tables, to_attrs|
|
|
493
|
+
foreign_key_column_name = "#{foreign_key_name_or_tables[1].singularize}_id"
|
|
494
|
+
|
|
495
|
+
foreign_key_name_or_tables[2] = nil if foreign_key_column_name == foreign_key_name_or_tables[2]
|
|
496
|
+
to_attrs[:options].delete(:column) if to_attrs[:options][:column] == foreign_key_column_name
|
|
497
|
+
|
|
498
|
+
[foreign_key_name_or_tables, to_attrs]
|
|
499
|
+
end
|
|
500
|
+
|
|
447
501
|
to.each do |foreign_key_name_or_tables, to_attrs|
|
|
448
502
|
ignore_fk = to_attrs.fetch(:options, {}).delete(:ignore)
|
|
449
503
|
from_attrs = from.delete(foreign_key_name_or_tables)
|
|
@@ -475,6 +529,108 @@ module Ridgepole
|
|
|
475
529
|
table_delta[:foreign_keys] = foreign_keys_delta unless foreign_keys_delta.empty?
|
|
476
530
|
end
|
|
477
531
|
|
|
532
|
+
def scan_check_constraints_change(from, to, table_delta)
|
|
533
|
+
from = (from || {}).dup
|
|
534
|
+
to = (to || {}).dup
|
|
535
|
+
check_constraints_delta = {}
|
|
536
|
+
|
|
537
|
+
to.each do |name, to_attrs|
|
|
538
|
+
from_attrs = from.delete(name)
|
|
539
|
+
|
|
540
|
+
if from_attrs
|
|
541
|
+
if normalize_check_constraint(from_attrs) != normalize_check_constraint(to_attrs)
|
|
542
|
+
check_constraints_delta[:add] ||= {}
|
|
543
|
+
check_constraints_delta[:add][name] = to_attrs
|
|
544
|
+
|
|
545
|
+
check_constraints_delta[:delete] ||= {}
|
|
546
|
+
check_constraints_delta[:delete][name] = from_attrs
|
|
547
|
+
end
|
|
548
|
+
else
|
|
549
|
+
check_constraints_delta[:add] ||= {}
|
|
550
|
+
check_constraints_delta[:add][name] = to_attrs
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
unless @options[:merge]
|
|
555
|
+
from.each do |name, from_attrs|
|
|
556
|
+
check_constraints_delta[:delete] ||= {}
|
|
557
|
+
check_constraints_delta[:delete][name] = from_attrs
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
table_delta[:check_constraints] = check_constraints_delta unless check_constraints_delta.empty?
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def normalize_check_constraint(attr)
|
|
565
|
+
attr = attr.dup
|
|
566
|
+
attr[:expression] = attr[:expression].delete(REGEX_COLUMN_IDENTIFIER_QUOTATION_CHARS)
|
|
567
|
+
attr
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def scan_exclusion_constraints_change(from, to, table_delta)
|
|
571
|
+
from = (from || {}).dup
|
|
572
|
+
to = (to || {}).dup
|
|
573
|
+
exclusion_constraints_delta = {}
|
|
574
|
+
|
|
575
|
+
to.each do |name, to_attrs|
|
|
576
|
+
from_attrs = from.delete(name)
|
|
577
|
+
|
|
578
|
+
if from_attrs
|
|
579
|
+
if from_attrs != to_attrs
|
|
580
|
+
exclusion_constraints_delta[:add] ||= {}
|
|
581
|
+
exclusion_constraints_delta[:add][name] = to_attrs
|
|
582
|
+
|
|
583
|
+
exclusion_constraints_delta[:delete] ||= {}
|
|
584
|
+
exclusion_constraints_delta[:delete][name] = from_attrs
|
|
585
|
+
end
|
|
586
|
+
else
|
|
587
|
+
exclusion_constraints_delta[:add] ||= {}
|
|
588
|
+
exclusion_constraints_delta[:add][name] = to_attrs
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
unless @options[:merge]
|
|
593
|
+
from.each do |name, from_attrs|
|
|
594
|
+
exclusion_constraints_delta[:delete] ||= {}
|
|
595
|
+
exclusion_constraints_delta[:delete][name] = from_attrs
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
table_delta[:exclusion_constraints] = exclusion_constraints_delta unless exclusion_constraints_delta.empty?
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def scan_unique_constraints_change(from, to, table_delta)
|
|
603
|
+
from = (from || {}).dup
|
|
604
|
+
to = (to || {}).dup
|
|
605
|
+
unique_constraints_delta = {}
|
|
606
|
+
|
|
607
|
+
to.each do |name, to_attrs|
|
|
608
|
+
from_attrs = from.delete(name)
|
|
609
|
+
|
|
610
|
+
if from_attrs
|
|
611
|
+
if from_attrs != to_attrs
|
|
612
|
+
unique_constraints_delta[:add] ||= {}
|
|
613
|
+
unique_constraints_delta[:add][name] = to_attrs
|
|
614
|
+
|
|
615
|
+
unique_constraints_delta[:delete] ||= {}
|
|
616
|
+
unique_constraints_delta[:delete][name] = from_attrs
|
|
617
|
+
end
|
|
618
|
+
else
|
|
619
|
+
unique_constraints_delta[:add] ||= {}
|
|
620
|
+
unique_constraints_delta[:add][name] = to_attrs
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
unless @options[:merge]
|
|
625
|
+
from.each do |name, from_attrs|
|
|
626
|
+
unique_constraints_delta[:delete] ||= {}
|
|
627
|
+
unique_constraints_delta[:delete][name] = from_attrs
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
table_delta[:unique_constraints] = unique_constraints_delta unless unique_constraints_delta.empty?
|
|
632
|
+
end
|
|
633
|
+
|
|
478
634
|
# XXX: MySQL only?
|
|
479
635
|
# https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L760
|
|
480
636
|
# https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb#L102
|
|
@@ -506,9 +662,30 @@ module Ridgepole
|
|
|
506
662
|
attrs2.fetch(:options).delete(:comment)
|
|
507
663
|
end
|
|
508
664
|
|
|
665
|
+
if attrs1[:options][:as] != attrs2[:options][:as] && attrs1[:options].fetch(:as, '').delete(' ') == attrs2[:options].fetch(:as, '').delete(' ')
|
|
666
|
+
@logger.warn(<<-MSG)
|
|
667
|
+
[WARNING] Same expressions but only differed by white spaces were detected. This operation may fail.
|
|
668
|
+
Before: '#{attrs1[:options][:as]}'
|
|
669
|
+
After : '#{attrs2[:options][:as]}'
|
|
670
|
+
MSG
|
|
671
|
+
end
|
|
672
|
+
|
|
509
673
|
attrs1 == attrs2
|
|
510
674
|
end
|
|
511
675
|
|
|
676
|
+
def comment_only_change?(attrs1, attrs2)
|
|
677
|
+
return false if @options[:skip_column_comment_change]
|
|
678
|
+
|
|
679
|
+
attrs1 = attrs1.merge(options: attrs1.fetch(:options, {}).dup)
|
|
680
|
+
attrs2 = attrs2.merge(options: attrs2.fetch(:options, {}).dup)
|
|
681
|
+
normalize_default_proc_options!(attrs1[:options], attrs2[:options])
|
|
682
|
+
|
|
683
|
+
comment1 = attrs1.fetch(:options).delete(:comment)
|
|
684
|
+
comment2 = attrs2.fetch(:options).delete(:comment)
|
|
685
|
+
|
|
686
|
+
attrs1 == attrs2 && comment1 != comment2
|
|
687
|
+
end
|
|
688
|
+
|
|
512
689
|
def normalize_default_proc_options!(opts1, opts2)
|
|
513
690
|
if opts1[:default].is_a?(Proc) && opts2[:default].is_a?(Proc)
|
|
514
691
|
opts1[:default] = opts1[:default].call
|
|
@@ -619,5 +796,16 @@ module Ridgepole
|
|
|
619
796
|
@logger.warn "[WARNING] '#{table_name}' definition is not found" unless definition.key?(table_name)
|
|
620
797
|
end
|
|
621
798
|
end
|
|
799
|
+
|
|
800
|
+
def hash_deep_equal?(hash1, hash2)
|
|
801
|
+
return false unless hash1.is_a?(Hash) && hash2.is_a?(Hash)
|
|
802
|
+
return false if hash1.keys.to_set != hash2.keys.to_set
|
|
803
|
+
|
|
804
|
+
hash1.each do |k, v|
|
|
805
|
+
return false unless hash2.key?(k) && v == hash2[k]
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
true
|
|
809
|
+
end
|
|
622
810
|
end
|
|
623
811
|
end
|
|
@@ -48,7 +48,7 @@ module Ridgepole
|
|
|
48
48
|
table_name = table_name.to_s
|
|
49
49
|
# Keep column_name for expression index support
|
|
50
50
|
# https://github.com/rails/rails/pull/23393
|
|
51
|
-
column_name = [column_name].flatten.map(&:to_s) unless column_name.is_a?(String) && /\W/ === column_name
|
|
51
|
+
column_name = [column_name].flatten.map(&:to_s) unless column_name.is_a?(String) && /\W/ === column_name
|
|
52
52
|
options[:name] = options[:name].to_s if options[:name]
|
|
53
53
|
@__definition[table_name] ||= {}
|
|
54
54
|
@__definition[table_name][:indices] ||= {}
|
|
@@ -77,8 +77,16 @@ module Ridgepole
|
|
|
77
77
|
from_table = from_table.to_s
|
|
78
78
|
to_table = to_table.to_s
|
|
79
79
|
options[:name] = options[:name].to_s if options[:name]
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if options[:primary_key].is_a?(Array)
|
|
81
|
+
options[:primary_key] = options[:primary_key].map(&:to_s)
|
|
82
|
+
elsif options[:primary_key]
|
|
83
|
+
options[:primary_key] = options[:primary_key].to_s
|
|
84
|
+
end
|
|
85
|
+
if options[:column].is_a?(Array)
|
|
86
|
+
options[:column] = options[:column].map(&:to_s)
|
|
87
|
+
elsif options[:column]
|
|
88
|
+
options[:column] = options[:column].to_s
|
|
89
|
+
end
|
|
82
90
|
@__definition[from_table] ||= {}
|
|
83
91
|
@__definition[from_table][:foreign_keys] ||= {}
|
|
84
92
|
idx = options[:name] || [from_table, to_table, options[:column]]
|
|
@@ -91,6 +99,51 @@ module Ridgepole
|
|
|
91
99
|
}
|
|
92
100
|
end
|
|
93
101
|
|
|
102
|
+
def add_check_constraint(table_name, expression, options = {})
|
|
103
|
+
table_name = table_name.to_s
|
|
104
|
+
expression = expression.to_s
|
|
105
|
+
options[:name] = options[:name].to_s if options[:name]
|
|
106
|
+
|
|
107
|
+
idx = options[:name] || expression
|
|
108
|
+
|
|
109
|
+
@__definition[table_name] ||= {}
|
|
110
|
+
@__definition[table_name][:check_constraints] ||= {}
|
|
111
|
+
@__definition[table_name][:check_constraints][idx] = {
|
|
112
|
+
expression: expression,
|
|
113
|
+
options: options,
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add_exclusion_constraint(table_name, expression, options = {})
|
|
118
|
+
table_name = table_name.to_s
|
|
119
|
+
expression = expression.to_s
|
|
120
|
+
options[:name] = options[:name].to_s if options[:name]
|
|
121
|
+
|
|
122
|
+
idx = options[:name] || expression
|
|
123
|
+
|
|
124
|
+
@__definition[table_name] ||= {}
|
|
125
|
+
@__definition[table_name][:exclusion_constraints] ||= {}
|
|
126
|
+
@__definition[table_name][:exclusion_constraints][idx] = {
|
|
127
|
+
expression: expression,
|
|
128
|
+
options: options,
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def add_unique_constraint(table_name, column_name, options = {})
|
|
133
|
+
table_name = table_name.to_s
|
|
134
|
+
column_name = Array(column_name).map(&:to_sym)
|
|
135
|
+
options[:name] = options[:name].to_s if options[:name]
|
|
136
|
+
|
|
137
|
+
idx = options[:name] || column_name
|
|
138
|
+
|
|
139
|
+
@__definition[table_name] ||= {}
|
|
140
|
+
@__definition[table_name][:unique_constraints] ||= {}
|
|
141
|
+
@__definition[table_name][:unique_constraints][idx] = {
|
|
142
|
+
column_name: column_name,
|
|
143
|
+
options: options,
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
94
147
|
def require(file)
|
|
95
148
|
schemafile = %r{\A/}.match?(file) ? file : File.join(@__working_dir, file)
|
|
96
149
|
|
|
@@ -26,32 +26,41 @@ module Ridgepole
|
|
|
26
26
|
DEFAULT_PRIMARY_KEY_TYPE = :bigint
|
|
27
27
|
|
|
28
28
|
TYPES = {
|
|
29
|
-
# https://github.com/rails/rails/blob/
|
|
30
|
-
string: {},
|
|
31
|
-
text: {},
|
|
32
|
-
integer: {},
|
|
29
|
+
# https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L300-L301
|
|
33
30
|
bigint: {},
|
|
34
|
-
float: {},
|
|
35
|
-
decimal: {},
|
|
36
|
-
datetime: {},
|
|
37
|
-
timestamp: {},
|
|
38
|
-
time: {},
|
|
39
|
-
date: {},
|
|
40
31
|
binary: {},
|
|
41
32
|
boolean: {},
|
|
33
|
+
date: {},
|
|
34
|
+
datetime: {},
|
|
35
|
+
decimal: {},
|
|
36
|
+
float: {},
|
|
37
|
+
integer: {},
|
|
38
|
+
json: {},
|
|
39
|
+
string: {},
|
|
40
|
+
text: {},
|
|
41
|
+
time: {},
|
|
42
|
+
timestamp: {},
|
|
43
|
+
virtual: {},
|
|
42
44
|
|
|
43
|
-
# https://github.com/rails/rails/blob/
|
|
45
|
+
# https://github.com/rails/rails/blob/v6.0.6/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L101
|
|
44
46
|
serial: { null: false },
|
|
45
47
|
bigserial: { null: false },
|
|
48
|
+
# string: {},
|
|
49
|
+
# text: {},
|
|
50
|
+
# integer: {},
|
|
51
|
+
# float: {},
|
|
52
|
+
# decimal: {},
|
|
53
|
+
# datetime: {},
|
|
54
|
+
# time: {},
|
|
55
|
+
# date: {},
|
|
46
56
|
daterange: {},
|
|
47
57
|
numrange: {},
|
|
48
58
|
tsrange: {},
|
|
49
59
|
tstzrange: {},
|
|
50
60
|
int4range: {},
|
|
51
61
|
int8range: {},
|
|
52
|
-
# binary: {},
|
|
53
|
-
# boolean: {},
|
|
54
|
-
# bigint: {}, # dup key
|
|
62
|
+
# binary: {},
|
|
63
|
+
# boolean: {},
|
|
55
64
|
xml: {},
|
|
56
65
|
tsvector: {},
|
|
57
66
|
hstore: {},
|
|
@@ -59,20 +68,27 @@ module Ridgepole
|
|
|
59
68
|
cidr: {},
|
|
60
69
|
macaddr: {},
|
|
61
70
|
uuid: {},
|
|
62
|
-
json: {},
|
|
71
|
+
# json: {},
|
|
63
72
|
jsonb: {},
|
|
64
73
|
ltree: {},
|
|
65
74
|
citext: {},
|
|
66
75
|
point: {},
|
|
76
|
+
line: {},
|
|
77
|
+
lseg: {},
|
|
78
|
+
box: {},
|
|
79
|
+
path: {},
|
|
80
|
+
polygon: {},
|
|
81
|
+
circle: {},
|
|
67
82
|
bit: {},
|
|
68
83
|
bit_varying: {},
|
|
69
84
|
money: {},
|
|
85
|
+
interval: {},
|
|
86
|
+
oid: {},
|
|
70
87
|
|
|
71
|
-
# https://github.com/
|
|
72
|
-
|
|
88
|
+
# https://github.com/ridgepole/ridgepole/issues/394
|
|
89
|
+
timestamptz: {},
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
# json: {}, # dup key
|
|
91
|
+
enum: {},
|
|
76
92
|
}.freeze
|
|
77
93
|
|
|
78
94
|
TYPES.each do |column_type, default_options|
|
|
@@ -129,8 +145,8 @@ module Ridgepole
|
|
|
129
145
|
|
|
130
146
|
def timestamps(*args)
|
|
131
147
|
options = { null: false }.merge(args.extract_options!)
|
|
132
|
-
column(:created_at, :datetime, options)
|
|
133
|
-
column(:updated_at, :datetime, options)
|
|
148
|
+
column(:created_at, :datetime, options.dup)
|
|
149
|
+
column(:updated_at, :datetime, options.dup)
|
|
134
150
|
end
|
|
135
151
|
|
|
136
152
|
def references(*args)
|
|
@@ -159,6 +175,18 @@ module Ridgepole
|
|
|
159
175
|
end
|
|
160
176
|
end
|
|
161
177
|
alias belongs_to references
|
|
178
|
+
|
|
179
|
+
def check_constraint(expression, options = {})
|
|
180
|
+
@base.add_check_constraint(@table_name, expression, options)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def exclusion_constraint(expression, options = {})
|
|
184
|
+
@base.add_exclusion_constraint(@table_name, expression, options)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def unique_constraint(column_name, options = {})
|
|
188
|
+
@base.add_unique_constraint(@table_name, column_name, options)
|
|
189
|
+
end
|
|
162
190
|
end
|
|
163
191
|
end
|
|
164
192
|
end
|
data/lib/ridgepole/dsl_parser.rb
CHANGED
|
@@ -23,25 +23,41 @@ module Ridgepole
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def check_orphan_index(table_name, attrs)
|
|
26
|
-
raise "Table `#{table_name}` to create the index is not defined: #{attrs[:indices].keys.join(',')}" if attrs[:indices] && !
|
|
26
|
+
raise "Table `#{table_name}` to create the index is not defined: #{attrs[:indices].keys.join(',')}" if attrs[:indices] && !attrs[:definition]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def check_orphan_foreign_key(table_name, attrs)
|
|
30
|
-
raise "Table `#{table_name}` to create the foreign key is not defined: #{attrs[:foreign_keys].keys.join(',')}" if attrs[:foreign_keys] && !
|
|
30
|
+
raise "Table `#{table_name}` to create the foreign key is not defined: #{attrs[:foreign_keys].keys.join(',')}" if attrs[:foreign_keys] && !attrs[:definition]
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def check_foreign_key_without_index(table_name, attrs)
|
|
34
|
+
return if Ridgepole::ConnectionAdapters.postgresql?
|
|
34
35
|
return unless attrs[:foreign_keys]
|
|
35
|
-
return unless
|
|
36
|
+
return unless innodb_table?(attrs)
|
|
36
37
|
|
|
37
|
-
attrs[:foreign_keys].
|
|
38
|
+
attrs[:foreign_keys].each_value do |foreign_key_attrs|
|
|
38
39
|
fk_index = foreign_key_attrs[:options][:column] || "#{foreign_key_attrs[:to_table].singularize}_id"
|
|
39
|
-
next if attrs[:indices]&.any? { |
|
|
40
|
-
|
|
41
|
-
next if Array(attrs[:options][:primary_key]).first == fk_index
|
|
40
|
+
next if attrs[:indices]&.any? { |_, v| match_column_name?(v[:column_name], fk_index) }
|
|
41
|
+
next if match_column_name?(attrs[:options][:primary_key], fk_index)
|
|
42
42
|
|
|
43
|
-
raise
|
|
43
|
+
raise("The column `#{fk_index}` of the table `#{table_name}` has a foreign key but no index. " \
|
|
44
|
+
'Although InnoDB creates an index automatically, ' \
|
|
45
|
+
'please add one explicitly in order for ridgepole to manage it.')
|
|
44
46
|
end
|
|
45
47
|
end
|
|
48
|
+
|
|
49
|
+
def match_column_name?(index_column_name, fk_index)
|
|
50
|
+
if fk_index.is_a?(Array)
|
|
51
|
+
index_column_name == fk_index
|
|
52
|
+
else
|
|
53
|
+
# NOTE: For composite primary keys, the first column of the primary key is used as the foreign key index
|
|
54
|
+
Array(index_column_name).first == fk_index
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def innodb_table?(attrs)
|
|
59
|
+
engine = attrs[:options][:options]&.match(/ENGINE=([^ ]+)/) && Regexp.last_match(1)
|
|
60
|
+
engine.nil? || engine == 'InnoDB'
|
|
61
|
+
end
|
|
46
62
|
end
|
|
47
63
|
end
|
data/lib/ridgepole/dumper.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Ridgepole
|
|
|
39
39
|
|
|
40
40
|
dsl = stream.string.lines.select do |line|
|
|
41
41
|
line !~ /\A#/ &&
|
|
42
|
-
line !~ /\AActiveRecord::Schema
|
|
42
|
+
line !~ /\AActiveRecord::Schema(\[\d\.\d\])?\.define/ &&
|
|
43
43
|
line !~ /\Aend/
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -85,14 +85,18 @@ module Ridgepole
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def target?(table_name)
|
|
88
|
-
|
|
88
|
+
!@options[:tables] || @options[:tables].include?(table_name)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def dump_from(conn)
|
|
92
92
|
stream = StringIO.new
|
|
93
93
|
conn.without_table_options(@options[:dump_without_table_options]) do
|
|
94
94
|
ActiveRecord::SchemaDumper.with_default_fk_name(@options[:dump_with_default_fk_name]) do
|
|
95
|
-
ActiveRecord::
|
|
95
|
+
if ActiveRecord.gem_version < Gem::Version.new('7.2.0')
|
|
96
|
+
ActiveRecord::SchemaDumper.dump(conn, stream)
|
|
97
|
+
else
|
|
98
|
+
ActiveRecord::SchemaDumper.dump(conn.pool, stream)
|
|
99
|
+
end
|
|
96
100
|
end
|
|
97
101
|
end
|
|
98
102
|
stream
|