dynamic_migrations 3.8.4 → 3.8.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a9d25190ae4b67f3154feae64f9aa7a11d6b6effc9a6422cce43cde28cbcc68
4
- data.tar.gz: 6a7a23a1d7793155f383899ec83f3477ec18581cf94a762a0729466e6d791ac4
3
+ metadata.gz: 00405e194209e083433ebb55aa305922a16e24290ead4f240420602c9465bc90
4
+ data.tar.gz: 57b7136d7eb1f7efc5382101cf24a4dc5ed4852e66a5f67faf368294e05632c0
5
5
  SHA512:
6
- metadata.gz: 84c9ed288329dbb5cc687335820feb293dd4aced564d9ebd8c65e0a84ab12368f8a2b774bef93af9fb9c845fb889eb8daadb9f8c60229e00cc8a78ce768ff3cd
7
- data.tar.gz: e2b639cc38df40ed02de870e97707a71a1305819ce3eeac3d61384cdda75c764a3793616149187bf2eed3a40443a3c30f2b19d32ca3a13cf1da2a5f010e01571
6
+ metadata.gz: bc594cb118caa67c0e9982f839c762e074b789b083328a1b69b3261297bd34055a53739f51cabb6f6e5685ea76f80eb4574b45649fd202bf213a579713842fac
7
+ data.tar.gz: 8d42ee7274c26e25086eae4e9c2646f958f930a18fd03eb9c6262f734cf55af380499763e3508a05156a6f1baad4c7bc30007e9d242674d9594bf83e75a7bf1f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.6](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.5...v3.8.6) (2023-10-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * replacing enum casts with the temporary enums when normalizing check clause and action conditions ([0cb67fc](https://github.com/craigulliott/dynamic_migrations/commit/0cb67fcff4d7833ec1dd60453526b1bc3dbe45ee))
9
+
10
+ ## [3.8.5](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.4...v3.8.5) (2023-10-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * missing error class ([761e478](https://github.com/craigulliott/dynamic_migrations/commit/761e478fbd9234014751d853da3dde35d2043a7e))
16
+ * normalized validation check constraint and trigger action condition now work with enum columns ([a657dd6](https://github.com/craigulliott/dynamic_migrations/commit/a657dd66f3bb3e140672db89224b0b562bb91ef0))
17
+
3
18
  ## [3.8.4](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.3...v3.8.4) (2023-10-06)
4
19
 
5
20
 
@@ -16,6 +16,9 @@ module DynamicMigrations
16
16
  class ValueAlreadyExistsError < StandardError
17
17
  end
18
18
 
19
+ class ValueMustBeStringError < StandardError
20
+ end
21
+
19
22
  attr_reader :schema
20
23
  attr_reader :name
21
24
  attr_reader :values
@@ -82,19 +82,6 @@ module DynamicMigrations
82
82
  def base_data_type
83
83
  array? ? @data_type[0..-3]&.to_sym : @data_type
84
84
  end
85
-
86
- # sometimes this system makes temporary tables in order to fetch the normalized
87
- # version of constraint check clauses, function definitions or trigger action conditions
88
- # because certain data types might not yet exist, we need to use alternative types
89
- def temp_table_data_type
90
- if enum
91
- :text
92
- elsif @data_type == :citext || @data_type == :"citext[]"
93
- :text
94
- else
95
- @data_type
96
- end
97
- end
98
85
  end
99
86
  end
100
87
  end
@@ -18,7 +18,9 @@ module DynamicMigrations
18
18
  # error if the column does not exist
19
19
  def column name
20
20
  raise ExpectedSymbolError, name unless name.is_a? Symbol
21
- raise ColumnDoesNotExistError, name unless has_column? name
21
+ unless has_column? name
22
+ raise ColumnDoesNotExistError, "column `#{name}` does not exist within table `#{self.name}`"
23
+ end
22
24
  @columns[name]
23
25
  end
24
26
 
@@ -40,7 +42,7 @@ module DynamicMigrations
40
42
  # adds a new column to this table, and returns it
41
43
  def add_column name, data_type, **column_options
42
44
  if has_column? name
43
- raise(DuplicateColumnError, "Column #{name} already exists")
45
+ raise DuplicateColumnError, "Column `#{name}` already exists"
44
46
  end
45
47
  included_target = self
46
48
  if included_target.is_a? Table
@@ -202,7 +202,7 @@ module DynamicMigrations
202
202
  end
203
203
 
204
204
  # create a temporary table in postgres to represent this trigger and fetch
205
- # the actual normalized check constraint directly from the database
205
+ # the actual normalized action_condition directly from the database
206
206
  def normalized_action_condition
207
207
  if action_condition.nil?
208
208
  nil
@@ -215,12 +215,8 @@ module DynamicMigrations
215
215
  # we don't want the function, temporary table or trigger to be persisted
216
216
  connection.exec("BEGIN")
217
217
 
218
- # create the temp table and add the expected columns and constraint
219
- connection.exec(<<~SQL)
220
- CREATE TEMP TABLE trigger_normalized_action_condition_temp_table (
221
- #{table.columns.map { |column| '"' + column.name.to_s + '" ' + column.temp_table_data_type.to_s }.join(", ")}
222
- );
223
- SQL
218
+ # create the temp table and add the expected columns
219
+ temp_enums = table.create_temp_table(connection, "trigger_normalized_action_condition_temp_table")
224
220
 
225
221
  # create a temporary function to trigger (triggers require a function)
226
222
  connection.exec(<<~SQL)
@@ -228,6 +224,13 @@ module DynamicMigrations
228
224
  $$ BEGIN END $$;
229
225
  SQL
230
226
 
227
+ temp_action_condition = action_condition
228
+ # string replace any real enum names with their temp enum names
229
+ temp_enums.each do |temp_enum_name, enum|
230
+ temp_action_condition.gsub!("::#{enum.name}", "::#{temp_enum_name}")
231
+ temp_action_condition.gsub!("::#{enum.full_name}", "::#{temp_enum_name}")
232
+ end
233
+
231
234
  # create a temporary trigger, from which we will fetch the normalized action condition
232
235
  connection.exec(<<~SQL)
233
236
  CREATE TRIGGER trigger_normalized_action_condition_temp_trigger
@@ -253,7 +256,15 @@ module DynamicMigrations
253
256
  connection.exec("ROLLBACK")
254
257
 
255
258
  # return the normalized action condition
256
- rows.first["action_condition"]
259
+ action_condition_result = rows.first["action_condition"]
260
+
261
+ # string replace any enum names with their real enum names
262
+ temp_enums.each do |temp_enum_name, enum|
263
+ real_enum_name = (enum.schema == table.schema) ? enum.name : enum.full_name
264
+ action_condition_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
265
+ end
266
+
267
+ action_condition_result
257
268
  end
258
269
 
259
270
  if ac.nil?
@@ -128,17 +128,22 @@ module DynamicMigrations
128
128
  if table.columns.empty?
129
129
  raise ExpectedTableColumnsError, "Can not normalize check clause or validation columnns because the table has no columns"
130
130
  end
131
- result = table.schema.database.with_connection do |connection|
132
- # wrapped in a transaction just in case something here fails, because
133
- # we don't want the temporary table to be persisted
131
+ table.schema.database.with_connection do |connection|
132
+ # wrapped in a transaction so we can rollback the creation of the table and any enums
134
133
  connection.exec("BEGIN")
135
134
 
136
- # create the temp table and add the expected columns and constraint
135
+ temp_enums = table.create_temp_table(connection, "validation_normalized_check_clause_temp_table")
136
+
137
+ temp_check_clause = check_clause
138
+ # string replace any real enum names with their temp enum names
139
+ temp_enums.each do |temp_enum_name, enum|
140
+ temp_check_clause.gsub!("::#{enum.name}", "::#{temp_enum_name}")
141
+ temp_check_clause.gsub!("::#{enum.full_name}", "::#{temp_enum_name}")
142
+ end
143
+
137
144
  connection.exec(<<~SQL)
138
- CREATE TEMP TABLE validation_normalized_check_clause_temp_table (
139
- #{table.columns.map { |column| '"' + column.name.to_s + '" ' + column.temp_table_data_type.to_s }.join(", ")},
140
- CONSTRAINT #{name} CHECK (#{check_clause})
141
- );
145
+ ALTER TABLE validation_normalized_check_clause_temp_table
146
+ ADD CONSTRAINT #{name} CHECK (#{temp_check_clause})
142
147
  SQL
143
148
 
144
149
  # get the normalized version of the constraint
@@ -158,29 +163,37 @@ module DynamicMigrations
158
163
  GROUP BY pg_constraint.oid;
159
164
  SQL
160
165
 
161
- # delete the temp table and close the transaction
166
+ # delete the table and any temporary enums
162
167
  connection.exec("ROLLBACK")
163
168
 
164
- rows.first
165
- end
169
+ check_clause_result = rows.first["check_clause"]
170
+ column_names_string = rows.first["column_names"]
166
171
 
167
- if result["check_clause"].nil?
168
- raise UnnormalizableCheckClauseError, "Failed to nomalize check clause `#{check_clause}`"
169
- end
172
+ if check_clause_result.nil?
173
+ raise UnnormalizableCheckClauseError, "Failed to nomalize check clause `#{check_clause_result}`"
174
+ end
170
175
 
171
- # extract the check clause from the result "CHECK(%check_clause%)"
172
- matches = result["check_clause"].match(/\ACHECK \((?<inner_clause>.*)\)\z/)
173
- if matches.nil?
174
- raise UnnormalizableCheckClauseError, "Unparsable normalized check_clause #{result["check_clause"]}"
175
- end
176
+ # extract the check clause from the result "CHECK(%check_clause%)"
177
+ matches = check_clause_result.match(/\ACHECK \((?<inner_clause>.*)\)\z/)
178
+ if matches.nil?
179
+ raise UnnormalizableCheckClauseError, "Unparsable normalized check_clause #{check_clause_result}"
180
+ end
181
+ check_clause_result = matches[:inner_clause]
176
182
 
177
- normalized_column_names = result["column_names"].gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
183
+ # string replace any enum names with their real enum names
184
+ temp_enums.each do |temp_enum_name, enum|
185
+ real_enum_name = (enum.schema == table.schema) ? enum.name : enum.full_name
186
+ check_clause_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
187
+ end
178
188
 
179
- # return the normalized check clause
180
- {
181
- check_clause: matches[:inner_clause],
182
- column_names: normalized_column_names
183
- }
189
+ column_names_result = column_names_string.gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
190
+
191
+ # return the normalized check clause
192
+ {
193
+ check_clause: check_clause_result,
194
+ column_names: column_names_result
195
+ }
196
+ end
184
197
  end
185
198
 
186
199
  # used internally to set the columns from this objects initialize method
@@ -78,6 +78,56 @@ module DynamicMigrations
78
78
  end
79
79
  pk
80
80
  end
81
+
82
+ # Used within validations and triggers when normalizing check clauses and other
83
+ # SQL statements which require a table to process the SQL.
84
+ #
85
+ # This method returns a hash representation of any temporary enums created to satisfy
86
+ # the columns in the table
87
+ def create_temp_table connection, temp_table_name
88
+ # create the temp table and add the expected columns
89
+
90
+ # if any of the columns are enums, then we need to create a temporary enum type for them.
91
+ # we cant just create temporary columns as text fields because postgres may automatically
92
+ # add casts to those columns, which would result in a different normalized check clause
93
+ temp_enums = {}
94
+
95
+ # an array of sql column definitions for within the create table SQL
96
+ # we process each column individually like this so that we can create temporary enums for
97
+ # any enum columns
98
+ columns_sql = columns.map do |column|
99
+ enum = column.enum
100
+ if enum
101
+ # create the temporary enum type
102
+ temp_enum_name = "#{temp_table_name}_enum_#{temp_enums.count}"
103
+ connection.exec(<<~SQL)
104
+ CREATE TYPE #{temp_enum_name} as ENUM ('#{enum.values.join("','")}');
105
+ SQL
106
+ temp_enums[temp_enum_name] = enum
107
+
108
+ # return the column definition used within the CREATE TABLE SQL
109
+ data_type = column.array? ? "#{temp_enum_name}[]" : temp_enum_name
110
+ "\"#{column.name}\" #{data_type}"
111
+
112
+ else
113
+ # return the column definition used within the CREATE TABLE SQL
114
+ "\"#{column.name}\" #{column.data_type}"
115
+ end
116
+ end
117
+
118
+ # in case any of the columnbs are citext columns
119
+ connection.exec("CREATE EXTENSION IF NOT EXISTS citext;")
120
+
121
+ # note, this is not actually a TEMP TABLE, it is created within a transaction
122
+ # and rolled back.
123
+ connection.exec(<<~SQL)
124
+ CREATE TABLE #{temp_table_name} (
125
+ #{columns_sql.join(", ")}
126
+ );
127
+ SQL
128
+
129
+ temp_enums
130
+ end
81
131
  end
82
132
  end
83
133
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicMigrations
4
- VERSION = "3.8.4"
4
+ VERSION = "3.8.6"
5
5
  end
@@ -19,7 +19,6 @@ module DynamicMigrations
19
19
  def has_description?: -> bool
20
20
  def array?: -> bool
21
21
  def enum?: -> bool
22
- def temp_table_data_type: -> Symbol
23
22
  # untyped because we cant specify this logic in rbs yet (compiler is concerned this might be nil)
24
23
  def base_data_type: -> untyped
25
24
 
@@ -15,6 +15,7 @@ module DynamicMigrations
15
15
 
16
16
  # these come from the table object (which this module is included into)
17
17
  def source: -> database_or_configuration
18
+ def name: -> String
18
19
 
19
20
  class ColumnDoesNotExistError < StandardError
20
21
  end
@@ -22,7 +22,7 @@ module DynamicMigrations
22
22
  def add_primary_key: (Symbol name, Array[Symbol] column_names, **untyped) -> untyped
23
23
  def has_primary_key?: -> bool
24
24
  def primary_key: -> PrimaryKey
25
-
25
+ def create_temp_table: (PG::Connection connection, String table_name) -> Hash[String, Enum]
26
26
  class ExpectedSchemaError < StandardError
27
27
  end
28
28
 
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.8.4
4
+ version: 3.8.6
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-10-06 00:00:00.000000000 Z
11
+ date: 2023-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg