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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/lib/dynamic_migrations/expected_boolean_error.rb +4 -0
- data/lib/dynamic_migrations/expected_integer_error.rb +4 -0
- data/lib/dynamic_migrations/expected_string_error.rb +4 -0
- data/lib/dynamic_migrations/expected_symbol_error.rb +4 -0
- data/lib/dynamic_migrations/invalid_source_error.rb +7 -0
- data/lib/dynamic_migrations/module_included_into_unexpected_target_error.rb +4 -0
- data/lib/dynamic_migrations/postgres/connections.rb +42 -0
- data/lib/dynamic_migrations/postgres/data_types.rb +273 -0
- data/lib/dynamic_migrations/postgres/server/database/configured_schemas.rb +55 -0
- data/lib/dynamic_migrations/postgres/server/database/connection.rb +39 -0
- data/lib/dynamic_migrations/postgres/server/database/differences.rb +292 -0
- data/lib/dynamic_migrations/postgres/server/database/keys_and_unique_constraints_loader.rb +149 -0
- data/lib/dynamic_migrations/postgres/server/database/loaded_schemas.rb +55 -0
- data/lib/dynamic_migrations/postgres/server/database/loaded_schemas_builder.rb +86 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/column.rb +84 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/columns.rb +58 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rb +132 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraints.rb +62 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/index.rb +144 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/indexes.rb +63 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/primary_key.rb +83 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rb +101 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraints.rb +59 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/validation.rb +90 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table/validations.rb +59 -0
- data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +73 -0
- data/lib/dynamic_migrations/postgres/server/database/schema.rb +72 -0
- data/lib/dynamic_migrations/postgres/server/database/source.rb +37 -0
- data/lib/dynamic_migrations/postgres/server/database/structure_loader.rb +242 -0
- data/lib/dynamic_migrations/postgres/server/database/validations_loader.rb +81 -0
- data/lib/dynamic_migrations/postgres/server/database.rb +54 -0
- data/lib/dynamic_migrations/postgres/server.rb +33 -0
- data/lib/dynamic_migrations/postgres.rb +8 -0
- data/lib/dynamic_migrations/version.rb +5 -0
- data/lib/dynamic_migrations.rb +44 -0
- 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
|