dynamic_migrations 3.8.6 → 3.8.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/lib/dynamic_migrations/postgres/generator/enum.rb +13 -9
  4. data/lib/dynamic_migrations/postgres/generator/fragment.rb +1 -1
  5. data/lib/dynamic_migrations/postgres/generator/function.rb +13 -13
  6. data/lib/dynamic_migrations/postgres/generator/migration.rb +45 -7
  7. data/lib/dynamic_migrations/postgres/generator/table_migration.rb +2 -0
  8. data/lib/dynamic_migrations/postgres/generator.rb +100 -46
  9. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/extensions.rb +2 -2
  10. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/enums.rb +10 -10
  11. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/functions.rb +11 -11
  12. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/columns.rb +11 -11
  13. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/foreign_key_constraints.rb +11 -11
  14. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/indexes.rb +11 -11
  15. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/primary_key.rb +6 -6
  16. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/triggers.rb +11 -11
  17. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/unique_constraints.rb +11 -11
  18. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/validations.rb +11 -11
  19. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables.rb +8 -8
  20. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas.rb +3 -3
  21. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations.rb +4 -4
  22. data/lib/dynamic_migrations/postgres/server/database/differences.rb +17 -17
  23. data/lib/dynamic_migrations/postgres/server/database/schema/enum.rb +7 -0
  24. data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +61 -1
  25. data/lib/dynamic_migrations/version.rb +1 -1
  26. data/sig/dynamic_migrations/postgres/generator/enum.rbs +2 -0
  27. data/sig/dynamic_migrations/postgres/generator/function.rbs +1 -0
  28. data/sig/dynamic_migrations/postgres/generator/migration.rbs +1 -0
  29. data/sig/dynamic_migrations/postgres/generator/schema_migration.rbs +2 -0
  30. data/sig/dynamic_migrations/postgres/generator/table_migration.rbs +3 -0
  31. data/sig/dynamic_migrations/postgres/generator.rbs +3 -1
  32. data/sig/dynamic_migrations/postgres/server/database/schema/enum.rbs +3 -0
  33. data/sig/dynamic_migrations/postgres/server/database/schema/table.rbs +3 -0
  34. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00405e194209e083433ebb55aa305922a16e24290ead4f240420602c9465bc90
4
- data.tar.gz: 57b7136d7eb1f7efc5382101cf24a4dc5ed4852e66a5f67faf368294e05632c0
3
+ metadata.gz: 443be07abb74aef74bd5d94de87ab2be7f227f81e1c401cb7bf8ebc55bc1c6af
4
+ data.tar.gz: f93c398273cb1ae28cf6d5b257adeda625f3c732952f327692fb3ef076798f7f
5
5
  SHA512:
6
- metadata.gz: bc594cb118caa67c0e9982f839c762e074b789b083328a1b69b3261297bd34055a53739f51cabb6f6e5685ea76f80eb4574b45649fd202bf213a579713842fac
7
- data.tar.gz: 8d42ee7274c26e25086eae4e9c2646f958f930a18fd03eb9c6262f734cf55af380499763e3508a05156a6f1baad4c7bc30007e9d242674d9594bf83e75a7bf1f
6
+ metadata.gz: c35b3c1d1834fea80cfdc2c9b14f611b5d76f005b408e0cbfd7592216780454647c519fbc5113bcd5a946a578320417cde47eeaa785fc097dc5062b79a310522
7
+ data.tar.gz: '09780db07d086da4fc27c5187770248b174d91e7c89a6f3021f1a87e42f97c3b4cf091e7ed510ef7e853e4da360f5b359bbc91a7375b94edd55d09c52e0f905a'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.7](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.6...v3.8.7) (2023-10-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * added detection of missing extensions and better error messages when normalizing check_constrains and action_conditions ([d82ff57](https://github.com/craigulliott/dynamic_migrations/commit/d82ff57f5b51c8636e87b40b8d6b016d11e6c72a))
9
+ * added enum value length validation ([d82ff57](https://github.com/craigulliott/dynamic_migrations/commit/d82ff57f5b51c8636e87b40b8d6b016d11e6c72a))
10
+ * also drastic performance improvement of migration circular dependency resolution ([2915b8f](https://github.com/craigulliott/dynamic_migrations/commit/2915b8f28c9202bd18f221cfafa2c4352a80fd2c))
11
+ * fixed regex for validating name within migration fragment (it was not allowing a single letter followed by an underscore) ([9b73eca](https://github.com/craigulliott/dynamic_migrations/commit/9b73ecac03816cc74f9b0d23084d0cfb09701012))
12
+ * migration generation now finds and resolves circular dependencies which exist through N migrations (it used to only resolve immediate circular dependencies) ([2915b8f](https://github.com/craigulliott/dynamic_migrations/commit/2915b8f28c9202bd18f221cfafa2c4352a80fd2c))
13
+ * providing the table dependency to all enum and function migrations if there is only one table which they rely on ([3d3708d](https://github.com/craigulliott/dynamic_migrations/commit/3d3708dfb7bdd2a83a477087ac22fd540ae1f881))
14
+ * reducing some log messages from info level to debug level ([cdf1200](https://github.com/craigulliott/dynamic_migrations/commit/cdf12000186a9e373d5085f83d2fb197c7796c6c))
15
+ * table migrations now allow enum related fragments ([b894bf1](https://github.com/craigulliott/dynamic_migrations/commit/b894bf14743c51b505693818407ba6bfb3f31571))
16
+ * updating log level for some log entries ([456a90e](https://github.com/craigulliott/dynamic_migrations/commit/456a90e7fe6da78667a84928b75217f89c344f35))
17
+
3
18
  ## [3.8.6](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.5...v3.8.6) (2023-10-08)
4
19
 
5
20
 
@@ -6,16 +6,8 @@ module DynamicMigrations
6
6
  end
7
7
 
8
8
  def create_enum enum, code_comment = nil
9
- # we only provide a table if the enum has a single column and they
10
- # are in the same schema, otherwise we can't reliable handle dependencies
11
- # so the enum will be created in the schema's migration
12
- enum_table = nil
13
- if enum.columns.count == 1 && enum.schema == enum.columns.first&.table&.schema
14
- enum_table = enum.columns.first&.table
15
- end
16
-
17
9
  add_fragment schema: enum.schema,
18
- table: enum_table,
10
+ table: optional_enum_table(enum),
19
11
  migration_method: :create_enum,
20
12
  object: enum,
21
13
  code_comment: code_comment,
@@ -35,6 +27,7 @@ module DynamicMigrations
35
27
  end
36
28
 
37
29
  add_fragment schema: updated_enum.schema,
30
+ table: optional_enum_table(updated_enum),
38
31
  migration_method: :add_enum_values,
39
32
  object: updated_enum,
40
33
  code_comment: code_comment,
@@ -47,6 +40,7 @@ module DynamicMigrations
47
40
 
48
41
  def drop_enum enum, code_comment = nil
49
42
  add_fragment schema: enum.schema,
43
+ table: optional_enum_table(enum),
50
44
  migration_method: :drop_enum,
51
45
  object: enum,
52
46
  code_comment: code_comment,
@@ -58,6 +52,7 @@ module DynamicMigrations
58
52
  # add a comment to a enum
59
53
  def set_enum_comment enum, code_comment = nil
60
54
  add_fragment schema: enum.schema,
55
+ table: optional_enum_table(enum),
61
56
  migration_method: :set_enum_comment,
62
57
  object: enum,
63
58
  code_comment: code_comment,
@@ -71,6 +66,7 @@ module DynamicMigrations
71
66
  # remove the comment from a enum
72
67
  def remove_enum_comment enum, code_comment = nil
73
68
  add_fragment schema: enum.schema,
69
+ table: optional_enum_table(enum),
74
70
  migration_method: :remove_enum_comment,
75
71
  object: enum,
76
72
  code_comment: code_comment,
@@ -78,6 +74,14 @@ module DynamicMigrations
78
74
  remove_enum_comment :#{enum.name}
79
75
  RUBY
80
76
  end
77
+
78
+ # we only provide a table to these migration fragments if the enum applies only to one table
79
+ # and that take is in the same schema as the enum
80
+ def optional_enum_table enum
81
+ # all the tables which use this enum
82
+ tables = enum.columns.map(&:table).uniq
83
+ (tables.count == 1 && tables.first&.schema == enum.schema) ? tables.first : nil
84
+ end
81
85
  end
82
86
  end
83
87
  end
@@ -15,7 +15,7 @@ module DynamicMigrations
15
15
  attr_reader :dependency_enum_name
16
16
 
17
17
  def initialize schema_name, table_name, migration_method, object_name, code_comment, content
18
- valid_name_regex = /\A[a-z][a-z0-9]+(_[a-z0-9]+)*\z/
18
+ valid_name_regex = /\A[a-z][a-z0-9]*(_[a-z0-9]+)*\z/
19
19
 
20
20
  unless schema_name.nil? || (schema_name.to_s.match valid_name_regex)
21
21
  raise InvalidNameError, "Invalid schema name `#{schema_name}`, must only be lowercase letters, numbers and underscores"
@@ -21,16 +21,8 @@ module DynamicMigrations
21
21
 
22
22
  fn_sql = function.definition.strip
23
23
 
24
- # we only provide a table if the function has a single trigger and they
25
- # are in the same schema, otherwise we can't reliable handle dependencies
26
- # so the function will be created in the schema's migration
27
- function_table = nil
28
- if function.triggers.count == 1 && function.schema == function.triggers.first&.table&.schema
29
- function_table = function.triggers.first&.table
30
- end
31
-
32
24
  add_fragment schema: function.schema,
33
- table: function_table,
25
+ table: optional_function_table(function),
34
26
  migration_method: :create_function,
35
27
  object: function,
36
28
  code_comment: code_comment,
@@ -47,7 +39,7 @@ module DynamicMigrations
47
39
  fn_sql = function.definition.strip
48
40
 
49
41
  add_fragment schema: function.schema,
50
- table: function.triggers.first&.table,
42
+ table: optional_function_table(function),
51
43
  migration_method: :update_function,
52
44
  object: function,
53
45
  code_comment: code_comment,
@@ -62,7 +54,7 @@ module DynamicMigrations
62
54
 
63
55
  def drop_function function, code_comment = nil
64
56
  add_fragment schema: function.schema,
65
- table: function.triggers.first&.table,
57
+ table: optional_function_table(function),
66
58
  migration_method: :drop_function,
67
59
  object: function,
68
60
  code_comment: code_comment,
@@ -74,7 +66,7 @@ module DynamicMigrations
74
66
  # add a comment to a function
75
67
  def set_function_comment function, code_comment = nil
76
68
  add_fragment schema: function.schema,
77
- table: function.triggers.first&.table,
69
+ table: optional_function_table(function),
78
70
  migration_method: :set_function_comment,
79
71
  object: function,
80
72
  code_comment: code_comment,
@@ -88,7 +80,7 @@ module DynamicMigrations
88
80
  # remove the comment from a function
89
81
  def remove_function_comment function, code_comment = nil
90
82
  add_fragment schema: function.schema,
91
- table: function.triggers.first&.table,
83
+ table: optional_function_table(function),
92
84
  migration_method: :remove_function_comment,
93
85
  object: function,
94
86
  code_comment: code_comment,
@@ -96,6 +88,14 @@ module DynamicMigrations
96
88
  remove_function_comment :#{function.name}
97
89
  RUBY
98
90
  end
91
+
92
+ # we only provide a table to these migration fragments if the function applies only to one table
93
+ # and that take is in the same schema as the function
94
+ def optional_function_table function
95
+ # all the tables which use this function
96
+ tables = function.triggers.map(&:table).uniq
97
+ (tables.count == 1 && tables.first&.schema == function.schema) ? tables.first : nil
98
+ end
99
99
  end
100
100
  end
101
101
  end
@@ -67,6 +67,39 @@ module DynamicMigrations
67
67
  @structure_templates = []
68
68
  end
69
69
 
70
+ def to_s
71
+ # because calling these methods will rise an error if there are no
72
+ # fragments, and this method is primarily used for debugging
73
+ if @fragments.any?
74
+ tds = table_dependencies
75
+ eds = enum_dependencies
76
+ fds = function_dependencies
77
+ else
78
+ tds = []
79
+ eds = []
80
+ fds = []
81
+ end
82
+ <<~PREVIEW.strip
83
+ # Migration content preview
84
+ # -------------------------
85
+ # Schema:#{@schema_name ? " #{@schema_name}" : ""}
86
+ # Table:#{@table_name ? " #{@table_name}" : ""}
87
+
88
+ # Table Dependencies (count: #{tds.count}):
89
+ #{(tds.any? ? "# " : "") + tds.map { |d| "Schema: `#{d[:schema_name]}` Table: `#{d[:table_name]}`" }.join("\n# ")}
90
+
91
+ # Enum Dependencies (count: #{eds.count}):
92
+ #{(eds.any? ? "# " : "") + eds.map { |d| "Schema: `#{d[:schema_name]}` Enum: `#{d[:enum_name]}`" }.join("\n# ")}
93
+
94
+ # Function Dependencies (count: #{fds.count}):
95
+ #{(fds.any? ? "# " : "") + fds.map { |d| "Schema: `#{d[:schema_name]}` Function: `#{d[:function_name]}`" }.join("\n# ")}
96
+
97
+ # Fragments (count: #{@fragments.count}):
98
+
99
+ #{@fragments.map(&:to_s).join("\n\n")}
100
+ PREVIEW
101
+ end
102
+
70
103
  # Add a migration fragment to this migration, if the migration is not
71
104
  # configured (via a structure template) to handle the method_name of the
72
105
  # fragment, then am error is raised. An error will also be raised if the
@@ -95,32 +128,37 @@ module DynamicMigrations
95
128
 
96
129
  # Return an array of table dependencies for this migration, this array comes from
97
130
  # combining any table dependencies from each fragment.
98
- # Will raise an error if no fragments have been provided.
131
+ # Will raise an error if no fragments are available.
99
132
  def table_dependencies
100
133
  raise NoFragmentsError if fragments.empty?
101
- @fragments.map(&:table_dependency).compact
134
+ @fragments.map(&:table_dependency).compact.uniq
102
135
  end
103
136
 
104
137
  # Return an array of function dependencies for this migration, this array comes from
105
138
  # combining any function dependencies from each fragment.
106
- # Will raise an error if no fragments have been provided.
139
+ # Will raise an error if no fragments are available.
107
140
  def function_dependencies
108
141
  raise NoFragmentsError if fragments.empty?
109
- @fragments.map(&:function_dependency).compact
142
+ @fragments.map(&:function_dependency).compact.uniq
110
143
  end
111
144
 
112
145
  # Return an array of enum dependencies for this migration, this array comes from
113
146
  # combining any enum dependencies from each fragment.
114
- # Will raise an error if no fragments have been provided.
147
+ # Will raise an error if no fragments are available.
115
148
  def enum_dependencies
116
149
  raise NoFragmentsError if fragments.empty?
117
- @fragments.map(&:enum_dependency).compact
150
+ @fragments.map(&:enum_dependency).compact.uniq
151
+ end
152
+
153
+ # returns the number of fragment within this migration which have the provided dependency
154
+ def fragments_with_table_dependency_count schema_name, table_name
155
+ @fragments.count { |f| f.is_dependent_on_table? schema_name, table_name }
118
156
  end
119
157
 
120
158
  # removes and returns any fragments which have a dependency on the table with the
121
159
  # provided schema_name and table_name, this is used for extracting fragments which
122
160
  # cause circular dependencies so they can be placed into their own migrations
123
- def extract_fragments_with_dependency schema_name, table_name
161
+ def extract_fragments_with_table_dependency schema_name, table_name
124
162
  results = @fragments.filter { |f| f.is_dependent_on_table? schema_name, table_name }
125
163
  # remove any of these from the internal array of fragments
126
164
  @fragments.filter! { |f| !f.is_dependent_on_table?(schema_name, table_name) }
@@ -15,6 +15,8 @@ module DynamicMigrations
15
15
  add_structure_template [:drop_table], "Remove Tables"
16
16
  add_structure_template [:create_table], "Create Table"
17
17
  add_structure_template [:remove_table_comment, :set_table_comment], "Tables"
18
+ add_structure_template [:remove_enum_comment, :drop_enum], "Drop Enums"
19
+ add_structure_template [:create_enum, :add_enum_values, :set_enum_comment], "Enums"
18
20
  add_structure_template [:add_column], "Additional Columns"
19
21
  add_structure_template [:change_column, :remove_column_comment, :set_column_comment], "Update Columns"
20
22
  add_structure_template [:add_primary_key, :set_primary_key_comment, :remove_primary_key_comment], "Primary Key"
@@ -34,10 +34,13 @@ module DynamicMigrations
34
34
 
35
35
  def initialize
36
36
  @fragments = []
37
+ @logger = Logging.logger[self]
37
38
  end
38
39
 
39
40
  # builds the final migrations
40
41
  def migrations
42
+ log.info "Generating migrations"
43
+
41
44
  # a hash to hold the generated migrations orgnized by their schema and table
42
45
  # this makes it easier and faster to work with them within this method
43
46
  database_migrations = {}
@@ -49,6 +52,7 @@ module DynamicMigrations
49
52
  # Process each fragment, and organize them into migrations. We create a shared
50
53
  # Migration for each table, and a single shared migration for any schema migrations
51
54
  # which do not relate to a table.
55
+ log.info " Organizing migration fragments"
52
56
  @fragments.each do |fragment|
53
57
  # The first time this schema is encountered we create an object to hold the migrations
54
58
  # and organize the different migrations.
@@ -87,32 +91,23 @@ module DynamicMigrations
87
91
 
88
92
  # Convert the hash of migrations into an array of migrations, this is
89
93
  # passed to the `circular_dependency?` method below, and any new migrations
90
- # requred to resolve circular dependencies will be added to this array
94
+ # required to resolve circular dependencies will be added to this array
91
95
  all_table_migrations = database_migrations.values.map { |m| m[:table_migrations].values }.flatten
92
96
 
93
- # iterate through all of the table migrations, and fix any circular dependencies caused
94
- # by foreign key constraints
95
- database_migrations.each do |schema_name, schema_migrations|
96
- # we only need to process the TableMigrations, as the SchemaMigration
97
- # never have dependencies
98
- schema_migrations[:table_migrations].values.each do |table_migration|
99
- # recursively test each table migration for circular dependencies
100
- table_migration.table_dependencies.each do |dependency|
101
- if circular_dependency? table_migration.schema_name, table_migration.table_name, dependency, all_table_migrations
102
- # remove the fragment which is causing the circular dependency
103
- removed_fragments = table_migration.extract_fragments_with_dependency dependency[:schema_name], dependency[:table_name]
104
- # create a new table migration for these fragments (there should only
105
- # be one, but we treat them as an array to futiure proof this)
106
- new_migration = TableMigration.new(schema_name, table_migration.table_name)
107
- # place these fragments in their own migration
108
- removed_fragments.each do |removed_fragment|
109
- new_migration.add_fragment removed_fragment
110
- end
111
- # add the new migration to the list of migrations
112
- schema_migrations[:additional_migrations] << new_migration
113
- end
114
- end
115
- end
97
+ # For each migration, we recursively traverse the dependency graph to detect and handle circular
98
+ # dependencies.
99
+ #
100
+ # Initially, all the fragments which pertain to a particular table are grouped together in
101
+ # the same migration. If a circular dependency between migrations is detected, then we simply
102
+ # pop the offending migration fragments out of the dedicated table migration and into a new
103
+ # migration. This allows the migration to be processed later, and resolves the circular dependency.
104
+ log.info " Resolving circular dependencies between migrations"
105
+ completed_table_migrations = []
106
+ all_table_migrations.each do |table_migration|
107
+ # skip it if it's already been processed
108
+ next if completed_table_migrations.include? table_migration
109
+ # recusrsively resolve the circular dependencies for this migration
110
+ resolve_circular_dependencies table_migration, all_table_migrations, database_migrations, completed_table_migrations
116
111
  end
117
112
 
118
113
  # Prepare a dependency sorter, this is used to sort the migrations via rubys included Tsort module
@@ -123,6 +118,7 @@ module DynamicMigrations
123
118
  # migration1 => [migration2, migration3],
124
119
  # migration3 => [migration2]
125
120
  # }
121
+ log.info " Preparing migrations for sorting"
126
122
  dependency_sorter = MigrationDependencySorter.new
127
123
  database_migrations.each do |schema_name, schema_migrations|
128
124
  if schema_migrations[:schema_migration]
@@ -161,13 +157,24 @@ module DynamicMigrations
161
157
  # if there is a schema migration, then it should always come first
162
158
  # so make the table migration depend on it
163
159
  deps << schema_migrations[:schema_migration] if schema_migrations[:schema_migration]
160
+
164
161
  # additional migrations are always dependent on the table migration which they came from
165
162
  table_migration = schema_migrations[:table_migrations][additional_migration.table_name]
166
163
  # if the table migration is not found, then it's safe to assume the table was created
167
164
  # by an earlier set of migrations
168
165
  unless table_migration.nil?
169
166
  deps << table_migration
167
+
168
+ # if the table migration has any dependencies on functions or enums, then add them
169
+ (table_migration.function_dependencies + table_migration.enum_dependencies).each do |dependency|
170
+ # functions are always added to a schema specific migration, if it does not exist then
171
+ # we can assume the function was added in a previous set of migrations
172
+ if (dependencies_schema_migration = database_migrations[dependency[:schema_name]] && database_migrations[dependency[:schema_name]][:schema_migration])
173
+ deps << dependencies_schema_migration
174
+ end
175
+ end
170
176
  end
177
+
171
178
  # if the additional_migration has any dependencies on other tables, then add them too
172
179
  additional_migration.table_dependencies.each do |dependency|
173
180
  # find the table migration which matches the dependency
@@ -178,19 +185,12 @@ module DynamicMigrations
178
185
  deps << dependent_migration
179
186
  end
180
187
  end
181
- # if the table migration has any dependencies on functions or enums, then add them
182
- (table_migration.function_dependencies + table_migration.enum_dependencies).each do |dependency|
183
- # functions are always added to a schema specific migration, if it does not exist then
184
- # we can assume the function was added in a previous set of migrations
185
- if (dependencies_schema_migration = database_migrations[dependency[:schema_name]] && database_migrations[dependency[:schema_name]][:schema_migration])
186
- deps << dependencies_schema_migration
187
- end
188
- end
189
188
  end
190
189
  end
191
190
 
192
191
  # sort the migrations so that they are executed in the correct order
193
192
  # the order is determined by their dependencies
193
+ log.info " Sorting migrations based on their dependencies"
194
194
  final_migrations = dependency_sorter.tsort
195
195
 
196
196
  # if any database only migrations exist, then add them to the front of the array here
@@ -210,23 +210,73 @@ module DynamicMigrations
210
210
 
211
211
  private
212
212
 
213
- def circular_dependency? schema_name, table_name, dependency, all_table_migrations
214
- # if the current dependency (schema_name and table_name) matches the original migration then we have a circular dependency
215
- if dependency[:schema_name] == schema_name && dependency[:table_name] == table_name
216
- true
217
- else
218
- # get all mirations which are for the same schema and table as the dependency
219
- dependent_migrations = all_table_migrations.filter { |m| m.schema_name == dependency[:schema_name] && m.table_name == dependency[:table_name] }
220
- # recursively call this method for all the dependencies for these migrations
221
- dependent_migrations.each do |dependent_migration|
222
- dependent_migration.table_dependencies.each do |next_dependency|
223
- # if we find a dependency which matches the original schema and table name then we have a circular dependency
224
- if circular_dependency?(schema_name, table_name, next_dependency, all_table_migrations)
225
- return true
213
+ # Initially, all the fragments which pertain to a particular table are grouped together in
214
+ # the same migration. If a circular dependency between migrations is detected, then we simply
215
+ # pop the offending migration fragments out of the dedicated table migration and into a new
216
+ # migration. This allows the migration to be processed later, and resolves the circular dependency.
217
+ #
218
+ # Note, "table migrations" are the default migrations which initially contain all the fragments for
219
+ # a particular table.
220
+ #
221
+ # `table_migration` is the current migration which is being processed
222
+ # `all_table_migrations` is all the table migrations in this current set of migrations
223
+ # `database_migrations` is a hash of all the migrations, organized by schema and table, we need this
224
+ # object so that we can add any new migrations which are created to resolve circular dependencies
225
+ # `completed_table_migrations` is an array of all the table migrations which have already been
226
+ # processed, we use this for performance reasons, so that we dont process the same migration twice
227
+ # `stack` is an array of all the migrations which have been processed so far in this current recursive
228
+ # path, this is used to detect circular dependencies.
229
+ def resolve_circular_dependencies table_migration, all_table_migrations, database_migrations, completed_table_migrations, stack = []
230
+ # process all the current dependencies for this migration
231
+ # each dependency is a hash, with the schema_name and table_name
232
+ table_migration.table_dependencies.each do |dependency|
233
+ # look in the list of all table migrations and try and find the migration which
234
+ # matches the current dependency, note that this migration may not exist because
235
+ # the table could have been created in a previous set of migrations
236
+ if (next_table_migration = all_table_migrations.find { |m| m.schema_name == dependency[:schema_name] && m.table_name == dependency[:table_name] })
237
+ # if this migration has already been processed, then we can skip it
238
+ next if completed_table_migrations.include? next_table_migration
239
+
240
+ key = "#{next_table_migration.schema_name}.#{next_table_migration.table_name}"
241
+ # if this migration already exists in the stack, then we have a circular dependency
242
+ if stack.include? key
243
+ log.info " Resolving circular dependency for #{table_migration.schema_name}.#{table_migration.table_name} -> #{next_table_migration.schema_name}.#{next_table_migration.table_name}"
244
+
245
+ # if the number of fragments in the table migration is equal to the number of fragments
246
+ # which would be removed, then there is no need to split the migration
247
+ next if table_migration.fragments.count == table_migration.fragments_with_table_dependency_count(next_table_migration.schema_name, next_table_migration.table_name)
248
+
249
+ # remove the fragments which are causing the circular dependency
250
+ removed_fragments = table_migration.extract_fragments_with_table_dependency next_table_migration.schema_name, next_table_migration.table_name
251
+
252
+ # create a new table migration for these fragments
253
+ new_migration = TableMigration.new(table_migration.schema_name, table_migration.table_name)
254
+
255
+ # place these fragments in their own migration
256
+ removed_fragments.each do |removed_fragment|
257
+ new_migration.add_fragment removed_fragment
226
258
  end
259
+
260
+ # add the new migration to the list of additional (not standard table migrations) for
261
+ # this schema
262
+ database_migrations[table_migration.schema_name][:additional_migrations] << new_migration
263
+
264
+ # continue to the next dependency
265
+ next
227
266
  end
267
+
268
+ # create a new stack, so that each recursive call has it's own copy
269
+ new_stack = stack + [key]
270
+
271
+ # recursively move on to the next migration
272
+ resolve_circular_dependencies next_table_migration, all_table_migrations, database_migrations, completed_table_migrations, new_stack
273
+
274
+ # when the code reaches this point, we have completed the recursive traversal of
275
+ # all the dependencies originating from next_table_migration, so we can add it to
276
+ # the array of completed migrations, note that this array is shared across all
277
+ # recursive calls, so that we can keep track of which migrations have been processed
278
+ completed_table_migrations << next_table_migration
228
279
  end
229
- false
230
280
  end
231
281
  end
232
282
 
@@ -274,6 +324,10 @@ module DynamicMigrations
274
324
  def trim_lines string
275
325
  string.split("\n").map(&:rstrip).join("\n")
276
326
  end
327
+
328
+ def log
329
+ @logger
330
+ end
277
331
  end
278
332
  end
279
333
  end
@@ -11,14 +11,14 @@ module DynamicMigrations
11
11
  # if the extension exists in the configuration but not in the database
12
12
  # then we have to create it
13
13
  if configuration_extension[:exists] == true && !database_extension[:exists]
14
- log.info "Extension `#{extension_name}` exists in configuration but not in the database"
14
+ log.debug "Extension `#{extension_name}` exists in configuration but not in the database"
15
15
  # a migration to create the extension
16
16
  @generator.enable_extension extension_name
17
17
 
18
18
  # if the extension exists in the database but not in the configuration
19
19
  # then we need to delete it
20
20
  elsif database_extension[:exists] == true && !configuration_extension[:exists]
21
- log.info "Extension `#{extension_name}` exists in database but not in the configuration"
21
+ log.debug "Extension `#{extension_name}` exists in database but not in the configuration"
22
22
  # a migration to drop the extension
23
23
  if Postgres.remove_unused_extensions?
24
24
  @generator.disable_extension extension_name
@@ -10,10 +10,10 @@ module DynamicMigrations
10
10
  module Enums
11
11
  def process_enums schema_name, configuration_enums, database_enums
12
12
  # process all the enums
13
- log.info " Processing tables..."
13
+ log.debug " Processing tables"
14
14
  enum_names = (configuration_enums.keys + database_enums.keys).uniq
15
15
  enum_names.each do |enum_name|
16
- log.info " Processing table #{enum_name}..."
16
+ log.debug " Processing table #{enum_name}"
17
17
  process_enum schema_name, enum_name, configuration_enums[enum_name] || {}, database_enums[enum_name] || {}
18
18
  end
19
19
  end
@@ -22,7 +22,7 @@ module DynamicMigrations
22
22
  # If the enum exists in the configuration but not in the database
23
23
  # then we have to create it.
24
24
  if configuration_enum[:exists] == true && !database_enum[:exists]
25
- log.info " Enum `#{enum_name}` exists in configuration but not in the database"
25
+ log.debug " Enum `#{enum_name}` exists in configuration but not in the database"
26
26
 
27
27
  # a migration to create the enum
28
28
  enum = @database.configured_schema(schema_name).enum(enum_name)
@@ -35,7 +35,7 @@ module DynamicMigrations
35
35
  # If the schema exists in the database but not in the configuration
36
36
  # then we need to delete it.
37
37
  elsif database_enum[:exists] == true && !configuration_enum[:exists]
38
- log.info " Enum `#{enum_name}` exists in database but not in the configuration"
38
+ log.debug " Enum `#{enum_name}` exists in database but not in the configuration"
39
39
 
40
40
  # a migration to create the enum
41
41
  enum = @database.loaded_schema(schema_name).enum(enum_name)
@@ -44,9 +44,9 @@ module DynamicMigrations
44
44
  # If the enum exists in both the configuration and database representations
45
45
  # but the values is different then we need to update the values.
46
46
  elsif configuration_enum[:values][:matches] == false
47
- log.info " Enum `#{enum_name}` exists in both configuration and the database"
47
+ log.debug " Enum `#{enum_name}` exists in both configuration and the database"
48
48
 
49
- log.info " Enum `#{enum_name}` values are different"
49
+ log.debug " Enum `#{enum_name}` values are different"
50
50
  original_enum = @database.loaded_schema(schema_name).enum(enum_name)
51
51
  updated_enum = @database.configured_schema(schema_name).enum(enum_name)
52
52
  @generator.update_enum original_enum, updated_enum
@@ -54,10 +54,10 @@ module DynamicMigrations
54
54
  if configuration_enum[:description][:matches] == false
55
55
  # if the description was removed
56
56
  if configuration_enum[:description].nil?
57
- log.info " Enum `#{enum_name}` description exists in database but not in the configuration"
57
+ log.debug " Enum `#{enum_name}` description exists in database but not in the configuration"
58
58
  @generator.remove_enum_comment updated_enum
59
59
  else
60
- log.info " Enum `#{enum_name}` description does not match"
60
+ log.debug " Enum `#{enum_name}` description does not match"
61
61
  @generator.set_enum_comment updated_enum
62
62
  end
63
63
  end
@@ -68,10 +68,10 @@ module DynamicMigrations
68
68
  enum = @database.configured_schema(schema_name).enum(enum_name)
69
69
  # if the description was removed
70
70
  if configuration_enum[:description].nil?
71
- log.info " Enum `#{enum_name}` description exists in database but not in the configuration"
71
+ log.debug " Enum `#{enum_name}` description exists in database but not in the configuration"
72
72
  @generator.remove_enum_comment enum
73
73
  else
74
- log.info " Enum `#{enum_name}` description does not match"
74
+ log.debug " Enum `#{enum_name}` description does not match"
75
75
  @generator.set_enum_comment enum
76
76
  end
77
77
  end