dynamic_migrations 3.1.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicMigrations
4
- VERSION = "3.1.0"
4
+ VERSION = "3.1.1"
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.1.1
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
@@ -1,92 +0,0 @@
1
- module DynamicMigrations
2
- module Postgres
3
- class Generator
4
- class SchemaMigrations
5
- class SectionNotFoundError < StandardError
6
- end
7
-
8
- attr_reader :current_migration_sections
9
-
10
- def initialize
11
- @migrations = []
12
- @current_migration_sections = []
13
- end
14
-
15
- def add_fragment schema_name, table_name, content_type, fragment
16
- @current_migration_sections << Section.new(schema_name, table_name, content_type, fragment)
17
- end
18
-
19
- def finalize
20
- if @current_migration_sections.any?
21
-
22
- contents = []
23
- @current_migration_sections.each do |section|
24
- contents << section.to_s
25
- # add an empty line between sections (unless this is a comment section)
26
- unless section.is_comment?
27
- contents << ""
28
- end
29
- end
30
-
31
- @migrations << {
32
- name: generate_current_migration_name,
33
- content: contents.join("\n").strip
34
- }
35
-
36
- @current_migration_sections = []
37
- end
38
- end
39
-
40
- def to_a
41
- @migrations
42
- end
43
-
44
- private
45
-
46
- def current_migration_has_content_type? content_type
47
- @current_migration_sections.map(&:content_type).include? content_type
48
- end
49
-
50
- def current_migration_section_of_content_type content_type
51
- section = @current_migration_sections.find(&:content_type)
52
- if section.nil?
53
- raise SectionNotFoundError, "No section of type #{content_type} found"
54
- end
55
- section
56
- end
57
-
58
- # return true if the current migration only has the provided content types and comments
59
- def current_migration_only_content_types? content_types
60
- (@current_migration_sections.map(&:content_type) - content_types - [:comment]).empty?
61
- end
62
-
63
- def generate_current_migration_name
64
- if current_migration_has_content_type? :create_schema
65
- "create_#{current_migration_section_of_content_type(:create_schema).schema_name}_schema".to_sym
66
-
67
- elsif current_migration_has_content_type? :drop_schema
68
- "drop_#{current_migration_section_of_content_type(:drop_schema).schema_name}_schema".to_sym
69
-
70
- elsif current_migration_has_content_type? :create_table
71
- "create_#{current_migration_section_of_content_type(:create_table).table_name}".to_sym
72
-
73
- elsif current_migration_has_content_type? :drop_table
74
- "drop_#{current_migration_section_of_content_type(:drop_table).table_name}".to_sym
75
-
76
- elsif current_migration_only_content_types? [:create_function]
77
- "create_function_#{@current_migration_sections.find { |s| s.content_type == :create_function }&.object_name}".to_sym
78
-
79
- elsif current_migration_only_content_types? [:create_function, :update_function, :drop_function, :set_function_comment, :remove_function_comment]
80
- :schema_functions
81
-
82
- elsif @current_migration_sections.first&.table_name
83
- "changes_for_#{@current_migration_sections.first&.table_name}".to_sym
84
-
85
- else
86
- :changes
87
- end
88
- end
89
- end
90
- end
91
- end
92
- end