dynamic_migrations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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