dynamic_migrations 3.1.0 → 3.2.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.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/dynamic_migrations/postgres/generator/column.rb +44 -19
  4. data/lib/dynamic_migrations/postgres/generator/foreign_key_constraint.rb +35 -14
  5. data/lib/dynamic_migrations/postgres/generator/fragment.rb +37 -2
  6. data/lib/dynamic_migrations/postgres/generator/function.rb +50 -25
  7. data/lib/dynamic_migrations/postgres/generator/index.rb +34 -14
  8. data/lib/dynamic_migrations/postgres/generator/migration.rb +175 -0
  9. data/lib/dynamic_migrations/postgres/generator/migration_dependency_sorter.rb +21 -0
  10. data/lib/dynamic_migrations/postgres/generator/primary_key.rb +16 -6
  11. data/lib/dynamic_migrations/postgres/generator/schema.rb +14 -6
  12. data/lib/dynamic_migrations/postgres/generator/schema_migration.rb +17 -0
  13. data/lib/dynamic_migrations/postgres/generator/table.rb +39 -19
  14. data/lib/dynamic_migrations/postgres/generator/table_migration.rb +51 -0
  15. data/lib/dynamic_migrations/postgres/generator/trigger.rb +34 -14
  16. data/lib/dynamic_migrations/postgres/generator/unique_constraint.rb +34 -14
  17. data/lib/dynamic_migrations/postgres/generator/validation.rb +38 -18
  18. data/lib/dynamic_migrations/postgres/generator.rb +163 -294
  19. data/lib/dynamic_migrations/postgres/server/database/connection.rb +15 -0
  20. data/lib/dynamic_migrations/version.rb +1 -1
  21. data/lib/dynamic_migrations.rb +4 -2
  22. metadata +6 -4
  23. data/lib/dynamic_migrations/postgres/generator/schema_migrations/section.rb +0 -37
  24. data/lib/dynamic_migrations/postgres/generator/schema_migrations.rb +0 -92
@@ -7,255 +7,14 @@ module DynamicMigrations
7
7
  class DeferrableOptionsError < StandardError
8
8
  end
9
9
 
10
- class UnexpectedMigrationMethodNameError < StandardError
11
- end
12
-
13
10
  class MissingDescriptionError < StandardError
14
11
  end
15
12
 
16
13
  class NoDifferenceError < StandardError
17
14
  end
18
15
 
19
- # these sections are in order for which they will appear in a migration,
20
- # note that removals come before additions, and that the order here optomizes
21
- # for dependencies (i.e. columns have to be created before indexes are added and
22
- # triggers are removed before functions are dropped)
23
- STRUCTURE = [
24
- {
25
- header_comment: <<~COMMENT,
26
- #
27
- # Remove Functions
28
- #
29
- COMMENT
30
- methods: [
31
- :remove_function_comment,
32
- :drop_function
33
- ]
34
- },
35
- {
36
- header_comment: <<~COMMENT,
37
- #
38
- # Remove Triggers
39
- #
40
- COMMENT
41
- methods: [
42
- :remove_trigger_comment,
43
- :remove_trigger
44
- ]
45
- },
46
- {
47
- header_comment: <<~COMMENT,
48
- #
49
- # Remove Validations
50
- #
51
- COMMENT
52
- methods: [
53
- :remove_validation,
54
- :remove_unique_constraint
55
- ]
56
- },
57
- {
58
- header_comment: <<~COMMENT,
59
- #
60
- # Remove Foreign Keys
61
- #
62
- COMMENT
63
- methods: [
64
- :remove_foreign_key
65
- ]
66
- },
67
- {
68
- header_comment: <<~COMMENT,
69
- #
70
- # Remove Primary Keys
71
- #
72
- COMMENT
73
- methods: [
74
- :remove_primary_key
75
- ]
76
- },
77
- {
78
- header_comment: <<~COMMENT,
79
- #
80
- # Remove Indexes
81
- #
82
- COMMENT
83
- methods: [
84
- :remove_index,
85
- :remove_index_comment
86
- ]
87
- },
88
- {
89
- header_comment: <<~COMMENT,
90
- #
91
- # Remove Columns
92
- #
93
- COMMENT
94
- methods: [
95
- :remove_column
96
- ]
97
- },
98
- {
99
- header_comment: <<~COMMENT,
100
- #
101
- # Remove Tables
102
- #
103
- COMMENT
104
- break_after: true,
105
- methods: [
106
- :drop_table
107
- ]
108
- },
109
- {
110
- # this is important enough to get it's own migration
111
- break_before: true,
112
- break_after: true,
113
- header_comment: <<~COMMENT,
114
- #
115
- # Drop this schema
116
- #
117
- COMMENT
118
- methods: [
119
- :drop_schema
120
- ]
121
- },
122
- {
123
- # this is important enough to get it's own migration
124
- break_before: true,
125
- break_after: true,
126
- header_comment: <<~COMMENT,
127
- #
128
- # Create this schema
129
- #
130
- COMMENT
131
- methods: [
132
- :create_schema
133
- ]
134
- },
135
- {
136
- header_comment: <<~COMMENT,
137
- #
138
- # Create Table
139
- #
140
- COMMENT
141
- methods: [
142
- :create_table
143
- ]
144
- },
145
- {
146
- header_comment: <<~COMMENT,
147
- #
148
- # Tables
149
- #
150
- COMMENT
151
- methods: [
152
- :remove_table_comment,
153
- :set_table_comment
154
- ]
155
- },
156
- {
157
- header_comment: <<~COMMENT,
158
- #
159
- # Additional Columns
160
- #
161
- COMMENT
162
- methods: [
163
- :add_column
164
- ]
165
- },
166
- {
167
- header_comment: <<~COMMENT,
168
- #
169
- # Update Columns
170
- #
171
- COMMENT
172
- methods: [
173
- :change_column,
174
- :remove_column_comment,
175
- :set_column_comment
176
- ]
177
- },
178
- {
179
- header_comment: <<~COMMENT,
180
- #
181
- # Primary Key
182
- #
183
- COMMENT
184
- methods: [
185
- :add_primary_key
186
- ]
187
- },
188
- {
189
- header_comment: <<~COMMENT,
190
- #
191
- # Indexes
192
- #
193
- COMMENT
194
- methods: [
195
- :add_index,
196
- :set_index_comment
197
- ]
198
- },
199
- {
200
- header_comment: <<~COMMENT,
201
- #
202
- # Foreign Keys
203
- #
204
- COMMENT
205
- methods: [
206
- :add_foreign_key,
207
- :set_foreign_key_constraint_comment,
208
- :remove_foreign_key_constraint_comment
209
- ]
210
- },
211
- {
212
- header_comment: <<~COMMENT,
213
- #
214
- # Validations
215
- #
216
- COMMENT
217
- methods: [
218
- :add_validation,
219
- :add_unique_constraint,
220
- :set_validation_comment,
221
- :remove_validation_comment,
222
- :set_unique_constraint_comment,
223
- :remove_unique_constraint_comment
224
- ]
225
- },
226
- {
227
- header_comment: <<~COMMENT,
228
- #
229
- # Functions
230
- #
231
- COMMENT
232
- methods: [
233
- :create_function
234
- ]
235
- },
236
- {
237
- header_comment: <<~COMMENT,
238
- #
239
- # Triggers
240
- #
241
- COMMENT
242
- methods: [
243
- :add_trigger,
244
- :set_trigger_comment
245
- ]
246
- },
247
- {
248
- header_comment: <<~COMMENT,
249
- #
250
- # Update Functions
251
- #
252
- COMMENT
253
- methods: [
254
- :update_function,
255
- :set_function_comment
256
- ]
257
- }
258
- ]
16
+ class TableMigrationNotFound < StandardError
17
+ end
259
18
 
260
19
  include Schema
261
20
  include Table
@@ -269,79 +28,189 @@ module DynamicMigrations
269
28
  include Trigger
270
29
 
271
30
  def initialize
272
- @migrations = {}
31
+ @fragments = []
273
32
  end
274
33
 
275
- # builds an array of migrations that can be used to create the provided schema
34
+ # builds the final migrations
276
35
  def migrations
277
- final_migrations = {}
278
- # an array of table names which have migrations, we group migrations for the same table together
279
- @migrations.map do |schema_name, table_migrations|
280
- schema_migrations = SchemaMigrations.new
281
- # iterate through the tables which have migrations
282
- table_migrations.map do |table_name, fragments|
283
- # iterate through the structure object in order, and create the final migrations
284
- STRUCTURE.each do |section|
285
- # if this section requires a new migration, then end any current one
286
- if section[:break_before]
287
- schema_migrations.finalize
288
- end
289
-
290
- # add the header comment if we have a migration which matches one of the
291
- # methods in this section
292
- if (section[:methods] & fragments.keys).any?
293
- header_fragment = Fragment.new nil, nil, section[:header_comment]
294
- schema_migrations.add_fragment schema_name, table_name, :comment, header_fragment
295
- end
36
+ # a hash to hold the generated migrations orgnized by their schema and table
37
+ # this makes it easier and faster to work with them within this method
38
+ database_migrations = {}
39
+
40
+ # Process each fragment, and organize them into migrations. We create a shared
41
+ # Migration for each table, and a single shared migration for any schema migrations
42
+ # which do not relate to a table.
43
+ @fragments.each do |fragment|
44
+ # The first time this schema is encountered we create an object to hold the migrations
45
+ # and organize the different migrations.
46
+ schema_migrations = database_migrations[fragment.schema_name] ||= {
47
+ schema_migration: nil,
48
+ table_migrations: {},
49
+ # this array will hold any migrations which were created by splitting apart table
50
+ # migrations to resolve circular dependencies
51
+ additional_migrations: []
52
+ }
53
+ schema_name = fragment.schema_name
54
+ table_name = fragment.table_name
55
+ # If we have a table name, then add the migration fragment to a
56
+ # TableMigration which holds all of the migrations for this table
57
+ if table_name
58
+ table_migration = schema_migrations[:table_migrations][table_name] ||= TableMigration.new(schema_name, table_name)
59
+ table_migration.add_fragment fragment
60
+
61
+ # migration fragments which do not belong to a specific table are added
62
+ # to a dedicated SchemaMigration object
63
+ else
64
+ schema_migration = schema_migrations[:schema_migration] ||= SchemaMigration.new(schema_name)
65
+ schema_migration.add_fragment fragment
66
+ end
67
+ end
296
68
 
297
- # iterate through this sections methods in order and look
298
- # for any that match the migrations we have
299
- section[:methods].each do |method_name|
300
- # if we have any migration fragments for this method then add them
301
- fragments[method_name]&.each do |fragment|
302
- schema_migrations.add_fragment schema_name, table_name, method_name, fragment
69
+ # Convert the hash of migrations into an array of migrations, this is
70
+ # passed to the `circular_dependency?` method below, and any new migrations
71
+ # requred to resolve circular dependencies will be added to this array
72
+ all_table_migrations = database_migrations.values.map { |m| m[:table_migrations].values }.flatten
73
+
74
+ # iterate through all of the table migrations, and fix any circular dependencies caused
75
+ # by foreign key constraints
76
+ database_migrations.each do |schema_name, schema_migrations|
77
+ # we only need to process the TableMigrations, as the SchemaMigration
78
+ # never have dependencies
79
+ schema_migrations[:table_migrations].values.each do |table_migration|
80
+ # recursively test each table migration for circular dependencies
81
+ table_migration.dependencies.each do |dependency|
82
+ if circular_dependency? table_migration.schema_name, table_migration.table_name, dependency, all_table_migrations
83
+ # remove the fragment which is causing the circular dependency
84
+ removed_fragments = table_migration.extract_fragments_with_dependency dependency[:schema_name], dependency[:table_name]
85
+ # create a new table migration for these fragments (there should only
86
+ # be one, but we treat them as an array to futiure proof this)
87
+ new_migration = TableMigration.new(schema_name, table_migration.table_name)
88
+ # place these fragments in their own migration
89
+ removed_fragments.each do |removed_fragment|
90
+ new_migration.add_fragment removed_fragment
303
91
  end
92
+ # add the new migration to the list of migrations
93
+ schema_migrations[:additional_migrations] << new_migration
304
94
  end
95
+ end
96
+ end
97
+ end
305
98
 
306
- # if this section causes a new migration then end any current one
307
- if section[:break_after]
308
- schema_migrations.finalize
99
+ # Prepare a dependency sorter, this is used to sort the migrations via rubys included Tsort module
100
+ # The object used to sort the migrations is extended from a hash, and takes the form:
101
+ # {
102
+ # # every migration exists as a key, and its corresponding array is all the
103
+ # # migrations which it depends on
104
+ # migration1 => [migration2, migration3],
105
+ # migration3 => [migration2]
106
+ # }
107
+ dependency_sorter = MigrationDependencySorter.new
108
+ database_migrations.each do |schema_name, schema_migrations|
109
+ if schema_migrations[:schema_migration]
110
+ # the schema migration never has any dependencies
111
+ dependency_sorter[schema_migrations[:schema_migration]] = []
112
+ end
113
+ # add each table migration, and its dependencies
114
+ schema_migrations[:table_migrations].values.each do |table_migration|
115
+ deps = dependency_sorter[table_migration] = []
116
+ # if there is a schema migration, then it should always come first
117
+ # so make the table migration depend on it
118
+ deps << schema_migrations[:schema_migration] if schema_migrations[:schema_migration]
119
+ # if the table migration has any dependencies, then add them
120
+ table_migration.dependencies.each do |dependency|
121
+ # find the migration which matches the dependency
122
+ dependent_migration = schema_migrations[:table_migrations][dependency[:table_name]]
123
+ # if the table migration is not found, then it's safe to assume the table was created
124
+ # by an earlier set of migrations
125
+ unless dependent_migration.nil?
126
+ # add the dependent migration to the list of dependencies
127
+ deps << dependent_migration
128
+ end
129
+ end
130
+ end
131
+ # add each additional migration, and its dependencies
132
+ schema_migrations[:additional_migrations].each do |additional_migration|
133
+ deps = dependency_sorter[additional_migration] = []
134
+ # if there is a schema migration, then it should always come first
135
+ # so make the table migration depend on it
136
+ deps << schema_migrations[:schema_migration] if schema_migrations[:schema_migration]
137
+ # additional migrations are always dependent on the table migration which they came from
138
+ table_migration = schema_migrations[:table_migrations][additional_migration.table_name]
139
+ # if the table migration is not found, then it's safe to assume the table was created
140
+ # by an earlier set of migrations
141
+ unless table_migration.nil?
142
+ deps << table_migration
143
+ end
144
+ # if the additional_migration has any dependencies, then add them too
145
+ additional_migration.dependencies.each do |dependency|
146
+ # find the table migration which matches the dependency
147
+ dependent_migration = schema_migrations[:table_migrations][dependency[:table_name]]
148
+ # if the table migration is not found, then it's safe to assume the table was created
149
+ # by an earlier set of migrations
150
+ unless dependent_migration.nil?
151
+ deps << dependent_migration
309
152
  end
310
153
  end
311
- schema_migrations.finalize
312
154
  end
313
- final_migrations[schema_name] = schema_migrations.to_a
314
155
  end
315
- final_migrations
156
+
157
+ # sort the migrations so that they are executed in the correct order
158
+ # the order is determined by their dependencies
159
+ final_migrations = dependency_sorter.tsort
160
+
161
+ # return the final migrations in the expected format
162
+ final_migrations.map do |migration|
163
+ {
164
+ schema_name: migration.schema_name,
165
+ name: migration.name,
166
+ content: migration.content
167
+ }
168
+ end
316
169
  end
317
170
 
318
171
  private
319
172
 
320
- def supported_migration_method_names
321
- @supported_migration_method_names ||= STRUCTURE.map { |s| s[:methods] }.flatten
173
+ def circular_dependency? schema_name, table_name, dependency, all_table_migrations
174
+ # if the current dependency (schema_name and table_name) matches the original migration then we have a circular dependency
175
+ if dependency[:schema_name] == schema_name && dependency[:table_name] == table_name
176
+ true
177
+ else
178
+ # get all mirations which are for the same schema and table as the dependency
179
+ dependent_migrations = all_table_migrations.filter { |m| m.schema_name == dependency[:schema_name] && m.table_name == dependency[:table_name] }
180
+ # recursively call this method for all the dependencies for these migrations
181
+ dependent_migrations.each do |dependent_migration|
182
+ dependent_migration.dependencies.each do |next_dependency|
183
+ # if we find a dependency which matches the original schema and table name then we have a circular dependency
184
+ if circular_dependency?(schema_name, table_name, next_dependency, all_table_migrations)
185
+ return true
186
+ end
187
+ end
188
+ end
189
+ false
190
+ end
322
191
  end
323
192
 
324
- def supported_migration_method? method_name
325
- supported_migration_method_names.include? method_name
193
+ # tsort_each_node is used to iterate for all nodes over a graph.
194
+ def tsort_each_node(&block)
195
+ @fragments.each(&block)
326
196
  end
327
197
 
328
- def add_migration schema_name, table_name, migration_method, object_name, code_comment, migration
329
- raise ExpectedSymbolError, "Expected schema_name to be a symbol, got #{schema_name}" unless schema_name.is_a?(Symbol)
330
- raise ExpectedSymbolError, "Expected table_name to be a symbol, got #{table_name}" unless schema_name.is_a?(Symbol)
331
-
332
- unless supported_migration_method? migration_method
333
- raise UnexpectedMigrationMethodNameError, "Expected migration_method to be a valid migrator method, got `#{migration_method}`"
334
- end
198
+ # tsort_each_child is used to iterate for child nodes of a given node.
199
+ def tsort_each_child(node, &block)
200
+ @dep[node].each(&block)
201
+ end
335
202
 
203
+ def add_fragment schema:, migration_method:, object:, migration:, table: nil, code_comment: nil, dependent_table: nil
204
+ # Remove any empty lines and whitespace from the beginning or the end of the migration and then
205
+ # strip any empty lines witin the migration (remove the whitespace from them, not delete them).
336
206
  final_migration = strip_empty_lines(migration).strip
337
- fragment = Fragment.new(object_name, code_comment, final_migration)
207
+ fragment = Fragment.new(schema.name, table&.name, migration_method, object.name, code_comment, final_migration)
208
+ if dependent_table
209
+ fragment.set_dependent_table dependent_table.schema.name, dependent_table.name
210
+ end
338
211
 
339
- # note, table_name can be nil, which is OK because nil is a valid
340
- # key and we do want to group them all together
341
- @migrations[schema_name] ||= {}
342
- @migrations[schema_name][table_name] ||= {}
343
- @migrations[schema_name][table_name][migration_method] ||= []
344
- @migrations[schema_name][table_name][migration_method] << fragment
212
+ # add this fragment to the list
213
+ @fragments << fragment
345
214
 
346
215
  # return the newly created migration fragment
347
216
  fragment
@@ -32,6 +32,21 @@ module DynamicMigrations
32
32
  raise NotConnectedError
33
33
  end
34
34
  end
35
+
36
+ # Opens a connection to the database server, and yields the provided block
37
+ # before automatically closing the connection again. This is useful for
38
+ # executing one time queries against the database server.
39
+ def with_connection &block
40
+ # create a temporary connection to the server
41
+ connect
42
+ # perform work with the connection
43
+ # todo: `yield connection` would have been preferred, but rbs/steep doesnt understand that syntax
44
+ if block.is_a? Proc
45
+ block.call connection
46
+ end
47
+ # close the connection
48
+ disconnect
49
+ end
35
50
  end
36
51
  end
37
52
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicMigrations
4
- VERSION = "3.1.0"
4
+ VERSION = "3.2.0"
5
5
  end
@@ -69,8 +69,10 @@ require "dynamic_migrations/postgres/generator/unique_constraint"
69
69
  require "dynamic_migrations/postgres/generator/validation"
70
70
  require "dynamic_migrations/postgres/generator"
71
71
  require "dynamic_migrations/postgres/generator/fragment"
72
- require "dynamic_migrations/postgres/generator/schema_migrations"
73
- require "dynamic_migrations/postgres/generator/schema_migrations/section"
72
+ require "dynamic_migrations/postgres/generator/migration"
73
+ require "dynamic_migrations/postgres/generator/schema_migration"
74
+ require "dynamic_migrations/postgres/generator/table_migration"
75
+ require "dynamic_migrations/postgres/generator/migration_dependency_sorter"
74
76
 
75
77
  require "dynamic_migrations/active_record/migrators/schema"
76
78
  require "dynamic_migrations/active_record/migrators/validation"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Craig Ulliott
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-14 00:00:00.000000000 Z
11
+ date: 2023-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -90,11 +90,13 @@ files:
90
90
  - lib/dynamic_migrations/postgres/generator/fragment.rb
91
91
  - lib/dynamic_migrations/postgres/generator/function.rb
92
92
  - lib/dynamic_migrations/postgres/generator/index.rb
93
+ - lib/dynamic_migrations/postgres/generator/migration.rb
94
+ - lib/dynamic_migrations/postgres/generator/migration_dependency_sorter.rb
93
95
  - lib/dynamic_migrations/postgres/generator/primary_key.rb
94
96
  - lib/dynamic_migrations/postgres/generator/schema.rb
95
- - lib/dynamic_migrations/postgres/generator/schema_migrations.rb
96
- - lib/dynamic_migrations/postgres/generator/schema_migrations/section.rb
97
+ - lib/dynamic_migrations/postgres/generator/schema_migration.rb
97
98
  - lib/dynamic_migrations/postgres/generator/table.rb
99
+ - lib/dynamic_migrations/postgres/generator/table_migration.rb
98
100
  - lib/dynamic_migrations/postgres/generator/trigger.rb
99
101
  - lib/dynamic_migrations/postgres/generator/unique_constraint.rb
100
102
  - lib/dynamic_migrations/postgres/generator/validation.rb
@@ -1,37 +0,0 @@
1
- module DynamicMigrations
2
- module Postgres
3
- class Generator
4
- class SchemaMigrations
5
- class Section
6
- attr_reader :schema_name
7
- attr_reader :table_name
8
- attr_reader :content_type
9
- attr_reader :fragment
10
-
11
- def initialize schema_name, table_name, content_type, fragment
12
- @schema_name = schema_name
13
- @table_name = table_name
14
- @content_type = content_type
15
- @fragment = fragment
16
- end
17
-
18
- def object_name
19
- @fragment.object_name
20
- end
21
-
22
- def to_s
23
- @fragment.to_s
24
- end
25
-
26
- def is_comment?
27
- content_type? :comment
28
- end
29
-
30
- def content_type? content_type
31
- @content_type == content_type
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end