activerecord-cipherstash-pg-adapter 0.8.0 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/column.rb +70 -0
  4. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/database_statements.rb +199 -0
  5. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/explain_pretty_printer.rb +44 -0
  6. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/array.rb +91 -0
  7. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/bit.rb +53 -0
  8. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/bit_varying.rb +15 -0
  9. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/bytea.rb +17 -0
  10. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/cidr.rb +48 -0
  11. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/date.rb +31 -0
  12. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/date_time.rb +36 -0
  13. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/decimal.rb +15 -0
  14. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/enum.rb +20 -0
  15. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/hstore.rb +109 -0
  16. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/inet.rb +15 -0
  17. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/interval.rb +49 -0
  18. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/jsonb.rb +15 -0
  19. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/legacy_point.rb +44 -0
  20. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/macaddr.rb +25 -0
  21. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/money.rb +41 -0
  22. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/oid.rb +15 -0
  23. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/point.rb +64 -0
  24. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/range.rb +124 -0
  25. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/specialized_string.rb +18 -0
  26. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/timestamp.rb +15 -0
  27. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/timestamp_with_time_zone.rb +30 -0
  28. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/type_map_initializer.rb +125 -0
  29. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/uuid.rb +35 -0
  30. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/vector.rb +28 -0
  31. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid/xml.rb +30 -0
  32. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/oid.rb +38 -0
  33. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/quoting.rb +237 -0
  34. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/referential_integrity.rb +71 -0
  35. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/schema_creation.rb +170 -0
  36. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/schema_definitions.rb +372 -0
  37. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/schema_dumper.rb +116 -0
  38. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/schema_statements.rb +1110 -0
  39. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/type_metadata.rb +44 -0
  40. data/lib/active_record/connection_adapters/7.1/cipherstash_pg/utils.rb +79 -0
  41. data/lib/active_record/connection_adapters/7.1/postgres_cipherstash_adapter.rb +1266 -0
  42. data/lib/active_record/connection_adapters/postgres_cipherstash_adapter.rb +5 -1
  43. data/lib/version.rb +1 -1
  44. metadata +42 -3
@@ -0,0 +1,1110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
6
+ module SchemaStatements
7
+ # Drops the database specified on the +name+ attribute
8
+ # and creates it again using the provided +options+.
9
+ def recreate_database(name, options = {}) # :nodoc:
10
+ drop_database(name)
11
+ create_database(name, options)
12
+ end
13
+
14
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
15
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
16
+ # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
17
+ # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
18
+ #
19
+ # Example:
20
+ # create_database config[:database], config
21
+ # create_database 'foo_development', encoding: 'unicode'
22
+ def create_database(name, options = {})
23
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
24
+
25
+ option_string = options.each_with_object(+"") do |(key, value), memo|
26
+ memo << case key
27
+ when :owner
28
+ " OWNER = \"#{value}\""
29
+ when :template
30
+ " TEMPLATE = \"#{value}\""
31
+ when :encoding
32
+ " ENCODING = '#{value}'"
33
+ when :collation
34
+ " LC_COLLATE = '#{value}'"
35
+ when :ctype
36
+ " LC_CTYPE = '#{value}'"
37
+ when :tablespace
38
+ " TABLESPACE = \"#{value}\""
39
+ when :connection_limit
40
+ " CONNECTION LIMIT = #{value}"
41
+ else
42
+ ""
43
+ end
44
+ end
45
+
46
+ execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
47
+ end
48
+
49
+ # Drops a PostgreSQL database.
50
+ #
51
+ # Example:
52
+ # drop_database 'matt_development'
53
+ def drop_database(name) # :nodoc:
54
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
55
+ end
56
+
57
+ def drop_table(table_name, **options) # :nodoc:
58
+ schema_cache.clear_data_source_cache!(table_name.to_s)
59
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
60
+ end
61
+
62
+ # Returns true if schema exists.
63
+ def schema_exists?(name)
64
+ query_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0
65
+ end
66
+
67
+ # Verifies existence of an index with a given name.
68
+ def index_name_exists?(table_name, index_name)
69
+ table = quoted_scope(table_name)
70
+ index = quoted_scope(index_name)
71
+
72
+ query_value(<<~SQL, "SCHEMA").to_i > 0
73
+ SELECT COUNT(*)
74
+ FROM pg_class t
75
+ INNER JOIN pg_index d ON t.oid = d.indrelid
76
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
77
+ LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
78
+ WHERE i.relkind IN ('i', 'I')
79
+ AND i.relname = #{index[:name]}
80
+ AND t.relname = #{table[:name]}
81
+ AND n.nspname = #{table[:schema]}
82
+ SQL
83
+ end
84
+
85
+ # Returns an array of indexes for the given table.
86
+ def indexes(table_name) # :nodoc:
87
+ scope = quoted_scope(table_name)
88
+
89
+ result = query(<<~SQL, "SCHEMA")
90
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
91
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid
92
+ FROM pg_class t
93
+ INNER JOIN pg_index d ON t.oid = d.indrelid
94
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
95
+ LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
96
+ WHERE i.relkind IN ('i', 'I')
97
+ AND d.indisprimary = 'f'
98
+ AND t.relname = #{scope[:name]}
99
+ AND n.nspname = #{scope[:schema]}
100
+ ORDER BY i.relname
101
+ SQL
102
+
103
+ result.map do |row|
104
+ index_name = row[0]
105
+ unique = row[1]
106
+ indkey = row[2].split(" ").map(&:to_i)
107
+ inddef = row[3]
108
+ oid = row[4]
109
+ comment = row[5]
110
+ valid = row[6]
111
+ using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
112
+
113
+ orders = {}
114
+ opclasses = {}
115
+ include_columns = include ? include.split(",").map(&:strip) : []
116
+
117
+ if indkey.include?(0)
118
+ columns = expressions
119
+ else
120
+ columns = column_names_from_column_numbers(oid, indkey)
121
+
122
+ # prevent INCLUDE columns from being matched
123
+ columns.reject! { |c| include_columns.include?(c) }
124
+
125
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
126
+ # and non-default opclasses
127
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
128
+ opclasses[column] = opclass.to_sym if opclass
129
+ if nulls
130
+ orders[column] = [desc, nulls].compact.join(" ")
131
+ else
132
+ orders[column] = :desc if desc
133
+ end
134
+ end
135
+ end
136
+
137
+ IndexDefinition.new(
138
+ table_name,
139
+ index_name,
140
+ unique,
141
+ columns,
142
+ orders: orders,
143
+ opclasses: opclasses,
144
+ where: where,
145
+ using: using.to_sym,
146
+ include: include_columns.presence,
147
+ nulls_not_distinct: nulls_not_distinct.present?,
148
+ comment: comment.presence,
149
+ valid: valid
150
+ )
151
+ end
152
+ end
153
+
154
+ def table_options(table_name) # :nodoc:
155
+ if comment = table_comment(table_name)
156
+ { comment: comment }
157
+ end
158
+ end
159
+
160
+ # Returns a comment stored in database for given table
161
+ def table_comment(table_name) # :nodoc:
162
+ scope = quoted_scope(table_name, type: "BASE TABLE")
163
+ if scope[:name]
164
+ query_value(<<~SQL, "SCHEMA")
165
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
166
+ FROM pg_catalog.pg_class c
167
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
168
+ WHERE c.relname = #{scope[:name]}
169
+ AND c.relkind IN (#{scope[:type]})
170
+ AND n.nspname = #{scope[:schema]}
171
+ SQL
172
+ end
173
+ end
174
+
175
+ # Returns the current database name.
176
+ def current_database
177
+ query_value("SELECT current_database()", "SCHEMA")
178
+ end
179
+
180
+ # Returns the current schema name.
181
+ def current_schema
182
+ query_value("SELECT current_schema", "SCHEMA")
183
+ end
184
+
185
+ # Returns the current database encoding format.
186
+ def encoding
187
+ query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA")
188
+ end
189
+
190
+ # Returns the current database collation.
191
+ def collation
192
+ query_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA")
193
+ end
194
+
195
+ # Returns the current database ctype.
196
+ def ctype
197
+ query_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA")
198
+ end
199
+
200
+ # Returns an array of schema names.
201
+ def schema_names
202
+ query_values(<<~SQL, "SCHEMA")
203
+ SELECT nspname
204
+ FROM pg_namespace
205
+ WHERE nspname !~ '^pg_.*'
206
+ AND nspname NOT IN ('information_schema')
207
+ ORDER by nspname;
208
+ SQL
209
+ end
210
+
211
+ # Creates a schema for the given schema name.
212
+ def create_schema(schema_name)
213
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
214
+ end
215
+
216
+ # Drops the schema for the given schema name.
217
+ def drop_schema(schema_name, **options)
218
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
219
+ end
220
+
221
+ # Sets the schema search path to a string of comma-separated schema names.
222
+ # Names beginning with $ have to be quoted (e.g. $user => '$user').
223
+ # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
224
+ #
225
+ # This should be not be called manually but set in database.yml.
226
+ def schema_search_path=(schema_csv)
227
+ if schema_csv
228
+ internal_execute("SET search_path TO #{schema_csv}")
229
+ @schema_search_path = schema_csv
230
+ end
231
+ end
232
+
233
+ # Returns the active schema search path.
234
+ def schema_search_path
235
+ @schema_search_path ||= query_value("SHOW search_path", "SCHEMA")
236
+ end
237
+
238
+ # Returns the current client message level.
239
+ def client_min_messages
240
+ query_value("SHOW client_min_messages", "SCHEMA")
241
+ end
242
+
243
+ # Set the client message level.
244
+ def client_min_messages=(level)
245
+ internal_execute("SET client_min_messages TO '#{level}'")
246
+ end
247
+
248
+ # Returns the sequence name for a table's primary key or some other specified key.
249
+ def default_sequence_name(table_name, pk = "id") # :nodoc:
250
+ result = serial_sequence(table_name, pk)
251
+ return nil unless result
252
+ Utils.extract_schema_qualified_name(result).to_s
253
+ rescue ActiveRecord::StatementInvalid
254
+ CipherStashPG::Name.new(nil, "#{table_name}_#{pk}_seq").to_s
255
+ end
256
+
257
+ def serial_sequence(table, column)
258
+ query_value("SELECT pg_get_serial_sequence(#{quote(table)}, #{quote(column)})", "SCHEMA")
259
+ end
260
+
261
+ # Sets the sequence of a table's primary key to the specified value.
262
+ def set_pk_sequence!(table, value) # :nodoc:
263
+ pk, sequence = pk_and_sequence_for(table)
264
+
265
+ if pk
266
+ if sequence
267
+ quoted_sequence = quote_table_name(sequence)
268
+
269
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
270
+ else
271
+ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
272
+ end
273
+ end
274
+ end
275
+
276
+ # Resets the sequence of a table's primary key to the maximum value.
277
+ def reset_pk_sequence!(table, pk = nil, sequence = nil) # :nodoc:
278
+ unless pk && sequence
279
+ default_pk, default_sequence = pk_and_sequence_for(table)
280
+
281
+ pk ||= default_pk
282
+ sequence ||= default_sequence
283
+ end
284
+
285
+ if @logger && pk && !sequence
286
+ @logger.warn "#{table} has primary key #{pk} with no default sequence."
287
+ end
288
+
289
+ if pk && sequence
290
+ quoted_sequence = quote_table_name(sequence)
291
+ max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
292
+ if max_pk.nil?
293
+ if database_version >= 10_00_00
294
+ minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
295
+ else
296
+ minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
297
+ end
298
+ end
299
+
300
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
301
+ end
302
+ end
303
+
304
+ # Returns a table's primary key and belonging sequence.
305
+ def pk_and_sequence_for(table) # :nodoc:
306
+ # First try looking for a sequence with a dependency on the
307
+ # given table's primary key.
308
+ result = query(<<~SQL, "SCHEMA")[0]
309
+ SELECT attr.attname, nsp.nspname, seq.relname
310
+ FROM pg_class seq,
311
+ pg_attribute attr,
312
+ pg_depend dep,
313
+ pg_constraint cons,
314
+ pg_namespace nsp
315
+ WHERE seq.oid = dep.objid
316
+ AND seq.relkind = 'S'
317
+ AND attr.attrelid = dep.refobjid
318
+ AND attr.attnum = dep.refobjsubid
319
+ AND attr.attrelid = cons.conrelid
320
+ AND attr.attnum = cons.conkey[1]
321
+ AND seq.relnamespace = nsp.oid
322
+ AND cons.contype = 'p'
323
+ AND dep.classid = 'pg_class'::regclass
324
+ AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
325
+ SQL
326
+
327
+ if result.nil? || result.empty?
328
+ result = query(<<~SQL, "SCHEMA")[0]
329
+ SELECT attr.attname, nsp.nspname,
330
+ CASE
331
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
332
+ WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
333
+ substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
334
+ strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
335
+ ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
336
+ END
337
+ FROM pg_class t
338
+ JOIN pg_attribute attr ON (t.oid = attrelid)
339
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
340
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
341
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
342
+ WHERE t.oid = #{quote(quote_table_name(table))}::regclass
343
+ AND cons.contype = 'p'
344
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
345
+ SQL
346
+ end
347
+
348
+ pk = result.shift
349
+ if result.last
350
+ [pk, CipherStashPG::Name.new(*result)]
351
+ else
352
+ [pk, nil]
353
+ end
354
+ rescue
355
+ nil
356
+ end
357
+
358
+ def primary_keys(table_name) # :nodoc:
359
+ query_values(<<~SQL, "SCHEMA")
360
+ SELECT a.attname
361
+ FROM (
362
+ SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
363
+ FROM pg_index
364
+ WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
365
+ AND indisprimary
366
+ ) i
367
+ JOIN pg_attribute a
368
+ ON a.attrelid = i.indrelid
369
+ AND a.attnum = i.indkey[i.idx]
370
+ ORDER BY i.idx
371
+ SQL
372
+ end
373
+
374
+ # Renames a table.
375
+ # Also renames a table's primary key sequence if the sequence name exists and
376
+ # matches the Active Record default.
377
+ #
378
+ # Example:
379
+ # rename_table('octopuses', 'octopi')
380
+ def rename_table(table_name, new_name, **options)
381
+ validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
382
+ clear_cache!
383
+ schema_cache.clear_data_source_cache!(table_name.to_s)
384
+ schema_cache.clear_data_source_cache!(new_name.to_s)
385
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
386
+ pk, seq = pk_and_sequence_for(new_name)
387
+ if pk
388
+ idx = "#{table_name}_pkey"
389
+ new_idx = "#{new_name}_pkey"
390
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
391
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
392
+ new_seq = "#{new_name}_#{pk}_seq"
393
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
394
+ end
395
+ end
396
+ rename_table_indexes(table_name, new_name)
397
+ end
398
+
399
+ def add_column(table_name, column_name, type, **options) # :nodoc:
400
+ clear_cache!
401
+ super
402
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
403
+ end
404
+
405
+ def change_column(table_name, column_name, type, **options) # :nodoc:
406
+ clear_cache!
407
+ sqls, procs = Array(change_column_for_alter(table_name, column_name, type, **options)).partition { |v| v.is_a?(String) }
408
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
409
+ procs.each(&:call)
410
+ end
411
+
412
+ # Builds a ChangeColumnDefinition object.
413
+ #
414
+ # This definition object contains information about the column change that would occur
415
+ # if the same arguments were passed to #change_column. See #change_column for information about
416
+ # passing a +table_name+, +column_name+, +type+ and other options that can be passed.
417
+ def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
418
+ td = create_table_definition(table_name)
419
+ cd = td.new_column_definition(column_name, type, **options)
420
+ ChangeColumnDefinition.new(cd, column_name)
421
+ end
422
+
423
+ # Changes the default value of a table column.
424
+ def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
425
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
426
+ end
427
+
428
+ def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
429
+ column = column_for(table_name, column_name)
430
+ return unless column
431
+
432
+ default = extract_new_default_value(default_or_changes)
433
+ ChangeColumnDefaultDefinition.new(column, default)
434
+ end
435
+
436
+ def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
437
+ validate_change_column_null_argument!(null)
438
+
439
+ clear_cache!
440
+ unless null || default.nil?
441
+ column = column_for(table_name, column_name)
442
+ execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
443
+ end
444
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
445
+ end
446
+
447
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
448
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
449
+ clear_cache!
450
+ comment = extract_new_comment_value(comment_or_changes)
451
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
452
+ end
453
+
454
+ # Adds comment for given table or drops it if +comment+ is a +nil+
455
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
456
+ clear_cache!
457
+ comment = extract_new_comment_value(comment_or_changes)
458
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
459
+ end
460
+
461
+ # Renames a column in a table.
462
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
463
+ clear_cache!
464
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
465
+ rename_column_indexes(table_name, column_name, new_column_name)
466
+ end
467
+
468
+ def add_index(table_name, column_name, **options) # :nodoc:
469
+ create_index = build_create_index_definition(table_name, column_name, **options)
470
+ result = execute schema_creation.accept(create_index)
471
+
472
+ index = create_index.index
473
+ execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
474
+ result
475
+ end
476
+
477
+ def build_create_index_definition(table_name, column_name, **options) # :nodoc:
478
+ index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
479
+ CreateIndexDefinition.new(index, algorithm, if_not_exists)
480
+ end
481
+
482
+ def remove_index(table_name, column_name = nil, **options) # :nodoc:
483
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
484
+
485
+ if options.key?(:name)
486
+ provided_index = Utils.extract_schema_qualified_name(options[:name].to_s)
487
+
488
+ options[:name] = provided_index.identifier
489
+ table = CipherStashPG::Name.new(provided_index.schema, table.identifier) unless table.schema.present?
490
+
491
+ if provided_index.schema.present? && table.schema != provided_index.schema
492
+ raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'")
493
+ end
494
+ end
495
+
496
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
497
+
498
+ index_to_remove = CipherStashPG::Name.new(table.schema, index_name_for_remove(table.to_s, column_name, options))
499
+
500
+ execute "DROP INDEX #{index_algorithm(options[:algorithm])} #{quote_table_name(index_to_remove)}"
501
+ end
502
+
503
+ # Renames an index of a table. Raises error if length of new
504
+ # index name is greater than allowed limit.
505
+ def rename_index(table_name, old_name, new_name)
506
+ validate_index_length!(table_name, new_name)
507
+
508
+ schema, = extract_schema_qualified_name(table_name)
509
+ execute "ALTER INDEX #{quote_table_name(schema) + '.' if schema}#{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
510
+ end
511
+
512
+ def index_name(table_name, options) # :nodoc:
513
+ _schema, table_name = extract_schema_qualified_name(table_name.to_s)
514
+ super
515
+ end
516
+
517
+ def add_foreign_key(from_table, to_table, **options)
518
+ if options[:deferrable] == true
519
+ ActiveRecord.deprecator.warn(<<~MSG)
520
+ `deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
521
+ MSG
522
+
523
+ options[:deferrable] = :immediate
524
+ end
525
+
526
+ assert_valid_deferrable(options[:deferrable])
527
+
528
+ super
529
+ end
530
+
531
+ def foreign_keys(table_name)
532
+ scope = quoted_scope(table_name)
533
+ fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
534
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
535
+ FROM pg_constraint c
536
+ JOIN pg_class t1 ON c.conrelid = t1.oid
537
+ JOIN pg_class t2 ON c.confrelid = t2.oid
538
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
539
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
540
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
541
+ WHERE c.contype = 'f'
542
+ AND t1.relname = #{scope[:name]}
543
+ AND t3.nspname = #{scope[:schema]}
544
+ ORDER BY c.conname
545
+ SQL
546
+
547
+ fk_info.map do |row|
548
+ to_table = Utils.unquote_identifier(row["to_table"])
549
+ conkey = row["conkey"].scan(/\d+/).map(&:to_i)
550
+ confkey = row["confkey"].scan(/\d+/).map(&:to_i)
551
+
552
+ if conkey.size > 1
553
+ column = column_names_from_column_numbers(row["conrelid"], conkey)
554
+ primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
555
+ else
556
+ column = Utils.unquote_identifier(row["column"])
557
+ primary_key = row["primary_key"]
558
+ end
559
+
560
+ options = {
561
+ column: column,
562
+ name: row["name"],
563
+ primary_key: primary_key
564
+ }
565
+
566
+ options[:on_delete] = extract_foreign_key_action(row["on_delete"])
567
+ options[:on_update] = extract_foreign_key_action(row["on_update"])
568
+ options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
569
+
570
+ options[:validate] = row["valid"]
571
+
572
+ ForeignKeyDefinition.new(table_name, to_table, options)
573
+ end
574
+ end
575
+
576
+ def foreign_tables
577
+ query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
578
+ end
579
+
580
+ def foreign_table_exists?(table_name)
581
+ query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
582
+ end
583
+
584
+ def check_constraints(table_name) # :nodoc:
585
+ scope = quoted_scope(table_name)
586
+
587
+ check_info = internal_exec_query(<<-SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
588
+ SELECT conname, pg_get_constraintdef(c.oid, true) AS constraintdef, c.convalidated AS valid
589
+ FROM pg_constraint c
590
+ JOIN pg_class t ON c.conrelid = t.oid
591
+ JOIN pg_namespace n ON n.oid = c.connamespace
592
+ WHERE c.contype = 'c'
593
+ AND t.relname = #{scope[:name]}
594
+ AND n.nspname = #{scope[:schema]}
595
+ SQL
596
+
597
+ check_info.map do |row|
598
+ options = {
599
+ name: row["conname"],
600
+ validate: row["valid"]
601
+ }
602
+ expression = row["constraintdef"][/CHECK \((.+)\)/m, 1]
603
+
604
+ CheckConstraintDefinition.new(table_name, expression, options)
605
+ end
606
+ end
607
+
608
+ # Returns an array of exclusion constraints for the given table.
609
+ # The exclusion constraints are represented as ExclusionConstraintDefinition objects.
610
+ def exclusion_constraints(table_name)
611
+ scope = quoted_scope(table_name)
612
+
613
+ exclusion_info = internal_exec_query(<<-SQL, "SCHEMA")
614
+ SELECT conname, pg_get_constraintdef(c.oid) AS constraintdef, c.condeferrable, c.condeferred
615
+ FROM pg_constraint c
616
+ JOIN pg_class t ON c.conrelid = t.oid
617
+ JOIN pg_namespace n ON n.oid = c.connamespace
618
+ WHERE c.contype = 'x'
619
+ AND t.relname = #{scope[:name]}
620
+ AND n.nspname = #{scope[:schema]}
621
+ SQL
622
+
623
+ exclusion_info.map do |row|
624
+ method_and_elements, predicate = row["constraintdef"].split(" WHERE ")
625
+ method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/)
626
+ predicate.remove!(/ DEFERRABLE(?: INITIALLY (?:IMMEDIATE|DEFERRED))?/) if predicate
627
+ predicate = predicate.from(2).to(-3) if predicate # strip 2 opening and closing parentheses
628
+
629
+ deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
630
+
631
+ options = {
632
+ name: row["conname"],
633
+ using: method_and_elements_parts["using"].to_sym,
634
+ where: predicate,
635
+ deferrable: deferrable
636
+ }
637
+
638
+ ExclusionConstraintDefinition.new(table_name, method_and_elements_parts["expression"], options)
639
+ end
640
+ end
641
+
642
+ # Returns an array of unique constraints for the given table.
643
+ # The unique constraints are represented as UniqueKeyDefinition objects.
644
+ def unique_keys(table_name)
645
+ scope = quoted_scope(table_name)
646
+
647
+ unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
648
+ SELECT c.conname, c.conindid, c.condeferrable, c.condeferred
649
+ FROM pg_constraint c
650
+ JOIN pg_class t ON c.conrelid = t.oid
651
+ JOIN pg_namespace n ON n.oid = c.connamespace
652
+ WHERE c.contype = 'u'
653
+ AND t.relname = #{scope[:name]}
654
+ AND n.nspname = #{scope[:schema]}
655
+ SQL
656
+
657
+ unique_info.map do |row|
658
+ deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
659
+
660
+ columns = query_values(<<~SQL, "SCHEMA")
661
+ SELECT a.attname
662
+ FROM pg_attribute a
663
+ WHERE a.attrelid = #{row['conindid']}
664
+ ORDER BY a.attnum
665
+ SQL
666
+
667
+ options = {
668
+ name: row["conname"],
669
+ deferrable: deferrable
670
+ }
671
+
672
+ UniqueKeyDefinition.new(table_name, columns, options)
673
+ end
674
+ end
675
+
676
+ # Adds a new exclusion constraint to the table. +expression+ is a String
677
+ # representation of a list of exclusion elements and operators.
678
+ #
679
+ # add_exclusion_constraint :products, "price WITH =, availability_range WITH &&", using: :gist, name: "price_check"
680
+ #
681
+ # generates:
682
+ #
683
+ # ALTER TABLE "products" ADD CONSTRAINT price_check EXCLUDE USING gist (price WITH =, availability_range WITH &&)
684
+ #
685
+ # The +options+ hash can include the following keys:
686
+ # [<tt>:name</tt>]
687
+ # The constraint name. Defaults to <tt>excl_rails_<identifier></tt>.
688
+ # [<tt>:deferrable</tt>]
689
+ # Specify whether or not the exclusion constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
690
+ def add_exclusion_constraint(table_name, expression, **options)
691
+ options = exclusion_constraint_options(table_name, expression, options)
692
+ at = create_alter_table(table_name)
693
+ at.add_exclusion_constraint(expression, options)
694
+
695
+ execute schema_creation.accept(at)
696
+ end
697
+
698
+ def exclusion_constraint_options(table_name, expression, options) # :nodoc:
699
+ assert_valid_deferrable(options[:deferrable])
700
+
701
+ options = options.dup
702
+ options[:name] ||= exclusion_constraint_name(table_name, expression: expression, **options)
703
+ options
704
+ end
705
+
706
+ # Removes the given exclusion constraint from the table.
707
+ #
708
+ # remove_exclusion_constraint :products, name: "price_check"
709
+ #
710
+ # The +expression+ parameter will be ignored if present. It can be helpful
711
+ # to provide this in a migration's +change+ method so it can be reverted.
712
+ # In that case, +expression+ will be used by #add_exclusion_constraint.
713
+ def remove_exclusion_constraint(table_name, expression = nil, **options)
714
+ excl_name_to_delete = exclusion_constraint_for!(table_name, expression: expression, **options).name
715
+
716
+ at = create_alter_table(table_name)
717
+ at.drop_exclusion_constraint(excl_name_to_delete)
718
+
719
+ execute schema_creation.accept(at)
720
+ end
721
+
722
+ # Adds a new unique constraint to the table.
723
+ #
724
+ # add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_position"
725
+ #
726
+ # generates:
727
+ #
728
+ # ALTER TABLE "sections" ADD CONSTRAINT unique_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
729
+ #
730
+ # If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints.
731
+ #
732
+ # add_unique_key :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position"
733
+ #
734
+ # The +options+ hash can include the following keys:
735
+ # [<tt>:name</tt>]
736
+ # The constraint name. Defaults to <tt>uniq_rails_<identifier></tt>.
737
+ # [<tt>:deferrable</tt>]
738
+ # Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
739
+ # [<tt>:using_index</tt>]
740
+ # To specify an existing unique index name. Defaults to +nil+.
741
+ def add_unique_key(table_name, column_name = nil, **options)
742
+ options = unique_key_options(table_name, column_name, options)
743
+ at = create_alter_table(table_name)
744
+ at.add_unique_key(column_name, options)
745
+
746
+ execute schema_creation.accept(at)
747
+ end
748
+
749
+ def unique_key_options(table_name, column_name, options) # :nodoc:
750
+ assert_valid_deferrable(options[:deferrable])
751
+
752
+ if column_name && options[:using_index]
753
+ raise ArgumentError, "Cannot specify both column_name and :using_index options."
754
+ end
755
+
756
+ options = options.dup
757
+ options[:name] ||= unique_key_name(table_name, column: column_name, **options)
758
+ options
759
+ end
760
+
761
+ # Removes the given unique constraint from the table.
762
+ #
763
+ # remove_unique_key :sections, name: "unique_position"
764
+ #
765
+ # The +column_name+ parameter will be ignored if present. It can be helpful
766
+ # to provide this in a migration's +change+ method so it can be reverted.
767
+ # In that case, +column_name+ will be used by #add_unique_key.
768
+ def remove_unique_key(table_name, column_name = nil, **options)
769
+ unique_name_to_delete = unique_key_for!(table_name, column: column_name, **options).name
770
+
771
+ at = create_alter_table(table_name)
772
+ at.drop_unique_key(unique_name_to_delete)
773
+
774
+ execute schema_creation.accept(at)
775
+ end
776
+
777
+ # Maps logical Rails types to PostgreSQL-specific data types.
778
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, enum_type: nil, **) # :nodoc:
779
+ sql = \
780
+ case type.to_s
781
+ when "binary"
782
+ # PostgreSQL doesn't support limits on binary (bytea) columns.
783
+ # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
784
+ case limit
785
+ when nil, 0..0x3fffffff; super(type)
786
+ else raise ArgumentError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
787
+ end
788
+ when "text"
789
+ # PostgreSQL doesn't support limits on text columns.
790
+ # The hard limit is 1GB, according to section 8.3 in the manual.
791
+ case limit
792
+ when nil, 0..0x3fffffff; super(type)
793
+ else raise ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
794
+ end
795
+ when "integer"
796
+ case limit
797
+ when 1, 2; "smallint"
798
+ when nil, 3, 4; "integer"
799
+ when 5..8; "bigint"
800
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
801
+ end
802
+ when "enum"
803
+ raise ArgumentError, "enum_type is required for enums" if enum_type.nil?
804
+
805
+ enum_type
806
+ else
807
+ super
808
+ end
809
+
810
+ sql = "#{sql}[]" if array && type != :primary_key
811
+ sql
812
+ end
813
+
814
+ # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
815
+ # requires that the ORDER BY include the distinct column.
816
+ def columns_for_distinct(columns, orders) # :nodoc:
817
+ order_columns = orders.compact_blank.map { |s|
818
+ # Convert Arel node to string
819
+ s = visitor.compile(s) unless s.is_a?(String)
820
+ # Remove any ASC/DESC modifiers
821
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
822
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
823
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
824
+
825
+ (order_columns << super).join(", ")
826
+ end
827
+
828
+ def update_table_definition(table_name, base) # :nodoc:
829
+ CipherStashPG::Table.new(table_name, base)
830
+ end
831
+
832
+ def create_schema_dumper(options) # :nodoc:
833
+ CipherStashPG::SchemaDumper.create(self, options)
834
+ end
835
+
836
+ # Validates the given constraint.
837
+ #
838
+ # Validates the constraint named +constraint_name+ on +accounts+.
839
+ #
840
+ # validate_constraint :accounts, :constraint_name
841
+ def validate_constraint(table_name, constraint_name)
842
+ at = create_alter_table table_name
843
+ at.validate_constraint constraint_name
844
+
845
+ execute schema_creation.accept(at)
846
+ end
847
+
848
+ # Validates the given foreign key.
849
+ #
850
+ # Validates the foreign key on +accounts.branch_id+.
851
+ #
852
+ # validate_foreign_key :accounts, :branches
853
+ #
854
+ # Validates the foreign key on +accounts.owner_id+.
855
+ #
856
+ # validate_foreign_key :accounts, column: :owner_id
857
+ #
858
+ # Validates the foreign key named +special_fk_name+ on the +accounts+ table.
859
+ #
860
+ # validate_foreign_key :accounts, name: :special_fk_name
861
+ #
862
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
863
+ def validate_foreign_key(from_table, to_table = nil, **options)
864
+ fk_name_to_validate = foreign_key_for!(from_table, to_table: to_table, **options).name
865
+
866
+ validate_constraint from_table, fk_name_to_validate
867
+ end
868
+
869
+ # Validates the given check constraint.
870
+ #
871
+ # validate_check_constraint :products, name: "price_check"
872
+ #
873
+ # The +options+ hash accepts the same keys as add_check_constraint[rdoc-ref:ConnectionAdapters::SchemaStatements#add_check_constraint].
874
+ def validate_check_constraint(table_name, **options)
875
+ chk_name_to_validate = check_constraint_for!(table_name, **options).name
876
+
877
+ validate_constraint table_name, chk_name_to_validate
878
+ end
879
+
880
+ def foreign_key_column_for(table_name, column_name) # :nodoc:
881
+ _schema, table_name = extract_schema_qualified_name(table_name)
882
+ super
883
+ end
884
+
885
+ def add_index_options(table_name, column_name, **options) # :nodoc:
886
+ if (where = options[:where]) && table_exists?(table_name) && column_exists?(table_name, where)
887
+ options[:where] = quote_column_name(where)
888
+ end
889
+ super
890
+ end
891
+
892
+ def quoted_include_columns_for_index(column_names) # :nodoc:
893
+ return quote_column_name(column_names) if column_names.is_a?(Symbol)
894
+
895
+ quoted_columns = column_names.each_with_object({}) do |name, result|
896
+ result[name.to_sym] = quote_column_name(name).dup
897
+ end
898
+ add_options_for_index_columns(quoted_columns).values.join(", ")
899
+ end
900
+
901
+ def schema_creation # :nodoc:
902
+ CipherStashPG::SchemaCreation.new(self)
903
+ end
904
+
905
+ private
906
+ def create_table_definition(name, **options)
907
+ CipherStashPG::TableDefinition.new(self, name, **options)
908
+ end
909
+
910
+ def create_alter_table(name)
911
+ CipherStashPG::AlterTable.new create_table_definition(name)
912
+ end
913
+
914
+ def new_column_from_field(table_name, field, _definitions)
915
+ column_name, type, default, notnull, oid, fmod, collation, comment, attgenerated = field
916
+ type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
917
+ default_value = extract_value_from_default(default)
918
+
919
+ if attgenerated.present?
920
+ default_function = default
921
+ else
922
+ default_function = extract_default_function(default_value, default)
923
+ end
924
+
925
+ if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
926
+ serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
927
+ end
928
+
929
+ CipherStashPG::Column.new(
930
+ column_name,
931
+ default_value,
932
+ type_metadata,
933
+ !notnull,
934
+ default_function,
935
+ collation: collation,
936
+ comment: comment.presence,
937
+ serial: serial,
938
+ generated: attgenerated
939
+ )
940
+ end
941
+
942
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
943
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
944
+ simple_type = SqlTypeMetadata.new(
945
+ sql_type: sql_type,
946
+ type: cast_type.type,
947
+ limit: cast_type.limit,
948
+ precision: cast_type.precision,
949
+ scale: cast_type.scale,
950
+ )
951
+ CipherStashPG::TypeMetadata.new(simple_type, oid: oid, fmod: fmod)
952
+ end
953
+
954
+ def sequence_name_from_parts(table_name, column_name, suffix)
955
+ over_length = [table_name, column_name, suffix].sum(&:length) + 2 - max_identifier_length
956
+
957
+ if over_length > 0
958
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
959
+ over_length -= column_name.length - column_name_length
960
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
961
+ end
962
+
963
+ if over_length > 0
964
+ table_name = table_name[0, table_name.length - over_length]
965
+ end
966
+
967
+ "#{table_name}_#{column_name}_#{suffix}"
968
+ end
969
+
970
+ def extract_foreign_key_action(specifier)
971
+ case specifier
972
+ when "c"; :cascade
973
+ when "n"; :nullify
974
+ when "r"; :restrict
975
+ end
976
+ end
977
+
978
+ def assert_valid_deferrable(deferrable)
979
+ return if !deferrable || %i(immediate deferred).include?(deferrable)
980
+
981
+ raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
982
+ end
983
+
984
+ def extract_constraint_deferrable(deferrable, deferred)
985
+ deferrable && (deferred ? :deferred : :immediate)
986
+ end
987
+
988
+ def reference_name_for_table(table_name)
989
+ _schema, table_name = extract_schema_qualified_name(table_name.to_s)
990
+ table_name.singularize
991
+ end
992
+
993
+ def add_column_for_alter(table_name, column_name, type, **options)
994
+ return super unless options.key?(:comment)
995
+ [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
996
+ end
997
+
998
+ def change_column_for_alter(table_name, column_name, type, **options)
999
+ change_col_def = build_change_column_definition(table_name, column_name, type, **options)
1000
+ sqls = [schema_creation.accept(change_col_def)]
1001
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
1002
+ sqls
1003
+ end
1004
+
1005
+ def change_column_null_for_alter(table_name, column_name, null, default = nil)
1006
+ if default.nil?
1007
+ "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
1008
+ else
1009
+ Proc.new { change_column_null(table_name, column_name, null, default) }
1010
+ end
1011
+ end
1012
+
1013
+ def add_index_opclass(quoted_columns, **options)
1014
+ opclasses = options_for_index_columns(options[:opclass])
1015
+ quoted_columns.each do |name, column|
1016
+ column << " #{opclasses[name]}" if opclasses[name].present?
1017
+ end
1018
+ end
1019
+
1020
+ def add_options_for_index_columns(quoted_columns, **options)
1021
+ quoted_columns = add_index_opclass(quoted_columns, **options)
1022
+ super
1023
+ end
1024
+
1025
+ def exclusion_constraint_name(table_name, **options)
1026
+ options.fetch(:name) do
1027
+ expression = options.fetch(:expression)
1028
+ identifier = "#{table_name}_#{expression}_excl"
1029
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1030
+
1031
+ "excl_rails_#{hashed_identifier}"
1032
+ end
1033
+ end
1034
+
1035
+ def exclusion_constraint_for(table_name, **options)
1036
+ excl_name = exclusion_constraint_name(table_name, **options)
1037
+ exclusion_constraints(table_name).detect { |excl| excl.name == excl_name }
1038
+ end
1039
+
1040
+ def exclusion_constraint_for!(table_name, expression: nil, **options)
1041
+ exclusion_constraint_for(table_name, expression: expression, **options) ||
1042
+ raise(ArgumentError, "Table '#{table_name}' has no exclusion constraint for #{expression || options}")
1043
+ end
1044
+
1045
+ def unique_key_name(table_name, **options)
1046
+ options.fetch(:name) do
1047
+ column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s)
1048
+ identifier = "#{table_name}_#{column_or_index * '_and_'}_unique"
1049
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1050
+
1051
+ "uniq_rails_#{hashed_identifier}"
1052
+ end
1053
+ end
1054
+
1055
+ def unique_key_for(table_name, **options)
1056
+ name = unique_key_name(table_name, **options) unless options.key?(:column)
1057
+ unique_keys(table_name).detect { |unique_key| unique_key.defined_for?(name: name, **options) }
1058
+ end
1059
+
1060
+ def unique_key_for!(table_name, column: nil, **options)
1061
+ unique_key_for(table_name, column: column, **options) ||
1062
+ raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}")
1063
+ end
1064
+
1065
+ def data_source_sql(name = nil, type: nil)
1066
+ scope = quoted_scope(name, type: type)
1067
+ scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
1068
+
1069
+ sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
1070
+ sql << " WHERE n.nspname = #{scope[:schema]}"
1071
+ sql << " AND c.relname = #{scope[:name]}" if scope[:name]
1072
+ sql << " AND c.relkind IN (#{scope[:type]})"
1073
+ sql
1074
+ end
1075
+
1076
+ def quoted_scope(name = nil, type: nil)
1077
+ schema, name = extract_schema_qualified_name(name)
1078
+ type = \
1079
+ case type
1080
+ when "BASE TABLE"
1081
+ "'r','p'"
1082
+ when "VIEW"
1083
+ "'v','m'"
1084
+ when "FOREIGN TABLE"
1085
+ "'f'"
1086
+ end
1087
+ scope = {}
1088
+ scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
1089
+ scope[:name] = quote(name) if name
1090
+ scope[:type] = type if type
1091
+ scope
1092
+ end
1093
+
1094
+ def extract_schema_qualified_name(string)
1095
+ name = Utils.extract_schema_qualified_name(string.to_s)
1096
+ [name.schema, name.identifier]
1097
+ end
1098
+
1099
+ def column_names_from_column_numbers(table_oid, column_numbers)
1100
+ Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
1101
+ SELECT a.attnum, a.attname
1102
+ FROM pg_attribute a
1103
+ WHERE a.attrelid = #{table_oid}
1104
+ AND a.attnum IN (#{column_numbers.join(", ")})
1105
+ SQL
1106
+ end
1107
+ end
1108
+ end
1109
+ end
1110
+ end