dynamic_migrations 3.8.3 → 3.8.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08737568ccf7605e0a4d404063b7e1708e67c0ed96ab9f091bb72db627c0d50b'
4
- data.tar.gz: 404f7bd4816def6c379a9277ec9c06ad4f58d188d3911f4ca9e9c1a38cf301f8
3
+ metadata.gz: 0237e61916864ca25f8f7ba1c8882ee7cd8ba687eb2ead948f7dc602cc11e58d
4
+ data.tar.gz: 8e883bcf30a9b2bf549b7e52062e1cd0576aecaedbe24eb20314522b7e6a6ac8
5
5
  SHA512:
6
- metadata.gz: a403e1da441c1625e89034f07141d48c15fa07953404638795c2c11d58c79e26468f7b4b9070a37d18d6d62a91f069950b4d9ed824c6404071cee59fd3627d35
7
- data.tar.gz: 3df74c476b5d84f8b98f774a69223934823670201649bf35f3aa5a6620738c0fba046bb56ac2e8139364a39ac5a6c4cbdcea547b2cf9625bf45f570261038b80
6
+ metadata.gz: c026e79bd2af5e73e7cc994fa0a3a28ed480c99f79e151377ae1f01a4953696875d9b3b3df8e53e8e4b6d0e463cadb784ceeed49e0e37b6b6fe9929261f030c5
7
+ data.tar.gz: 4de30c0cdc9586519da3790fd073eaa4e039214fff6448a064b1fc603a694f3264885ffa3258f542f588e22523173eec55b26c9596753d03a72bbebc5145e7a6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.5](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.4...v3.8.5) (2023-10-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * missing error class ([761e478](https://github.com/craigulliott/dynamic_migrations/commit/761e478fbd9234014751d853da3dde35d2043a7e))
9
+ * normalized validation check constraint and trigger action condition now work with enum columns ([a657dd6](https://github.com/craigulliott/dynamic_migrations/commit/a657dd66f3bb3e140672db89224b0b562bb91ef0))
10
+
11
+ ## [3.8.4](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.3...v3.8.4) (2023-10-06)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * asserting that enum values must be unique strings, and adding ability to add additional enum values ([5cf6093](https://github.com/craigulliott/dynamic_migrations/commit/5cf6093eed96387042f70987c3999b40e4041b76))
17
+
3
18
  ## [3.8.3](https://github.com/craigulliott/dynamic_migrations/compare/v3.8.2...v3.8.3) (2023-10-06)
4
19
 
5
20
 
@@ -13,6 +13,12 @@ module DynamicMigrations
13
13
  class ExpectedValuesError < StandardError
14
14
  end
15
15
 
16
+ class ValueAlreadyExistsError < StandardError
17
+ end
18
+
19
+ class ValueMustBeStringError < StandardError
20
+ end
21
+
16
22
  attr_reader :schema
17
23
  attr_reader :name
18
24
  attr_reader :values
@@ -25,8 +31,6 @@ module DynamicMigrations
25
31
 
26
32
  @columns = []
27
33
 
28
- @values = []
29
-
30
34
  raise ExpectedSchemaError, schema unless schema.is_a? Schema
31
35
  @schema = schema
32
36
 
@@ -36,7 +40,10 @@ module DynamicMigrations
36
40
  unless values.is_a?(Array) && values.count > 0
37
41
  raise ExpectedValuesError, "Values are required for enums"
38
42
  end
39
- @values = values
43
+ @values = []
44
+ values.each do |value|
45
+ add_value value
46
+ end
40
47
 
41
48
  unless description.nil?
42
49
  raise ExpectedStringError, description unless description.is_a? String
@@ -45,6 +52,18 @@ module DynamicMigrations
45
52
  end
46
53
  end
47
54
 
55
+ def add_value value
56
+ unless value.is_a? String
57
+ raise ValueMustBeStringError, "Value `#{value}` must be a string"
58
+ end
59
+
60
+ if @values.include? value
61
+ raise ValueAlreadyExistsError, "Value `#{value}` already exists in enum `#{name}`"
62
+ end
63
+
64
+ @values << value
65
+ end
66
+
48
67
  # returns true if this enum has a description, otehrwise false
49
68
  def has_description?
50
69
  !@description.nil?
@@ -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)
@@ -253,7 +249,15 @@ module DynamicMigrations
253
249
  connection.exec("ROLLBACK")
254
250
 
255
251
  # return the normalized action condition
256
- rows.first["action_condition"]
252
+ action_condition_result = rows.first["action_condition"]
253
+
254
+ # string replace any enum names with their real enum names
255
+ temp_enums.each do |temp_enum_name, enum|
256
+ real_enum_name = (enum.schema == table.schema) ? enum.name : enum.full_name
257
+ action_condition_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
258
+ end
259
+
260
+ action_condition_result
257
261
  end
258
262
 
259
263
  if ac.nil?
@@ -128,17 +128,15 @@ 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
137
  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
- );
138
+ ALTER TABLE validation_normalized_check_clause_temp_table
139
+ ADD CONSTRAINT #{name} CHECK (#{check_clause})
142
140
  SQL
143
141
 
144
142
  # get the normalized version of the constraint
@@ -158,29 +156,37 @@ module DynamicMigrations
158
156
  GROUP BY pg_constraint.oid;
159
157
  SQL
160
158
 
161
- # delete the temp table and close the transaction
159
+ # delete the table and any temporary enums
162
160
  connection.exec("ROLLBACK")
163
161
 
164
- rows.first
165
- end
162
+ check_clause_result = rows.first["check_clause"]
163
+ column_names_string = rows.first["column_names"]
166
164
 
167
- if result["check_clause"].nil?
168
- raise UnnormalizableCheckClauseError, "Failed to nomalize check clause `#{check_clause}`"
169
- end
165
+ if check_clause_result.nil?
166
+ raise UnnormalizableCheckClauseError, "Failed to nomalize check clause `#{check_clause_result}`"
167
+ end
170
168
 
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
169
+ # extract the check clause from the result "CHECK(%check_clause%)"
170
+ matches = check_clause_result.match(/\ACHECK \((?<inner_clause>.*)\)\z/)
171
+ if matches.nil?
172
+ raise UnnormalizableCheckClauseError, "Unparsable normalized check_clause #{check_clause_result}"
173
+ end
174
+ check_clause_result = matches[:inner_clause]
175
+
176
+ # string replace any enum names with their real enum names
177
+ temp_enums.each do |temp_enum_name, enum|
178
+ real_enum_name = (enum.schema == table.schema) ? enum.name : enum.full_name
179
+ check_clause_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
180
+ end
176
181
 
177
- normalized_column_names = result["column_names"].gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
182
+ column_names_result = column_names_string.gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
178
183
 
179
- # return the normalized check clause
180
- {
181
- check_clause: matches[:inner_clause],
182
- column_names: normalized_column_names
183
- }
184
+ # return the normalized check clause
185
+ {
186
+ check_clause: check_clause_result,
187
+ column_names: column_names_result
188
+ }
189
+ end
184
190
  end
185
191
 
186
192
  # 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.3"
4
+ VERSION = "3.8.5"
5
5
  end
@@ -16,6 +16,7 @@ module DynamicMigrations
16
16
  def full_name: -> Symbol
17
17
  def has_description?: -> bool
18
18
  def add_column: (Schema::Table::Column column) -> void
19
+ def add_value: (String value) -> void
19
20
  def differences_descriptions: (Enum other_enum) -> Array[String]
20
21
 
21
22
  class ExpectedSchemaError < StandardError
@@ -23,6 +24,12 @@ module DynamicMigrations
23
24
 
24
25
  class ExpectedValuesError < StandardError
25
26
  end
27
+
28
+ class ValueAlreadyExistsError < StandardError
29
+ end
30
+
31
+ class ValueMustBeStringError < StandardError
32
+ end
26
33
  end
27
34
  end
28
35
  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.3
4
+ version: 3.8.5
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