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.
@@ -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 (attrs_delta = diff_inspect(from_attrs, to_attrs)).empty?
32
- @logger.verbose_info(attrs_delta)
33
- end
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
- scan_change(table_name, from_attrs, to_attrs, delta)
36
- else
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.warn(<<-MSG)
186
- [WARNING] Table option changes are ignored on `#{table_name}`.
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
- to_attrs = build_attrs_if_changed(to_attrs, from_attrs)
246
- if to_attrs
247
- definition_delta[:change] ||= {}
248
- definition_delta[:change][column_name] = to_attrs
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
- opts[:default] = nil if !opts.key?(:default) && !primary_key
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 unless opts.key?(:unsigned)
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] && (attrs[:type] == :text || attrs[:type] == :blob || attrs[:type] == :binary)
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 # rubocop:disable Style/CaseEquality
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
- options[:primary_key] = options[:primary_key].to_s if options[:primary_key]
81
- options[:column] = options[:column].to_s if options[:column]
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/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L274
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/v4.2.1/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L79
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: {}, # dup key
53
- # boolean: {}, # dup key
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/rails/rails/blob/v5.1.1/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L184
72
- virtual: {},
88
+ # https://github.com/ridgepole/ridgepole/issues/394
89
+ timestamptz: {},
73
90
 
74
- # https://github.com/rails/rails/blob/v5.0.4/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L53
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
@@ -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] && !(attrs[:definition])
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] && !(attrs[:definition])
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 attrs[:options][:options]&.include?('ENGINE=InnoDB')
36
+ return unless innodb_table?(attrs)
36
37
 
37
- attrs[:foreign_keys].each do |_, foreign_key_attrs|
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? { |_k, v| v[:column_name].first == fk_index }
40
- # NOTE: For composite primary keys, the first column of the primary key is used as the foreign key index
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 "The column `#{fk_index}` of the table `#{table_name}` has a foreign key but no index. Although InnoDB creates an index automatically, please add one explicitly in order for ridgepole to manage it."
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
@@ -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\.define/ &&
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
- !(@options[:tables]) || @options[:tables].include?(table_name)
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::SchemaDumper.dump(conn, stream)
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