dynamic_migrations 1.0.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +71 -0
  6. data/lib/dynamic_migrations/expected_boolean_error.rb +4 -0
  7. data/lib/dynamic_migrations/expected_integer_error.rb +4 -0
  8. data/lib/dynamic_migrations/expected_string_error.rb +4 -0
  9. data/lib/dynamic_migrations/expected_symbol_error.rb +4 -0
  10. data/lib/dynamic_migrations/invalid_source_error.rb +7 -0
  11. data/lib/dynamic_migrations/module_included_into_unexpected_target_error.rb +4 -0
  12. data/lib/dynamic_migrations/postgres/connections.rb +42 -0
  13. data/lib/dynamic_migrations/postgres/data_types.rb +273 -0
  14. data/lib/dynamic_migrations/postgres/server/database/configured_schemas.rb +55 -0
  15. data/lib/dynamic_migrations/postgres/server/database/connection.rb +39 -0
  16. data/lib/dynamic_migrations/postgres/server/database/differences.rb +292 -0
  17. data/lib/dynamic_migrations/postgres/server/database/keys_and_unique_constraints_loader.rb +149 -0
  18. data/lib/dynamic_migrations/postgres/server/database/loaded_schemas.rb +55 -0
  19. data/lib/dynamic_migrations/postgres/server/database/loaded_schemas_builder.rb +86 -0
  20. data/lib/dynamic_migrations/postgres/server/database/schema/table/column.rb +84 -0
  21. data/lib/dynamic_migrations/postgres/server/database/schema/table/columns.rb +58 -0
  22. data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rb +132 -0
  23. data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraints.rb +62 -0
  24. data/lib/dynamic_migrations/postgres/server/database/schema/table/index.rb +144 -0
  25. data/lib/dynamic_migrations/postgres/server/database/schema/table/indexes.rb +63 -0
  26. data/lib/dynamic_migrations/postgres/server/database/schema/table/primary_key.rb +83 -0
  27. data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rb +101 -0
  28. data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraints.rb +59 -0
  29. data/lib/dynamic_migrations/postgres/server/database/schema/table/validation.rb +90 -0
  30. data/lib/dynamic_migrations/postgres/server/database/schema/table/validations.rb +59 -0
  31. data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +73 -0
  32. data/lib/dynamic_migrations/postgres/server/database/schema.rb +72 -0
  33. data/lib/dynamic_migrations/postgres/server/database/source.rb +37 -0
  34. data/lib/dynamic_migrations/postgres/server/database/structure_loader.rb +242 -0
  35. data/lib/dynamic_migrations/postgres/server/database/validations_loader.rb +81 -0
  36. data/lib/dynamic_migrations/postgres/server/database.rb +54 -0
  37. data/lib/dynamic_migrations/postgres/server.rb +33 -0
  38. data/lib/dynamic_migrations/postgres.rb +8 -0
  39. data/lib/dynamic_migrations/version.rb +5 -0
  40. data/lib/dynamic_migrations.rb +44 -0
  41. metadata +113 -0
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Differences
8
+ class ExpectedDatabaseError < StandardError
9
+ end
10
+
11
+ class TableRequiredError < StandardError
12
+ end
13
+
14
+ class SchemaRequiredError < StandardError
15
+ end
16
+
17
+ def initialize database
18
+ raise ExpectedDatabaseError, database unless database.is_a? Database
19
+ @database = database
20
+ end
21
+
22
+ # return a hash representing any differenced betweek the loaded and configured
23
+ # versions of the current database
24
+ def to_h
25
+ {
26
+ configuration: self.class.compare_schemas(@database.configured_schemas_hash, @database.loaded_schemas_hash),
27
+ database: self.class.compare_schemas(@database.loaded_schemas_hash, @database.configured_schemas_hash)
28
+ }
29
+ end
30
+
31
+ def self.compare_schemas schemas, comparison_schemas
32
+ result = {}
33
+ # the base schemas
34
+ schemas.each do |schema_name, schema|
35
+ # compare this schema to the equivilent in the comparison list
36
+ # note that the comparison may be nil
37
+ result[schema_name] = compare_schema schema, comparison_schemas[schema_name]
38
+ end
39
+ # look for any in the comparison list which were not in the base list
40
+ comparison_schemas.each do |schema_name, schema|
41
+ unless result.key? schema_name
42
+ result[schema_name] = {
43
+ exists: false
44
+ }
45
+ end
46
+ end
47
+ result
48
+ end
49
+
50
+ # compare two schemas and return an object which represents the provided `schema` and
51
+ # any differences between it and the provided `comparison_schema`
52
+ def self.compare_schema schema, comparison_schema
53
+ raise SchemaRequiredError if schema.nil?
54
+
55
+ comparison_tables = comparison_schema.nil? ? {} : comparison_schema.tables_hash
56
+ {
57
+ exists: true,
58
+ tables: compare_tables(schema.tables_hash, comparison_tables)
59
+ }
60
+ end
61
+
62
+ # compare two hash representations of a set of tables and return
63
+ # an object which represents the provided `tables` and any differences
64
+ # between it and the `comparison_tables`
65
+ def self.compare_tables tables, comparison_tables
66
+ result = {}
67
+ # the base tables
68
+ tables.each do |table_name, table|
69
+ # compare this table to the equivilent in the comparison list
70
+ # note that the comparison may be nil
71
+ result[table_name] = compare_table(table, comparison_tables[table_name])
72
+ end
73
+ # look for any in the comparison list which were not in the base list
74
+ comparison_tables.each do |table_name, table|
75
+ unless result.key? table_name
76
+ result[table_name] = {
77
+ exists: false
78
+ }
79
+ end
80
+ end
81
+ result
82
+ end
83
+
84
+ # compare two tables and return an object which represents the provided `table` and
85
+ # any differences between it and the provided `comparison_table`
86
+ def self.compare_table table, comparison_table
87
+ raise TableRequiredError if table.nil?
88
+
89
+ primary_key = table.has_primary_key? ? table.primary_key : nil
90
+ if comparison_table
91
+ comparison_primary_key = comparison_table.has_primary_key? ? comparison_table.primary_key : nil
92
+ comparison_columns = comparison_table.columns_hash
93
+ comparison_validations = comparison_table.validations_hash
94
+ comparison_foreign_key_constraints = comparison_table.foreign_key_constraints_hash
95
+ comparison_unique_constraints = comparison_table.unique_constraints_hash
96
+ else
97
+ comparison_primary_key = {}
98
+ comparison_columns = {}
99
+ comparison_validations = {}
100
+ comparison_foreign_key_constraints = {}
101
+ comparison_unique_constraints = {}
102
+ end
103
+ {
104
+ exists: true,
105
+ description: {
106
+ value: table.description,
107
+ matches: (comparison_table && comparison_table.description == table.description) || false
108
+ },
109
+ primary_key: compare_record(primary_key, comparison_primary_key, [
110
+ :primary_key_name,
111
+ :index_type
112
+ ]),
113
+ columns: compare_columns(table.columns_hash, comparison_columns),
114
+ validations: compare_validations(table.validations_hash, comparison_validations),
115
+ foreign_key_constraints: compare_foreign_key_constraints(table.foreign_key_constraints_hash, comparison_foreign_key_constraints),
116
+ unique_constraints: compare_unique_constraints(table.unique_constraints_hash, comparison_unique_constraints)
117
+ }
118
+ end
119
+
120
+ # compare two hash representations of a set of columns and return
121
+ # an object which represents the provided `columns` and any differences
122
+ # between it and the `comparison_columns`
123
+ def self.compare_columns columns, comparison_columns
124
+ result = {}
125
+ # the base columns
126
+ columns.each do |column_name, column|
127
+ # compare this column to the equivilent in the comparison list
128
+ result[column_name] = compare_record column, comparison_columns[column_name], [
129
+ :data_type,
130
+ :null,
131
+ :default,
132
+ :description,
133
+ :character_maximum_length,
134
+ :character_octet_length,
135
+ :numeric_precision,
136
+ :numeric_precision_radix,
137
+ :numeric_scale,
138
+ :datetime_precision,
139
+ :interval_type,
140
+ :udt_schema,
141
+ :udt_name,
142
+ :updatable
143
+ ]
144
+ end
145
+ # look for any columns in the comparison list which were not in the base list
146
+ comparison_columns.each do |column_name, column|
147
+ unless result.key? column_name
148
+ result[column_name] = {
149
+ exists: false
150
+ }
151
+ end
152
+ end
153
+ result
154
+ end
155
+
156
+ # compare two hash representations of a set of unique_constraints and return
157
+ # an object which represents the provided `unique_constraints` and any differences
158
+ # between it and the `comparison_unique_constraints`
159
+ def self.compare_unique_constraints unique_constraints, comparison_unique_constraints
160
+ result = {}
161
+ # the base unique_constraints
162
+ unique_constraints.each do |unique_constraint_name, unique_constraint|
163
+ # compare this unique_constraint to the equivilent in the comparison list
164
+ result[unique_constraint_name] = compare_record unique_constraint, comparison_unique_constraints[unique_constraint_name], [
165
+ :column_names,
166
+ :index_type,
167
+ :deferrable,
168
+ :initially_deferred
169
+ ]
170
+ end
171
+ # look for any unique_constraints in the comparison list which were not in the base list
172
+ comparison_unique_constraints.each do |unique_constraint_name, unique_constraint|
173
+ unless result.key? unique_constraint_name
174
+ result[unique_constraint_name] = {
175
+ exists: false
176
+ }
177
+ end
178
+ end
179
+ result
180
+ end
181
+
182
+ # compare two hash representations of a set of indexes and return
183
+ # an object which represents the provided `indexes` and any differences
184
+ # between it and the `comparison_indexes`
185
+ def self.compare_indexes indexes, comparison_indexes
186
+ result = {}
187
+ # the base indexes
188
+ indexes.each do |index_name, index|
189
+ # compare this index to the equivilent in the comparison list
190
+ result[index_name] = compare_record index, comparison_indexes[index_name], [
191
+ :column_names,
192
+ :unique,
193
+ :where,
194
+ :type,
195
+ :deferrable,
196
+ :initially_deferred,
197
+ :order,
198
+ :nulls_position
199
+ ]
200
+ end
201
+ # look for any indexes in the comparison list which were not in the base list
202
+ comparison_indexes.each do |index_name, index|
203
+ unless result.key? index_name
204
+ result[index_name] = {
205
+ exists: false
206
+ }
207
+ end
208
+ end
209
+ result
210
+ end
211
+
212
+ # compare two hash representations of a set of validations and return
213
+ # an object which represents the provided `validations` and any differences
214
+ # between it and the `comparison_validations`
215
+ def self.compare_validations validations, comparison_validations
216
+ result = {}
217
+ # the base validations
218
+ validations.each do |validation_name, validation|
219
+ # compare this validation to the equivilent in the comparison list
220
+ result[validation_name] = compare_record validation, comparison_validations[validation_name], [
221
+ :check_clause,
222
+ :column_names,
223
+ :deferrable,
224
+ :initially_deferred
225
+ ]
226
+ end
227
+ # look for any validations in the comparison list which were not in the base list
228
+ comparison_validations.each do |validation_name, validation|
229
+ unless result.key? validation_name
230
+ result[validation_name] = {
231
+ exists: false
232
+ }
233
+ end
234
+ end
235
+ result
236
+ end
237
+
238
+ # compare two hash representations of a set of foreign key constraints and return
239
+ # an object which represents the provided `foreign_key_constraints` and any differences
240
+ # between it and the `comparison_foreign_key_constraints`
241
+ def self.compare_foreign_key_constraints foreign_key_constraints, comparison_foreign_key_constraints
242
+ result = {}
243
+ # the base foreign_key_constraints
244
+ foreign_key_constraints.each do |foreign_key_constraint_name, foreign_key_constraint|
245
+ # compare this foreign_key_constraint to the equivilent in the comparison list
246
+ result[foreign_key_constraint_name] = compare_record foreign_key_constraint, comparison_foreign_key_constraints[foreign_key_constraint_name], [
247
+ :column_names,
248
+ :foreign_schema_name,
249
+ :foreign_table_name,
250
+ :foreign_column_names,
251
+ :deferrable,
252
+ :initially_deferred
253
+ ]
254
+ end
255
+ # look for any foreign_key_constraints in the comparison list which were not in the base list
256
+ comparison_foreign_key_constraints.each do |foreign_key_constraint_name, foreign_key_constraint|
257
+ unless result.key? foreign_key_constraint_name
258
+ result[foreign_key_constraint_name] = {
259
+ exists: false
260
+ }
261
+ end
262
+ end
263
+ result
264
+ end
265
+
266
+ # Accepts an optional base and comparison objects, and a list of methods.
267
+ # Returns a hash representing the base record and all the data which it
268
+ # is returned for each mehod in the provided method list and any differences
269
+ # it and the comparison.
270
+ def self.compare_record base, comparison, method_list
271
+ if base.nil?
272
+ {
273
+ exists: false
274
+ }
275
+ else
276
+ result = {}
277
+ method_list.each do |method_name|
278
+ matches = (comparison && comparison.send(method_name) == base.send(method_name)) || false
279
+ result[method_name] = {
280
+ value: base.send(method_name),
281
+ matches: matches
282
+ }
283
+ end
284
+ result[:exists] = true
285
+ result
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ module KeysAndUniqueConstraintsLoader
8
+ def create_database_keys_and_unique_constraints_cache
9
+ connection.exec(<<~SQL)
10
+ CREATE MATERIALIZED VIEW public.dynamic_migrations_keys_and_unique_constraints_cache as
11
+ SELECT
12
+ c.conname AS constraint_name,
13
+ CASE c.contype
14
+ WHEN 'f'::"char" THEN 'FOREIGN_KEY'::text
15
+ WHEN 'p'::"char" THEN 'PRIMARY_KEY'::text
16
+ WHEN 'u'::"char" THEN 'UNIQUE'::text
17
+ END::information_schema.character_data AS constraint_type,
18
+ sch.nspname AS schema_name,
19
+ tbl.relname AS table_name,
20
+ ARRAY_AGG(col.attname ORDER BY u.attposition) AS column_names,
21
+ f_sch.nspname AS foreign_schema_name,
22
+ f_tbl.relname AS foreign_table_name,
23
+ -- null if is required to prevent indexes and unique constraints from being included
24
+ NULLIF(ARRAY_AGG(f_col.attname ORDER BY f_u.attposition), ARRAY[null]::name[]) AS foreign_column_names,
25
+ c.condeferrable as deferrable,
26
+ c.condeferred as initially_deferred,
27
+ am.amname as index_type,
28
+ 1 as table_version
29
+ FROM pg_constraint c
30
+ LEFT JOIN LATERAL UNNEST(c.conkey)
31
+ WITH ORDINALITY AS u(attnum, attposition)
32
+ ON TRUE
33
+ LEFT JOIN LATERAL UNNEST(c.confkey)
34
+ WITH ORDINALITY AS f_u(attnum, attposition)
35
+ ON f_u.attposition = u.attposition
36
+ JOIN pg_class tbl
37
+ ON
38
+ tbl.oid = c.conrelid
39
+ AND left(tbl.relname, 3) != 'pg_'
40
+ JOIN pg_namespace sch
41
+ ON
42
+ sch.oid = tbl.relnamespace
43
+ AND sch.nspname != 'information_schema'
44
+ AND sch.nspname != 'postgis'
45
+ AND left(sch.nspname, 3) != 'pg_'
46
+ LEFT JOIN pg_attribute col
47
+ ON
48
+ (col.attrelid = tbl.oid
49
+ AND col.attnum = u.attnum)
50
+ LEFT JOIN pg_class f_tbl
51
+ ON
52
+ f_tbl.oid = c.confrelid
53
+ AND left(f_tbl.relname, 3) != 'pg_'
54
+ LEFT JOIN pg_namespace f_sch
55
+ ON
56
+ f_sch.oid = f_tbl.relnamespace
57
+ AND f_sch.nspname != 'information_schema'
58
+ AND f_sch.nspname != 'postgis'
59
+ AND left(f_sch.nspname, 3) != 'pg_'
60
+ LEFT JOIN pg_attribute f_col
61
+ ON
62
+ f_col.attrelid = f_tbl.oid
63
+ AND f_col.attnum = f_u.attnum
64
+
65
+ -- joins below to get the index type
66
+ LEFT JOIN pg_class index_cls ON index_cls.relname = c.conname AND index_cls.relnamespace = sch.oid
67
+ LEFT JOIN pg_index on index_cls.oid = pg_index.indexrelid AND tbl.oid = pg_index.indrelid
68
+ LEFT JOIN pg_am am ON am.oid=index_cls.relam
69
+
70
+ WHERE
71
+ -- only FOREIGN_KEY, UNIQUE or PRIMARY_KEY
72
+ c.contype in ('f', 'u', 'p')
73
+
74
+ GROUP BY constraint_name, constraint_type, condeferrable, condeferred, schema_name, table_name, foreign_schema_name, foreign_table_name, am.amname
75
+ ORDER BY schema_name, table_name;
76
+ SQL
77
+ connection.exec(<<~SQL)
78
+ CREATE UNIQUE INDEX dynamic_migrations_keys_and_unique_constraints_cache_index ON public.dynamic_migrations_keys_and_unique_constraints_cache (schema_name, table_name, constraint_name);
79
+ SQL
80
+ connection.exec(<<~SQL)
81
+ COMMENT ON MATERIALIZED VIEW public.dynamic_migrations_keys_and_unique_constraints_cache IS 'A cached representation of the database constraints. This is used by the dynamic migrations library and is created automatically and updated automatically after migrations have run.';
82
+ SQL
83
+ end
84
+
85
+ # fetch all required data from the database and build and return a
86
+ # useful hash representing the keys and indexes of your database
87
+ def fetch_keys_and_unique_constraints
88
+ begin
89
+ rows = connection.exec_params(<<~SQL)
90
+ SELECT * FROM public.dynamic_migrations_keys_and_unique_constraints_cache
91
+ SQL
92
+ rescue PG::UndefinedTable
93
+ create_database_keys_and_unique_constraints_cache
94
+ rows = connection.exec_params(<<~SQL)
95
+ SELECT * FROM public.dynamic_migrations_keys_and_unique_constraints_cache
96
+ SQL
97
+ end
98
+
99
+ schemas = {}
100
+ rows.each do |row|
101
+ schema_name = row["schema_name"].to_sym
102
+ schema = schemas[schema_name] ||= {}
103
+
104
+ table_name = row["table_name"].to_sym
105
+ table = schema[table_name] ||= {}
106
+
107
+ constraint_type = row["constraint_type"].to_sym
108
+ constraints = table[constraint_type] ||= {}
109
+
110
+ constraint_name = row["constraint_name"].to_sym
111
+
112
+ column_names = row["column_names"].gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
113
+
114
+ if constraint_type == :FOREIGN_KEY
115
+ foreign_schema_name = row["foreign_schema_name"].to_sym
116
+ foreign_table_name = row["foreign_table_name"].to_sym
117
+ foreign_column_names = row["foreign_column_names"].gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
118
+ else
119
+ foreign_schema_name = nil
120
+ foreign_table_name = nil
121
+ foreign_column_names = nil
122
+ end
123
+
124
+ deferrable = row["deferrable"] == "TRUE"
125
+ initially_deferred = row["initially_deferred"] == "TRUE"
126
+
127
+ index_type = if row["index_type"].nil?
128
+ nil
129
+ else
130
+ row["index_type"].to_sym
131
+ end
132
+
133
+ constraints[constraint_name] = {
134
+ column_names: column_names,
135
+ foreign_schema_name: foreign_schema_name,
136
+ foreign_table_name: foreign_table_name,
137
+ foreign_column_names: foreign_column_names,
138
+ deferrable: deferrable,
139
+ initially_deferred: initially_deferred,
140
+ index_type: index_type
141
+ }
142
+ end
143
+ schemas
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ module LoadedSchemas
8
+ class LoadedSchemaAlreadyExistsError < StandardError
9
+ end
10
+
11
+ class LoadedSchemaDoesNotExistError < StandardError
12
+ end
13
+
14
+ # adds a new loaded schema for this database
15
+ def add_loaded_schema schema_name
16
+ raise ExpectedSymbolError, schema_name unless schema_name.is_a? Symbol
17
+ if has_loaded_schema? schema_name
18
+ raise(LoadedSchemaAlreadyExistsError, "Loaded schema #{schema_name} already exists")
19
+ end
20
+ included_target = self
21
+ if included_target.is_a? Database
22
+ @loaded_schemas[schema_name] = Schema.new :database, included_target, schema_name
23
+ else
24
+ raise ModuleIncludedIntoUnexpectedTargetError, included_target
25
+ end
26
+ end
27
+
28
+ # returns the loaded schema object for the provided schema name, and raises an
29
+ # error if the schema does not exist
30
+ def loaded_schema schema_name
31
+ raise ExpectedSymbolError, schema_name unless schema_name.is_a? Symbol
32
+ raise LoadedSchemaDoesNotExistError unless has_loaded_schema? schema_name
33
+ @loaded_schemas[schema_name]
34
+ end
35
+
36
+ # returns true if this table has a loaded schema with the provided name, otherwise false
37
+ def has_loaded_schema? schema_name
38
+ raise ExpectedSymbolError, schema_name unless schema_name.is_a? Symbol
39
+ @loaded_schemas.key? schema_name
40
+ end
41
+
42
+ # returns an array of this tables loaded schemas
43
+ def loaded_schemas
44
+ @loaded_schemas.values
45
+ end
46
+
47
+ # returns a hash of this tables loaded schemas, keyed by schema name
48
+ def loaded_schemas_hash
49
+ @loaded_schemas
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ module LoadedSchemasBuilder
8
+ class UnexpectedConstrintTypeError < StandardError
9
+ end
10
+
11
+ # recursively process the database and build all the schemas,
12
+ # tables and columns
13
+ def recursively_build_schemas_from_database
14
+ validations = fetch_validations
15
+ fetch_structure.each do |schema_name, schema_definition|
16
+ schema = add_loaded_schema schema_name
17
+ schema_validations = validations[schema_name]
18
+
19
+ schema_definition[:tables].each do |table_name, table_definition|
20
+ table = schema.add_table table_name, table_definition[:description]
21
+ table_validations = schema_validations && schema_validations[table_name]
22
+
23
+ # add each table column
24
+ table_definition[:columns].each do |column_name, column_definition|
25
+ # we only need these for arrays and user-defined types
26
+ # (user-defined is usually ENUMS)
27
+ if [:ARRAY, :"USER-DEFINED"].include? column_definition[:data_type]
28
+ udt_schema = column_definition[:udt_schema]
29
+ udt_name = column_definition[:udt_name]
30
+ else
31
+ udt_schema = nil
32
+ udt_name = nil
33
+ end
34
+
35
+ table.add_column column_name, column_definition[:data_type],
36
+ null: column_definition[:null],
37
+ default: column_definition[:default],
38
+ description: column_definition[:description],
39
+ character_maximum_length: column_definition[:character_maximum_length],
40
+ character_octet_length: column_definition[:character_octet_length],
41
+ numeric_precision: column_definition[:numeric_precision],
42
+ numeric_precision_radix: column_definition[:numeric_precision_radix],
43
+ numeric_scale: column_definition[:numeric_scale],
44
+ datetime_precision: column_definition[:datetime_precision],
45
+ udt_schema: udt_schema,
46
+ udt_name: udt_name
47
+ end
48
+
49
+ # add any validations
50
+ table_validations&.each do |validation_name, validation_definition|
51
+ table.add_validation validation_name, validation_definition[:columns], validation_definition[:check_clause]
52
+ end
53
+ end
54
+ end
55
+
56
+ # now that the structure has been loaded, we can add keys (foreign
57
+ # keys need to be added last, because they can depend on tables from
58
+ # different schemas)
59
+ fetch_keys_and_unique_constraints.each do |schema_name, schema_definition|
60
+ schema_definition.each do |table_name, keys_and_unique_constraints|
61
+ table = loaded_schema(schema_name).table(table_name)
62
+ keys_and_unique_constraints.each do |constraint_type, constraint_definitions|
63
+ constraint_definitions.each do |constraint_name, constraint_definition|
64
+ case constraint_type
65
+ when :PRIMARY_KEY
66
+ table.add_primary_key constraint_name, constraint_definition[:column_names], index_type: constraint_definition[:index_type]
67
+
68
+ when :FOREIGN_KEY
69
+ table.add_foreign_key_constraint constraint_name, constraint_definition[:column_names], constraint_definition[:foreign_schema_name], constraint_definition[:foreign_table_name], constraint_definition[:foreign_column_names], deferrable: constraint_definition[:deferrable], initially_deferred: constraint_definition[:initially_deferred]
70
+
71
+ when :UNIQUE
72
+ table.add_unique_constraint constraint_name, constraint_definition[:column_names], deferrable: constraint_definition[:deferrable], initially_deferred: constraint_definition[:initially_deferred], index_type: constraint_definition[:index_type]
73
+
74
+ else
75
+ raise UnexpectedConstrintTypeError, constraint_type
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Schema
8
+ class Table
9
+ # This class represents a single column within a postgres table
10
+ class Column < Source
11
+ class ExpectedTableError < StandardError
12
+ end
13
+
14
+ attr_reader :table
15
+ attr_reader :column_name
16
+ attr_reader :description
17
+ attr_reader :null
18
+ attr_reader :default
19
+ attr_reader :data_type
20
+ attr_reader :character_maximum_length
21
+ attr_reader :character_octet_length
22
+ attr_reader :numeric_precision
23
+ attr_reader :numeric_precision_radix
24
+ attr_reader :numeric_scale
25
+ attr_reader :datetime_precision
26
+ attr_reader :interval_type
27
+ attr_reader :udt_schema
28
+ attr_reader :udt_name
29
+ attr_reader :updatable
30
+
31
+ # initialize a new object to represent a column in a postgres table
32
+ def initialize source, table, column_name, data_type, null: true, default: nil, description: nil, character_maximum_length: nil, character_octet_length: nil, numeric_precision: nil, numeric_precision_radix: nil, numeric_scale: nil, datetime_precision: nil, interval_type: nil, udt_schema: nil, udt_name: nil, updatable: true
33
+ super source
34
+ raise ExpectedTableError, table unless table.is_a? Table
35
+ @table = table
36
+
37
+ raise ExpectedSymbolError, column_name unless column_name.is_a? Symbol
38
+ @column_name = column_name
39
+
40
+ @data_type = data_type
41
+
42
+ @null = null
43
+
44
+ @default = default
45
+
46
+ unless description.nil?
47
+ raise ExpectedStringError, description unless description.is_a? String
48
+ @description = description
49
+ end
50
+
51
+ DataTypes.validate_column_properties!(data_type,
52
+ character_maximum_length: character_maximum_length,
53
+ character_octet_length: character_octet_length,
54
+ numeric_precision: numeric_precision,
55
+ numeric_precision_radix: numeric_precision_radix,
56
+ numeric_scale: numeric_scale,
57
+ datetime_precision: datetime_precision,
58
+ interval_type: interval_type,
59
+ udt_schema: udt_schema,
60
+ udt_name: udt_name)
61
+
62
+ @character_maximum_length = character_maximum_length
63
+ @character_octet_length = character_octet_length
64
+ @numeric_precision = numeric_precision
65
+ @numeric_precision_radix = numeric_precision_radix
66
+ @numeric_scale = numeric_scale
67
+ @datetime_precision = datetime_precision
68
+ @interval_type = interval_type
69
+ @udt_schema = udt_schema
70
+ @udt_name = udt_name
71
+ @updatable = updatable
72
+ end
73
+
74
+ # return true if this column has a description, otherwise false
75
+ def has_description?
76
+ !@description.nil?
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end