nando 1.0.6

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.
@@ -0,0 +1,805 @@
1
+ module NandoSchemaDiff
2
+
3
+ SCHEMA_PLACEHOLDER = '___SCHEMANAME___'
4
+ TABLE_TYPE = {
5
+ 'r' => :tables,
6
+ 'v' => :views
7
+ }
8
+
9
+ def self.diff_schemas (source_schema, target_schema)
10
+
11
+ @schema_variable = NandoMigrator.instance.schema_variable
12
+
13
+ source_info = get_info_base_structure()
14
+ target_info = get_info_base_structure()
15
+
16
+ source = get_schema_structure(source_schema)
17
+ target = get_schema_structure(target_schema)
18
+
19
+ # start comparing structure
20
+
21
+ # checking for different tables
22
+ check_different_tables(source[:tables], target[:tables], source_info, target_info)
23
+ check_different_tables(target[:tables], source[:tables], target_info, source_info)
24
+
25
+ # checking for different views
26
+ check_different_views(source[:views], target[:views], source_info, target_info)
27
+ check_different_views(target[:views], source[:views], target_info, source_info)
28
+
29
+
30
+ # checking for different columns in all shared tables
31
+ check_different_columns(source[:tables], target[:tables], source_info, target_info)
32
+ check_different_columns(target[:tables], source[:tables], target_info, source_info)
33
+
34
+ # checking for mismatching columns in all shared tables
35
+ check_mismatching_columns(source[:tables], target[:tables], source_info, target_info)
36
+ check_mismatching_columns(target[:tables], source[:tables], target_info, source_info)
37
+
38
+
39
+ # checking for different triggers in all shared tables
40
+ check_different_triggers(source[:tables], target[:tables], source_info, target_info)
41
+ check_different_triggers(target[:tables], source[:tables], target_info, source_info)
42
+
43
+ # checking for mismatching triggers in all shared tables
44
+ check_mismatching_triggers(source[:tables], target[:tables], source_info, target_info)
45
+ check_mismatching_triggers(target[:tables], source[:tables], target_info, source_info)
46
+
47
+
48
+ # checking for different constraints in all shared tables
49
+ check_different_constraints(source[:tables], target[:tables], source_info, target_info)
50
+ check_different_constraints(target[:tables], source[:tables], target_info, source_info)
51
+
52
+ # checking for mismatching constraints in all shared tables
53
+ check_mismatching_constraints(source[:tables], target[:tables], source_info, target_info)
54
+ check_mismatching_constraints(target[:tables], source[:tables], target_info, source_info)
55
+
56
+
57
+ # checking for different indexes in all shared tables
58
+ check_different_indexes(source[:tables], target[:tables], source_info, target_info)
59
+ check_different_indexes(target[:tables], source[:tables], target_info, source_info)
60
+
61
+ # checking for mismatching indexes in all shared tables
62
+ check_mismatching_indexes(source[:tables], target[:tables], source_info, target_info)
63
+ check_mismatching_indexes(target[:tables], source[:tables], target_info, source_info)
64
+
65
+
66
+ source_suggestions = print_diff_info(source_info, @schema_variable, source_schema, target_schema)
67
+ target_suggestions = print_diff_info(target_info, @schema_variable, target_schema, source_schema)
68
+
69
+ # TODO: might skip this if there is no diff
70
+
71
+ wants_suggestions = NandoInterface.get_user_input_boolean("Do want to see the suggestions for changing the schema?")
72
+ if !wants_suggestions
73
+ return
74
+ end
75
+
76
+ # suggestions
77
+ puts "\n\n===========================//===========================\n".magenta.bold
78
+ puts "\nSuggestion for ".magenta.bold + "'up'".white.bold + ":".magenta.bold
79
+ print_schema_correction_suggestions(@schema_variable, source_suggestions)
80
+
81
+ puts "\nSuggestion for ".magenta.bold + "'down'".white.bold + ":".magenta.bold
82
+ print_schema_correction_suggestions(@schema_variable, target_suggestions)
83
+ puts ""
84
+ end
85
+
86
+ def self.get_schema_structure (curr_schema)
87
+ schema_structure = {
88
+ :tables => {},
89
+ :views => {}
90
+ }
91
+ db_connection = NandoMigrator.instance.get_database_connection()
92
+
93
+ # get all tables/views in the schema
94
+ results = db_connection.exec("
95
+ SELECT n.nspname AS table_schema,
96
+ t.relname AS table_name,
97
+ t.relkind AS table_type
98
+ FROM pg_class t
99
+ JOIN pg_namespace n ON n.oid = t.relnamespace
100
+ WHERE t.relkind IN ('r', 'v')
101
+ AND n.nspname = '#{curr_schema}'
102
+ ")
103
+
104
+ for row in results do
105
+ schema_structure[TABLE_TYPE[row['table_type']]][row['table_name']] = {
106
+ :columns => {},
107
+ :triggers => {},
108
+ :constraints => {},
109
+ :indexes => {}
110
+ }
111
+ end
112
+
113
+ # get all columns for each table/view
114
+ results = db_connection.exec("
115
+ SELECT n.nspname AS table_schema,
116
+ t.relname AS table_name,
117
+ t.relkind AS table_type,
118
+ a.attname AS column_name,
119
+ a.atthasdef AS column_has_default,
120
+ a.attnotnull AS column_not_null,
121
+ ROW_NUMBER () OVER (PARTITION BY t.oid ORDER BY a.attnum) AS column_num,
122
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_datatype,
123
+ (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
124
+ FROM pg_catalog.pg_attrdef d
125
+ WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) AS column_default
126
+ FROM pg_catalog.pg_attribute a
127
+ JOIN pg_catalog.pg_class t ON a.attrelid = t.oid
128
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
129
+ WHERE a.attnum > 0
130
+ AND NOT a.attisdropped
131
+ AND t.relkind IN ('r', 'v')
132
+ AND n.nspname = '#{curr_schema}'
133
+ ORDER BY column_num
134
+ ")
135
+
136
+ for row in results do
137
+ schema_structure[TABLE_TYPE[row['table_type']]][row['table_name']][:columns][row['column_name']] = {
138
+ :column_num => row['column_num'], # column_num does not use a.attnum, since that field keeps incrementing after dropping/adding columns
139
+ :column_has_default => row['column_has_default'],
140
+ :column_default => row['column_default'].nil? ? row['column_default'] : row['column_default'].gsub(curr_schema, SCHEMA_PLACEHOLDER), # remove the schema, since sequences include it in their name
141
+ :column_not_null => row['column_not_null'],
142
+ :column_datatype => row['column_datatype']
143
+ }
144
+ end
145
+
146
+ # get all triggers for each table
147
+ results = db_connection.exec("
148
+ SELECT n.nspname AS table_schema,
149
+ t.relname AS table_name,
150
+ t.relkind AS table_type,
151
+ tr.tgname AS trigger_name,
152
+ pg_catalog.pg_get_triggerdef(tr.oid, true) AS trigger_definition
153
+ FROM pg_catalog.pg_trigger tr
154
+ JOIN pg_catalog.pg_class t ON tr.tgrelid = t.oid
155
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
156
+ WHERE t.relkind IN ('r', 'v')
157
+ AND (NOT tr.tgisinternal OR (tr.tgisinternal AND tr.tgenabled = 'D'))
158
+ AND n.nspname = '#{curr_schema}'
159
+ ")
160
+
161
+ for row in results do
162
+ schema_structure[TABLE_TYPE[row['table_type']]][row['table_name']][:triggers][row['trigger_name']] = {
163
+ :trigger_definition => row['trigger_definition'].gsub(curr_schema, SCHEMA_PLACEHOLDER) # replace the schema with a value to later replace, to create the trigger definition on the new schema
164
+ }
165
+ end
166
+
167
+ # get all constraints for each table
168
+ # TODO: this will get face some issues with VFK to public_companies (that logic is specific to CW, but might make an exception)
169
+ results = db_connection.exec("
170
+ SELECT n.nspname AS table_schema,
171
+ t.relname AS table_name,
172
+ t.relkind AS table_type,
173
+ con.conname AS constraint_name,
174
+ con.consrc AS constraint_source,
175
+ pg_get_constraintdef(con.oid, true) AS constraint_definition
176
+ FROM pg_catalog.pg_constraint con
177
+ JOIN pg_catalog.pg_class t ON con.conrelid = t.oid
178
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
179
+ WHERE t.relkind IN ('r', 'v')
180
+ AND n.nspname = '#{curr_schema}'
181
+ ")
182
+
183
+ for row in results do
184
+ schema_structure[TABLE_TYPE[row['table_type']]][row['table_name']][:constraints][row['constraint_name']] = {
185
+ :constraint_source => row['constraint_source'],
186
+ :constraint_definition => row['constraint_definition']
187
+ }
188
+ end
189
+
190
+ # get all indexes for each table
191
+ results = db_connection.exec("
192
+ SELECT n.nspname AS table_schema,
193
+ t.relname AS table_name,
194
+ t.relkind AS table_type,
195
+ i.relname AS index_name,
196
+ pg_catalog.pg_get_indexdef(ix.indexrelid, 0, true) AS index_definition,
197
+ array_to_string(array_agg(a.attname), ', ') AS index_columns
198
+ FROM pg_catalog.pg_index ix
199
+ JOIN pg_catalog.pg_class i ON ix.indexrelid = i.oid
200
+ JOIN pg_catalog.pg_class t ON ix.indrelid = t.oid
201
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid
202
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
203
+ WHERE t.relkind IN ('r', 'v')
204
+ AND a.attnum = ANY(ix.indkey)
205
+ AND n.nspname = '#{curr_schema}'
206
+ GROUP BY 1, 2, 3, 4, 5
207
+ ")
208
+
209
+ for row in results do
210
+ schema_structure[TABLE_TYPE[row['table_type']]][row['table_name']][:indexes][row['index_name']] = {
211
+ :index_definition => row['index_definition'].gsub(curr_schema, SCHEMA_PLACEHOLDER), # replace the schema with a value to later replace, to create the trigger definition on the new schema
212
+ :index_columns => row['index_columns']
213
+ }
214
+ end
215
+
216
+ return schema_structure
217
+ end
218
+
219
+ def self.get_info_base_structure
220
+ return {
221
+ :tables => {
222
+ :missing => {},
223
+ :extra => [],
224
+ :mismatching => {}
225
+ },
226
+ :views => {
227
+ :missing => [],
228
+ :extra => [],
229
+ :mismatching => {}
230
+ }
231
+ }
232
+ end
233
+
234
+ def self.setup_table_info (info, table_name)
235
+ # create table structure if one does not exist
236
+ if info[:tables][:mismatching][table_name].nil?
237
+ info[:tables][:mismatching][table_name] = {
238
+ :columns => {
239
+ :missing => {},
240
+ :extra => [],
241
+ :mismatching => {}
242
+ },
243
+ :indexes => {
244
+ :missing => {},
245
+ :extra => [],
246
+ :mismatching => {}
247
+ },
248
+ :triggers => {
249
+ :missing => {},
250
+ :extra => [],
251
+ :mismatching => {}
252
+ },
253
+ :constraints => {
254
+ :missing => {},
255
+ :extra => [],
256
+ :mismatching => {}
257
+ }
258
+ }
259
+ end
260
+ end
261
+
262
+
263
+ # table comparison
264
+ def self.check_different_tables (left_schema, right_schema, left_info, right_info)
265
+ if !(keys_diff = left_schema.keys - right_schema.keys).empty?
266
+ left_info[:tables][:extra] += keys_diff
267
+ end
268
+
269
+ if !(keys_diff = right_schema.keys - left_schema.keys).empty?
270
+ keys_diff.each do |table_key|
271
+ left_info[:tables][:missing][table_key] = right_schema[table_key]
272
+ end
273
+ end
274
+ end
275
+
276
+ # views comparison
277
+ def self.check_different_views (left_schema, right_schema, left_info, right_info)
278
+ if !(keys_diff = left_schema.keys - right_schema.keys).empty?
279
+ left_info[:views][:extra] += keys_diff
280
+ end
281
+
282
+ if !(keys_diff = right_schema.keys - left_schema.keys).empty?
283
+ left_info[:views][:missing] += keys_diff
284
+ end
285
+ end
286
+
287
+
288
+ # column comparison
289
+ def self.check_different_columns (left_schema, right_schema, left_info, right_info)
290
+ left_schema.each do |table_key, table_value|
291
+ # ignore tables that only appear in one of the schemas
292
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
293
+ _debug "Skipping table (1): #{table_key}"
294
+ next
295
+ end
296
+
297
+ if !(keys_diff = left_schema[table_key][:columns].keys - right_schema[table_key][:columns].keys).empty?
298
+ setup_table_info(left_info, table_key)
299
+ left_info[:tables][:mismatching][table_key][:columns][:extra] += keys_diff
300
+ end
301
+
302
+ if !(keys_diff = right_schema[table_key][:columns].keys - left_schema[table_key][:columns].keys).empty?
303
+ setup_table_info(left_info, table_key)
304
+ keys_diff.each do |column_key|
305
+ left_info[:tables][:mismatching][table_key][:columns][:missing][column_key] = right_schema[table_key][:columns][column_key]
306
+ end
307
+ end
308
+ end
309
+ end
310
+
311
+ def self.check_mismatching_columns (left_schema, right_schema, left_info, right_info)
312
+ left_schema.each do |table_key, table_value|
313
+ # ignore tables that only appear in one of the schemas
314
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
315
+ _debug "Skipping table (2): #{table_key}"
316
+ next
317
+ end
318
+
319
+ table_value[:columns].each do |column_key, column_value|
320
+ # ignore columns that only appear in one of the tables
321
+ if (!left_info[:tables][:mismatching][table_key].nil? && !right_info[:tables][:mismatching][table_key].nil?) && (left_info[:tables][:mismatching][table_key][:columns][:missing].include?(column_key) || right_info[:tables][:mismatching][table_key][:columns][:missing].include?(column_key))
322
+ _debug "Skipping column: #{column_key}"
323
+ next
324
+ end
325
+
326
+ if left_schema[table_key][:columns][column_key] != right_schema[table_key][:columns][column_key]
327
+ setup_table_info(left_info, table_key)
328
+ left_info[:tables][:mismatching][table_key][:columns][:mismatching][column_key] = merge_left_right_hashes(left_schema[table_key][:columns][column_key], right_schema[table_key][:columns][column_key])
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+
335
+ # trigger comparison
336
+ def self.check_different_triggers (left_schema, right_schema, left_info, right_info)
337
+ left_schema.each do |table_key, table_value|
338
+ # ignore tables that only appear in one of the schemas
339
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
340
+ _debug "Skipping table (3): #{table_key}"
341
+ next
342
+ end
343
+
344
+ if !(keys_diff = left_schema[table_key][:triggers].keys - right_schema[table_key][:triggers].keys).empty?
345
+ setup_table_info(left_info, table_key)
346
+ left_info[:tables][:mismatching][table_key][:triggers][:extra] += keys_diff
347
+ end
348
+
349
+ if !(keys_diff = right_schema[table_key][:triggers].keys - left_schema[table_key][:triggers].keys).empty?
350
+ setup_table_info(left_info, table_key)
351
+ keys_diff.each do |trigger_key|
352
+ left_info[:tables][:mismatching][table_key][:triggers][:missing][trigger_key] = right_schema[table_key][:triggers][trigger_key]
353
+ end
354
+ end
355
+ end
356
+ end
357
+
358
+ def self.check_mismatching_triggers (left_schema, right_schema, left_info, right_info)
359
+ left_schema.each do |table_key, table_value|
360
+ # ignore tables that only appear in one of the schemas
361
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
362
+ _debug "Skipping table (4): #{table_key}"
363
+ next
364
+ end
365
+
366
+ table_value[:triggers].each do |trigger_key, trigger_value|
367
+ # ignore triggers that only appear in one of the tables
368
+ if (!left_info[:tables][:mismatching][table_key].nil? && !right_info[:tables][:mismatching][table_key].nil?) && (left_info[:tables][:mismatching][table_key][:triggers][:missing].include?(trigger_key) || right_info[:tables][:mismatching][table_key][:triggers][:missing].include?(trigger_key))
369
+ _debug "Skipping trigger: #{trigger_key}"
370
+ next
371
+ end
372
+
373
+ if left_schema[table_key][:triggers][trigger_key] != right_schema[table_key][:triggers][trigger_key]
374
+ setup_table_info(left_info, table_key)
375
+ left_info[:tables][:mismatching][table_key][:triggers][:mismatching][trigger_key] = merge_left_right_hashes(left_schema[table_key][:triggers][trigger_key], right_schema[table_key][:triggers][trigger_key])
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+
382
+ # constraint comparison
383
+ def self.check_different_constraints (left_schema, right_schema, left_info, right_info)
384
+ left_schema.each do |table_key, table_value|
385
+ # ignore tables that only appear in one of the schemas
386
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
387
+ _debug "Skipping table (5): #{table_key}"
388
+ next
389
+ end
390
+
391
+ if !(keys_diff = left_schema[table_key][:constraints].keys - right_schema[table_key][:constraints].keys).empty?
392
+ setup_table_info(left_info, table_key)
393
+ left_info[:tables][:mismatching][table_key][:constraints][:extra] += keys_diff
394
+ end
395
+
396
+ if !(keys_diff = right_schema[table_key][:constraints].keys - left_schema[table_key][:constraints].keys).empty?
397
+ setup_table_info(left_info, table_key)
398
+ keys_diff.each do |constraint_key|
399
+ left_info[:tables][:mismatching][table_key][:constraints][:missing][constraint_key] = right_schema[table_key][:constraints][constraint_key]
400
+ end
401
+ end
402
+ end
403
+ end
404
+
405
+ def self.check_mismatching_constraints (left_schema, right_schema, left_info, right_info)
406
+ left_schema.each do |table_key, table_value|
407
+ # ignore tables that only appear in one of the schemas
408
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
409
+ _debug "Skipping table (6): #{table_key}"
410
+ next
411
+ end
412
+
413
+ table_value[:constraints].each do |constraint_key, constraint_value|
414
+ # ignore constraints that only appear in one of the tables
415
+ if (!left_info[:tables][:mismatching][table_key].nil? && !right_info[:tables][:mismatching][table_key].nil?) && (left_info[:tables][:mismatching][table_key][:constraints][:missing].include?(constraint_key) || right_info[:tables][:mismatching][table_key][:constraints][:missing].include?(constraint_key))
416
+ _debug "Skipping constraint: #{constraint_key}"
417
+ next
418
+ end
419
+
420
+ if left_schema[table_key][:constraints][constraint_key] != right_schema[table_key][:constraints][constraint_key]
421
+ setup_table_info(left_info, table_key)
422
+ left_info[:tables][:mismatching][table_key][:constraints][:mismatching][constraint_key] = merge_left_right_hashes(left_schema[table_key][:constraints][constraint_key], right_schema[table_key][:constraints][constraint_key])
423
+ end
424
+ end
425
+ end
426
+ end
427
+
428
+
429
+ # index comparison
430
+ def self.check_different_indexes (left_schema, right_schema, left_info, right_info)
431
+ left_schema.each do |table_key, table_value|
432
+ # ignore tables that only appear in one of the schemas
433
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
434
+ _debug "Skipping table (7): #{table_key}"
435
+ next
436
+ end
437
+
438
+ if !(keys_diff = left_schema[table_key][:indexes].keys - right_schema[table_key][:indexes].keys).empty?
439
+ setup_table_info(left_info, table_key)
440
+ left_info[:tables][:mismatching][table_key][:indexes][:extra] += keys_diff
441
+ end
442
+
443
+ if !(keys_diff = right_schema[table_key][:indexes].keys - left_schema[table_key][:indexes].keys).empty?
444
+ setup_table_info(left_info, table_key)
445
+ keys_diff.each do |index_key|
446
+ left_info[:tables][:mismatching][table_key][:indexes][:missing][index_key] = right_schema[table_key][:indexes][index_key]
447
+ end
448
+ end
449
+ end
450
+ end
451
+
452
+ def self.check_mismatching_indexes (left_schema, right_schema, left_info, right_info)
453
+ left_schema.each do |table_key, table_value|
454
+ # ignore tables that only appear in one of the schemas
455
+ if left_info[:tables][:missing].include?(table_key) || right_info[:tables][:missing].include?(table_key)
456
+ _debug "Skipping table (8): #{table_key}"
457
+ next
458
+ end
459
+
460
+ table_value[:indexes].each do |index_key, index_value|
461
+ # ignore indexes that only appear in one of the tables
462
+ if (!left_info[:tables][:mismatching][table_key].nil? && !right_info[:tables][:mismatching][table_key].nil?) && (left_info[:tables][:mismatching][table_key][:indexes][:missing].include?(index_key) || right_info[:tables][:mismatching][table_key][:indexes][:missing].include?(index_key))
463
+ _debug "Skipping index: #{index_key}"
464
+ next
465
+ end
466
+
467
+ if left_schema[table_key][:indexes][index_key] != right_schema[table_key][:indexes][index_key]
468
+ setup_table_info(left_info, table_key)
469
+ left_info[:tables][:mismatching][table_key][:indexes][:mismatching][index_key] = merge_left_right_hashes(left_schema[table_key][:indexes][index_key], right_schema[table_key][:indexes][index_key])
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ def self.print_diff_info (info, suggestion_schema, source_schema, target_schema)
476
+ puts "\nComparing '#{source_schema}' to '#{target_schema}'".magenta.bold
477
+
478
+ extra_tables = {}
479
+ missing_tables = {}
480
+ mismatching_tables = {}
481
+ mismatching_views = {}
482
+
483
+ info[:tables][:extra].each do |table|
484
+ print_extra "Table '#{table}'"
485
+ extra_tables[table] = "DROP TABLE IF EXISTS #{suggestion_schema}.#{table};"
486
+ end
487
+
488
+ info[:tables][:missing].each do |table_key, table_value|
489
+ print_missing "Table '#{table_key}'"
490
+ missing_tables[table_key] = build_create_table_lines(table_key, table_value)
491
+ end
492
+
493
+ # iterate over all tables with info
494
+ info[:tables][:mismatching].each do |table_key, table_value|
495
+ print_mismatching "Table '#{table_key}'"
496
+
497
+ mismatching_tables[table_key] = {
498
+ :isolated_drop_commands => [],
499
+ :isolated_create_commands => [],
500
+ :alter_tables => [],
501
+ :warnings => []
502
+ }
503
+
504
+ # alter tables
505
+ table_value[:constraints][:extra].each do |constraint|
506
+ print_extra " Constraint '#{constraint}'"
507
+ mismatching_tables[table_key][:alter_tables] << "DROP CONSTRAINT IF EXISTS \"#{constraint}\""
508
+ end
509
+
510
+ table_value[:columns][:extra].each do |column|
511
+ print_extra " Column '#{column}'"
512
+ mismatching_tables[table_key][:alter_tables] << "DROP COLUMN IF EXISTS #{column}"
513
+ end
514
+
515
+ table_value[:columns][:missing].each do |column_key, column_value|
516
+ print_missing " Column '#{column_key}'"
517
+ mismatching_tables[table_key][:alter_tables] << build_add_column_line(column_key, column_value, table_key)
518
+ end
519
+
520
+ table_value[:columns][:mismatching].each do |column_key, column_value|
521
+ print_mismatching " Column '#{column_key}'"
522
+ column_warnings, column_alter_tables = build_mismatching_column_lines(column_key, column_value)
523
+ mismatching_tables[table_key][:warnings] += column_warnings
524
+ mismatching_tables[table_key][:alter_tables] += column_alter_tables
525
+ end
526
+
527
+ table_value[:constraints][:missing].each do |constraint_key, constraint_value|
528
+ print_missing " Constraint '#{constraint_key}'"
529
+ mismatching_tables[table_key][:alter_tables] << build_add_constraint_line(constraint_key, constraint_value)
530
+ end
531
+
532
+ # isolated drop commands
533
+ table_value[:triggers][:extra].each do |trigger|
534
+ print_extra " Trigger '#{trigger}'"
535
+ mismatching_tables[table_key][:isolated_drop_commands] << "DROP TRIGGER IF EXISTS #{trigger} ON #{suggestion_schema}.#{table_key}"
536
+ end
537
+
538
+ table_value[:indexes][:extra].each do |index|
539
+ print_extra " Index '#{index}'"
540
+ mismatching_tables[table_key][:isolated_drop_commands] << "DROP INDEX IF EXISTS #{suggestion_schema}.#{index}"
541
+ end
542
+
543
+ # isolated create commands
544
+ table_value[:indexes][:missing].each do |index_key, index_value|
545
+ print_missing " Index '#{index_key}'"
546
+ mismatching_tables[table_key][:isolated_create_commands] << build_add_index_line(index_key, index_value)
547
+ end
548
+
549
+ table_value[:triggers][:missing].each do |trigger_key, trigger_value|
550
+ print_missing " Trigger '#{trigger_key}'"
551
+ mismatching_tables[table_key][:isolated_create_commands] << build_add_trigger_line(trigger_key, trigger_value)
552
+ end
553
+
554
+ # warnings
555
+ table_value[:triggers][:mismatching].each do |trigger_key, trigger_value|
556
+ print_mismatching " Trigger '#{trigger_key}'"
557
+ mismatching_tables[table_key][:warnings] += build_mismatching_trigger_lines(trigger_key, trigger_value)
558
+ end
559
+
560
+ table_value[:constraints][:mismatching].each do |constraint_key, constraint_value|
561
+ print_mismatching " Constraint '#{constraint_key}'"
562
+ mismatching_tables[table_key][:warnings] += build_mismatching_constraint_lines(constraint_key, constraint_value)
563
+ end
564
+
565
+ table_value[:indexes][:mismatching].each do |index_key, index_value|
566
+ print_mismatching " Index '#{index_key}'"
567
+ mismatching_tables[table_key][:warnings] += build_mismatching_index_lines(index_key, index_value)
568
+ end
569
+
570
+ end
571
+
572
+ # iterate over all views with info
573
+ info[:views][:extra].each do |view_key|
574
+ print_extra "View '#{view_key}'"
575
+ mismatching_views[view_key] = "View '#{view_key.bold}' exists in '#{source_schema}' but not in the target schema. Might need to drop it"
576
+ end
577
+
578
+ info[:views][:missing].each do |view_key|
579
+ print_missing "View '#{view_key}'"
580
+ mismatching_views[view_key] = "View '#{view_key.bold}' does not exist in '#{source_schema}'. Might need to recreate it"
581
+ end
582
+
583
+ info[:views][:mismatching].each do |view_key|
584
+ print_mismatching "View '#{view_key}'"
585
+ mismatching_views[view_key] = "View '#{view_key.bold}' does not match between schemas, please recreate it"
586
+ end
587
+
588
+ command_suggestions = {
589
+ :extra_tables => extra_tables,
590
+ :missing_tables => missing_tables,
591
+ :mismatching_tables => mismatching_tables,
592
+ :mismatching_views => mismatching_views
593
+ }
594
+
595
+ return command_suggestions
596
+ end
597
+
598
+ def self.print_schema_correction_suggestions (schema_suggestion, suggestions)
599
+
600
+ suggestions[:extra_tables].each do |table_key, command|
601
+ puts "\n-- #{table_key}".white.bold
602
+ puts "#{command}".green.bold
603
+ end
604
+
605
+ suggestions[:missing_tables].each do |table_key, table_value|
606
+ puts "\n-- #{table_key}".white.bold
607
+ puts "CREATE TABLE IF NOT EXISTS #{schema_suggestion}.#{table_key}();".green.bold
608
+ print_alter_table_commands(schema_suggestion, table_key, table_value[:alter_tables])
609
+ table_value[:isolated_commands].each do |command|
610
+ puts "#{command};".green.bold
611
+ end
612
+ table_value[:warnings].each do |warning|
613
+ _warn "#{warning}"
614
+ end
615
+ end
616
+
617
+ suggestions[:mismatching_tables].each do |table_key, table_value|
618
+ puts "\n-- #{table_key}".white.bold
619
+
620
+ # print isolated drop commands
621
+ table_value[:isolated_drop_commands].each do |command|
622
+ puts "#{command};".green.bold
623
+ end
624
+
625
+ print_alter_table_commands(schema_suggestion, table_key, table_value[:alter_tables])
626
+
627
+ # print isolated create commands
628
+ table_value[:isolated_create_commands].each do |command|
629
+ puts "#{command};".green.bold
630
+ end
631
+
632
+ # print warnings
633
+ table_value[:warnings].each do |command|
634
+ _warn "#{command}"
635
+ end
636
+ end
637
+
638
+ suggestions[:mismatching_views].each do |view_key, command|
639
+ puts "\n-- #{view_key} (View)".white.bold
640
+ _warn "#{command}"
641
+ end
642
+ end
643
+
644
+ def self.print_extra (message)
645
+ puts "+ #{message}".green.bold
646
+ end
647
+
648
+ def self.print_missing (message)
649
+ puts "- #{message}".red.bold
650
+ end
651
+
652
+ def self.print_mismatching (message)
653
+ puts "? #{message}".yellow.bold
654
+ end
655
+
656
+ # print all alter table commands together
657
+ def self.print_alter_table_commands (schema, table_key, commands)
658
+ if commands.length > 0
659
+ puts "ALTER TABLE #{schema}.#{table_key}".green.bold
660
+ commands.each_with_index do |command, index|
661
+ terminator = (index == commands.length - 1) ? ';' : ',';
662
+ puts " #{command}#{terminator}".green.bold
663
+ end
664
+ end
665
+ end
666
+
667
+ # takes 2 hashes and returns a object with both of the keys remapped
668
+ def self.merge_left_right_hashes (left_hash, right_hash)
669
+ left_rehashed = remap_hash(left_hash, 'left_')
670
+ right_rehashed = remap_hash(right_hash, 'right_')
671
+ merged_hash = {}.merge(left_rehashed).merge(right_rehashed)
672
+ return merged_hash
673
+ end
674
+
675
+ def self.remap_hash (hash, prefix)
676
+ return_hash = {}
677
+ hash.each do |key, value|
678
+ new_symbol = prefix + key.to_s
679
+ return_hash[new_symbol.to_sym] = value
680
+ end
681
+ return return_hash
682
+ end
683
+
684
+ # functions to build certain SQL commands
685
+ def self.build_create_table_lines(table_key, table_value)
686
+ alter_tables = []
687
+ isolated_commands = []
688
+ warnings = []
689
+
690
+ # columns
691
+ table_value[:columns].each do |column_key, column_value|
692
+ alter_tables << build_add_column_line(column_key, column_value, table_key)
693
+ end
694
+
695
+ # triggers
696
+ table_value[:triggers].each do |trigger_key, trigger_value|
697
+ isolated_commands << build_add_trigger_line(trigger_key, trigger_value)
698
+ end
699
+
700
+ # constraints
701
+ table_value[:constraints].each do |constraint_key, constraint_value|
702
+ alter_tables << build_add_constraint_line(constraint_key, constraint_value)
703
+ end
704
+
705
+ # indexes
706
+ table_value[:indexes].each do |index_key, index_value|
707
+ isolated_commands << build_add_index_line(index_key, index_value)
708
+ end
709
+
710
+ # warnings
711
+ warnings << 'When creating a table, keep in mind the tablespace!'
712
+ warnings << 'This is merely a suggestion of the table structure, it\'s prefered to create the table columns inside the CREATE TABLE command'
713
+
714
+ return {
715
+ :alter_tables => alter_tables,
716
+ :isolated_commands => isolated_commands,
717
+ :warnings => warnings
718
+ }
719
+ end
720
+
721
+ def self.build_add_column_line (column_key, column_info, table_key)
722
+ # if the column has a default value that matches serials, suggest column as SERIAL
723
+ # in serials, the default value is always like "nextval('<table_name>_<col_name>_seq')"
724
+ if column_info[:column_default] == "nextval('#{SCHEMA_PLACEHOLDER}.#{table_key}_#{column_key}_seq'::regclass)"
725
+ add_column_line = "ADD COLUMN #{column_key} SERIAL"
726
+ else
727
+ data_type = column_info[:column_datatype]
728
+ has_default = column_info[:column_has_default] == 't' ? true : false
729
+ default_string = has_default ? "DEFAULT #{column_info[:column_default]}" : ''
730
+ nullable = column_info[:column_not_null] == 't' ? 'NOT NULL' : ''
731
+ add_column_line = "ADD COLUMN #{column_key} #{data_type} #{nullable} #{default_string}"
732
+ end
733
+
734
+ # replace placeholder, clear extra spaces
735
+ return add_column_line.gsub(SCHEMA_PLACEHOLDER, @schema_variable).gsub(/\s+/, ' ').strip
736
+ end
737
+
738
+ def self.build_mismatching_column_lines (column_key, column_info)
739
+ warnings = []
740
+ alter_tables = []
741
+ caution_message = " Changing this property may cause problems, use with caution!".light_red # "↳" -> symbol if I decide to pass this warning in a separate line
742
+
743
+ if column_info[:left_column_num] != column_info[:right_column_num]
744
+ warnings << "Column '#{column_key}' is on position '#{column_info[:left_column_num]}' on current schema, but on position '#{column_info[:right_column_num]}' in the target schema"
745
+ end
746
+ if column_info[:left_column_default] != column_info[:right_column_default]
747
+ operation = column_info[:right_column_has_default] == 't' ? "SET DEFAULT #{column_info[:right_column_default]}".gsub(SCHEMA_PLACEHOLDER, @schema_variable) : "DROP DEFAULT"
748
+ warnings << "Column '#{column_key.bold}' DEFAULT value differs between schemas." + caution_message
749
+ alter_tables << "ALTER COLUMN #{column_key} #{operation}"
750
+ end
751
+ if column_info[:left_column_datatype] != column_info[:right_column_datatype]
752
+ warnings << "Column '#{column_key.bold}' TYPE differs between schemas." + caution_message
753
+ alter_tables << "ALTER COLUMN #{column_key} SET DATA TYPE #{column_info[:right_column_datatype]}"
754
+ end
755
+ if column_info[:left_column_not_null] != column_info[:right_column_not_null]
756
+ operation = column_info[:left_column_not_null] == 't' ? 'DROP' : 'SET'
757
+ warnings << "Column '#{column_key.bold}' NOT NULL property differs between schemas." + caution_message
758
+ alter_tables << "ALTER COLUMN #{column_key} #{operation} NOT NULL"
759
+ end
760
+ return warnings, alter_tables
761
+ end
762
+
763
+ def self.build_add_trigger_line (trigger_key, trigger_info)
764
+ trigger_def = trigger_info[:trigger_definition].gsub(SCHEMA_PLACEHOLDER, @schema_variable)
765
+ return trigger_def
766
+ end
767
+
768
+ def self.build_mismatching_trigger_lines (trigger_key, trigger_info)
769
+ warnings = []
770
+ if trigger_info[:left_trigger_definition] != trigger_info[:right_trigger_definition]
771
+ warnings << "Trigger '#{trigger_key.bold}' definition is different between schemas"
772
+ end
773
+ return warnings
774
+ end
775
+
776
+ def self.build_add_constraint_line (constraint_key, constraint_info)
777
+ constraint_def = constraint_info[:constraint_definition].gsub(SCHEMA_PLACEHOLDER, @schema_variable)
778
+ return "ADD CONSTRAINT \"#{constraint_key}\" #{constraint_def}"
779
+ end
780
+
781
+ def self.build_mismatching_constraint_lines (constraint_key, constraint_info)
782
+ warnings = []
783
+ if constraint_info[:left_constraint_definition] != constraint_info[:right_constraint_definition]
784
+ warnings << "Constraint '#{constraint_key.bold}' definition is different between schemas"
785
+ end
786
+ return warnings
787
+ end
788
+
789
+ def self.build_add_index_line(index_key, index_info)
790
+ index_def = index_info[:index_definition].gsub(SCHEMA_PLACEHOLDER, @schema_variable)
791
+ return index_def
792
+ end
793
+
794
+ def self.build_mismatching_index_lines(index_key, index_info)
795
+ warnings = []
796
+ if index_info[:left_index_definition] != index_info[:right_index_definition]
797
+ warnings << "Index '#{index_key.bold}' definition is different between schemas"
798
+ end
799
+ if index_info[:left_index_columns] != index_info[:right_index_columns]
800
+ warnings << "Index '#{index_key.bold}' affects different columns between schemas"
801
+ end
802
+ return warnings
803
+ end
804
+
805
+ end