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