dynamic_migrations 3.8.6 → 3.8.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/lib/dynamic_migrations/active_record/migrators/validation.rb +2 -20
  4. data/lib/dynamic_migrations/postgres/generator/enum.rb +13 -9
  5. data/lib/dynamic_migrations/postgres/generator/fragment.rb +10 -3
  6. data/lib/dynamic_migrations/postgres/generator/function.rb +13 -13
  7. data/lib/dynamic_migrations/postgres/generator/migration.rb +45 -7
  8. data/lib/dynamic_migrations/postgres/generator/table_migration.rb +2 -0
  9. data/lib/dynamic_migrations/postgres/generator/validation.rb +1 -3
  10. data/lib/dynamic_migrations/postgres/generator/validation_template_base.rb +1 -7
  11. data/lib/dynamic_migrations/postgres/generator.rb +100 -46
  12. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/extensions.rb +2 -2
  13. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/enums.rb +10 -10
  14. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/functions.rb +11 -11
  15. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/columns.rb +11 -11
  16. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/foreign_key_constraints.rb +11 -11
  17. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/indexes.rb +11 -11
  18. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/primary_key.rb +6 -6
  19. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/triggers.rb +11 -11
  20. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/unique_constraints.rb +11 -11
  21. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/validations.rb +11 -11
  22. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables.rb +8 -8
  23. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas.rb +3 -3
  24. data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations.rb +4 -4
  25. data/lib/dynamic_migrations/postgres/server/database/differences.rb +25 -20
  26. data/lib/dynamic_migrations/postgres/server/database/keys_and_unique_constraints_loader.rb +2 -2
  27. data/lib/dynamic_migrations/postgres/server/database/loaded_schemas_builder.rb +1 -1
  28. data/lib/dynamic_migrations/postgres/server/database/schema/enum.rb +9 -2
  29. data/lib/dynamic_migrations/postgres/server/database/schema/function.rb +2 -2
  30. data/lib/dynamic_migrations/postgres/server/database/schema/table/column.rb +6 -2
  31. data/lib/dynamic_migrations/postgres/server/database/schema/table/columns.rb +0 -6
  32. data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rb +6 -2
  33. data/lib/dynamic_migrations/postgres/server/database/schema/table/index.rb +7 -3
  34. data/lib/dynamic_migrations/postgres/server/database/schema/table/primary_key.rb +6 -2
  35. data/lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb +10 -8
  36. data/lib/dynamic_migrations/postgres/server/database/schema/table/triggers.rb +2 -2
  37. data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rb +6 -2
  38. data/lib/dynamic_migrations/postgres/server/database/schema/table/validation.rb +12 -19
  39. data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +62 -2
  40. data/lib/dynamic_migrations/postgres/server/database/validations_loader.rb +1 -3
  41. data/lib/dynamic_migrations/version.rb +1 -1
  42. data/sig/dynamic_migrations/active_record/migrators/validation.rbs +1 -1
  43. data/sig/dynamic_migrations/postgres/generator/enum.rbs +2 -0
  44. data/sig/dynamic_migrations/postgres/generator/fragment.rbs +3 -0
  45. data/sig/dynamic_migrations/postgres/generator/function.rbs +1 -0
  46. data/sig/dynamic_migrations/postgres/generator/migration.rbs +1 -0
  47. data/sig/dynamic_migrations/postgres/generator/schema_migration.rbs +2 -0
  48. data/sig/dynamic_migrations/postgres/generator/table_migration.rbs +3 -0
  49. data/sig/dynamic_migrations/postgres/generator/validation_template_base.rbs +0 -1
  50. data/sig/dynamic_migrations/postgres/generator.rbs +3 -1
  51. data/sig/dynamic_migrations/postgres/server/database/schema/enum.rbs +3 -0
  52. data/sig/dynamic_migrations/postgres/server/database/schema/table/column.rbs +3 -0
  53. data/sig/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rbs +3 -0
  54. data/sig/dynamic_migrations/postgres/server/database/schema/table/index.rbs +3 -0
  55. data/sig/dynamic_migrations/postgres/server/database/schema/table/primary_key.rbs +3 -0
  56. data/sig/dynamic_migrations/postgres/server/database/schema/table/trigger.rbs +3 -0
  57. data/sig/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rbs +3 -0
  58. data/sig/dynamic_migrations/postgres/server/database/schema/table/validation.rbs +4 -3
  59. data/sig/dynamic_migrations/postgres/server/database/schema/table.rbs +3 -0
  60. metadata +2 -3
  61. data/lib/dynamic_migrations/name_helper.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00405e194209e083433ebb55aa305922a16e24290ead4f240420602c9465bc90
4
- data.tar.gz: 57b7136d7eb1f7efc5382101cf24a4dc5ed4852e66a5f67faf368294e05632c0
3
+ metadata.gz: d822c8e95efe29d0f03f07162f27909ba4222ab4be98c421d64bd167fe923443
4
+ data.tar.gz: 88a20111dae2a34872d0759f47a0c8c29eab87d354ebb7a55d33152c9d06c6e3
5
5
  SHA512:
6
- metadata.gz: bc594cb118caa67c0e9982f839c762e074b789b083328a1b69b3261297bd34055a53739f51cabb6f6e5685ea76f80eb4574b45649fd202bf213a579713842fac
7
- data.tar.gz: 8d42ee7274c26e25086eae4e9c2646f958f930a18fd03eb9c6262f734cf55af380499763e3508a05156a6f1baad4c7bc30007e9d242674d9594bf83e75a7bf1f
6
+ metadata.gz: 2210081126891f62ebadb6dd9f9f53cafd5cdd7ae196558b00e94ffdf91b64b1686d65c95ec07357ee8043ba1e8c698812ed07503d8b46287ab5496e26beb46d
7
+ data.tar.gz: eb23900019d373320e0f2510eadd2714c1c2c4324af0b0a4f68886754204deb7ebc0de11f833bf43b0a53b38f80448b70370ccd57ca55005c97715c397f4b579
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.8](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.7...v3.8.8) (2023-10-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * adding a specific error type for invalid names ([12df7bd](https://github.com/craigulliott/dynamic_migrations/commit/12df7bddcb947d3ab5b1b00eebefdda0a3ce15ac))
9
+ * fixed bug where check_clause and action_condition were being modified, also freezing strings to prevent similar bugs in the future ([dc1191b](https://github.com/craigulliott/dynamic_migrations/commit/dc1191b7e219b73da8469742259686721265a2a7))
10
+ * removed arbitrary column name sorting, as we want migrations to create columns in the order they were added ([e9a515e](https://github.com/craigulliott/dynamic_migrations/commit/e9a515eb9dbf16ef5a5e77ca0084a6514690530b))
11
+ * removing deferrable and initially deferred from check constraints because postgres doesn't support them ([945aef2](https://github.com/craigulliott/dynamic_migrations/commit/945aef2419fd5f55a0551929244f3f9b0a29a905))
12
+ * should not have assumed enums in the same schema use their short name, enums are always referenced by their full name (including schema) ([a9863ba](https://github.com/craigulliott/dynamic_migrations/commit/a9863ba49727e5016c8eae74f5f2a6e7ab8c1042))
13
+ * we were not resolving the correct deferred and initially_deferred values when loading existing database structure ([02c539a](https://github.com/craigulliott/dynamic_migrations/commit/02c539a8ea153cde6d06f6be6d8fdb0aaf851f40))
14
+
15
+ ## [3.8.7](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.6...v3.8.7) (2023-10-09)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * 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))
21
+ * added enum value length validation ([d82ff57](https://github.com/craigulliott/dynamic_migrations/commit/d82ff57f5b51c8636e87b40b8d6b016d11e6c72a))
22
+ * also drastic performance improvement of migration circular dependency resolution ([2915b8f](https://github.com/craigulliott/dynamic_migrations/commit/2915b8f28c9202bd18f221cfafa2c4352a80fd2c))
23
+ * 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))
24
+ * 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))
25
+ * 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))
26
+ * reducing some log messages from info level to debug level ([cdf1200](https://github.com/craigulliott/dynamic_migrations/commit/cdf12000186a9e373d5085f83d2fb197c7796c6c))
27
+ * table migrations now allow enum related fragments ([b894bf1](https://github.com/craigulliott/dynamic_migrations/commit/b894bf14743c51b505693818407ba6bfb3f31571))
28
+ * updating log level for some log entries ([456a90e](https://github.com/craigulliott/dynamic_migrations/commit/456a90e7fe6da78667a84928b75217f89c344f35))
29
+
3
30
  ## [3.8.6](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.5...v3.8.6) (2023-10-08)
4
31
 
5
32
 
@@ -3,7 +3,7 @@ module DynamicMigrations
3
3
  module Migrators
4
4
  module Validation
5
5
  # this exists because because the standard rails migration does not support deffered constraints
6
- def add_validation table_name, name:, initially_deferred: false, deferrable: false, comment: nil, &block
6
+ def add_validation table_name, name:, comment: nil, &block
7
7
  unless block
8
8
  raise MissingFunctionBlockError, "create_function requires a block"
9
9
  end
@@ -12,28 +12,10 @@ module DynamicMigrations
12
12
  sql = block.call.strip
13
13
  end
14
14
 
15
- if initially_deferred == true && deferrable == false
16
- raise DeferrableOptionsError, "A constraint can only be initially deferred if it is also deferrable"
17
- end
18
-
19
- # allow it to be deferred, and defer it by default
20
- deferrable_sql = if initially_deferred
21
- "DEFERRABLE INITIALLY DEFERRED"
22
-
23
- # allow it to be deferred, but do not deferr by default
24
- elsif deferrable
25
- "DEFERRABLE INITIALLY IMMEDIATE"
26
-
27
- # it can not be deferred (this is the default)
28
- else
29
- "NOT DEFERRABLE"
30
- end
31
-
32
15
  execute <<~SQL
33
16
  ALTER TABLE #{table_name}
34
17
  ADD CONSTRAINT #{name}
35
- CHECK (#{sql})
36
- #{deferrable_sql};
18
+ CHECK (#{sql});
37
19
  SQL
38
20
 
39
21
  if comment.is_a? String
@@ -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
@@ -5,6 +5,9 @@ module DynamicMigrations
5
5
  class InvalidNameError < StandardError
6
6
  end
7
7
 
8
+ class ContentRequiredError < StandardError
9
+ end
10
+
8
11
  attr_reader :schema_name
9
12
  attr_reader :table_name
10
13
  attr_reader :migration_method
@@ -15,7 +18,7 @@ module DynamicMigrations
15
18
  attr_reader :dependency_enum_name
16
19
 
17
20
  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/
21
+ valid_name_regex = /\A[a-z][a-z0-9]*(_[a-z0-9]+)*\z/
19
22
 
20
23
  unless schema_name.nil? || (schema_name.to_s.match valid_name_regex)
21
24
  raise InvalidNameError, "Invalid schema name `#{schema_name}`, must only be lowercase letters, numbers and underscores"
@@ -33,8 +36,12 @@ module DynamicMigrations
33
36
  @object_name = object_name
34
37
 
35
38
  @migration_method = migration_method
36
- @code_comment = code_comment
37
- @content = content
39
+ @code_comment = code_comment&.freeze
40
+
41
+ if content.nil?
42
+ raise ContentRequiredError, "Content is required for a fragment"
43
+ end
44
+ @content = content.freeze
38
45
  end
39
46
 
40
47
  # Returns a string representation of the fragment for use in the final
@@ -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"
@@ -38,9 +38,7 @@ module DynamicMigrations
38
38
  else
39
39
 
40
40
  options = {
41
- name: ":#{validation.name}",
42
- deferrable: validation.deferrable,
43
- initially_deferred: validation.initially_deferred
41
+ name: ":#{validation.name}"
44
42
  }
45
43
 
46
44
  if validation.description.nil?
@@ -10,17 +10,11 @@ module DynamicMigrations
10
10
 
11
11
  def initialize validation, code_comment
12
12
  @validation = validation
13
- @code_comment = code_comment
13
+ @code_comment = code_comment&.freeze
14
14
  end
15
15
 
16
16
  private
17
17
 
18
- def assert_not_deferred!
19
- if @validation.initially_deferred || @validation.deferrable
20
- raise TemplateError, "#{self.class.name} validation template requires constraints to be are not deferrable"
21
- end
22
- end
23
-
24
18
  def assert_column_count! count = 1
25
19
  if @validation.columns.count != count
26
20
  raise TemplateError, "#{self.class.name} validation template requires a validation with only #{count} column"
@@ -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