activerecord-yugabytedb-adapter 7.0.4.1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +10 -0
  6. data/Gemfile.lock +67 -0
  7. data/README.md +31 -0
  8. data/Rakefile +8 -0
  9. data/activerecord-yugabytedb-adapter.gemspec +39 -0
  10. data/lib/active_record/connection_adapters/yugabytedb/column.rb +69 -0
  11. data/lib/active_record/connection_adapters/yugabytedb/database_statements.rb +156 -0
  12. data/lib/active_record/connection_adapters/yugabytedb/database_tasks.rb +19 -0
  13. data/lib/active_record/connection_adapters/yugabytedb/explain_pretty_printer.rb +44 -0
  14. data/lib/active_record/connection_adapters/yugabytedb/oid/array.rb +91 -0
  15. data/lib/active_record/connection_adapters/yugabytedb/oid/bit.rb +53 -0
  16. data/lib/active_record/connection_adapters/yugabytedb/oid/bit_varying.rb +15 -0
  17. data/lib/active_record/connection_adapters/yugabytedb/oid/bytea.rb +17 -0
  18. data/lib/active_record/connection_adapters/yugabytedb/oid/cidr.rb +48 -0
  19. data/lib/active_record/connection_adapters/yugabytedb/oid/date.rb +31 -0
  20. data/lib/active_record/connection_adapters/yugabytedb/oid/date_time.rb +36 -0
  21. data/lib/active_record/connection_adapters/yugabytedb/oid/decimal.rb +15 -0
  22. data/lib/active_record/connection_adapters/yugabytedb/oid/enum.rb +20 -0
  23. data/lib/active_record/connection_adapters/yugabytedb/oid/hstore.rb +109 -0
  24. data/lib/active_record/connection_adapters/yugabytedb/oid/inet.rb +15 -0
  25. data/lib/active_record/connection_adapters/yugabytedb/oid/interval.rb +49 -0
  26. data/lib/active_record/connection_adapters/yugabytedb/oid/jsonb.rb +15 -0
  27. data/lib/active_record/connection_adapters/yugabytedb/oid/legacy_point.rb +44 -0
  28. data/lib/active_record/connection_adapters/yugabytedb/oid/macaddr.rb +25 -0
  29. data/lib/active_record/connection_adapters/yugabytedb/oid/money.rb +41 -0
  30. data/lib/active_record/connection_adapters/yugabytedb/oid/oid.rb +15 -0
  31. data/lib/active_record/connection_adapters/yugabytedb/oid/point.rb +64 -0
  32. data/lib/active_record/connection_adapters/yugabytedb/oid/range.rb +115 -0
  33. data/lib/active_record/connection_adapters/yugabytedb/oid/specialized_string.rb +18 -0
  34. data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp.rb +15 -0
  35. data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp_with_time_zone.rb +30 -0
  36. data/lib/active_record/connection_adapters/yugabytedb/oid/type_map_initializer.rb +125 -0
  37. data/lib/active_record/connection_adapters/yugabytedb/oid/uuid.rb +35 -0
  38. data/lib/active_record/connection_adapters/yugabytedb/oid/vector.rb +28 -0
  39. data/lib/active_record/connection_adapters/yugabytedb/oid/xml.rb +30 -0
  40. data/lib/active_record/connection_adapters/yugabytedb/oid.rb +38 -0
  41. data/lib/active_record/connection_adapters/yugabytedb/quoting.rb +205 -0
  42. data/lib/active_record/connection_adapters/yugabytedb/referential_integrity.rb +77 -0
  43. data/lib/active_record/connection_adapters/yugabytedb/schema_creation.rb +100 -0
  44. data/lib/active_record/connection_adapters/yugabytedb/schema_definitions.rb +243 -0
  45. data/lib/active_record/connection_adapters/yugabytedb/schema_dumper.rb +74 -0
  46. data/lib/active_record/connection_adapters/yugabytedb/schema_statements.rb +812 -0
  47. data/lib/active_record/connection_adapters/yugabytedb/type_metadata.rb +44 -0
  48. data/lib/active_record/connection_adapters/yugabytedb/utils.rb +80 -0
  49. data/lib/active_record/connection_adapters/yugabytedb_adapter.rb +1069 -0
  50. data/lib/activerecord-yugabytedb-adapter.rb +11 -0
  51. data/lib/arel/visitors/yugabytedb.rb +99 -0
  52. data/lib/version.rb +5 -0
  53. data/sig/activerecord-yugabytedb-adapter.rbs +4 -0
  54. metadata +124 -0
@@ -0,0 +1,812 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
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 = i.relnamespace
78
+ WHERE i.relkind IN ('i', 'I')
79
+ AND i.relname = #{index[:name]}
80
+ AND t.relname = #{table[:name]}
81
+ AND n.nspname = #{index[: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
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 = i.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
+
111
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
112
+
113
+ orders = {}
114
+ opclasses = {}
115
+
116
+ if indkey.include?(0)
117
+ columns = expressions
118
+ else
119
+ columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
120
+ SELECT a.attnum, a.attname
121
+ FROM pg_attribute a
122
+ WHERE a.attrelid = #{oid}
123
+ AND a.attnum IN (#{indkey.join(",")})
124
+ SQL
125
+
126
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
127
+ # and non-default opclasses
128
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
129
+ opclasses[column] = opclass.to_sym if opclass
130
+ if nulls
131
+ orders[column] = [desc, nulls].compact.join(" ")
132
+ else
133
+ orders[column] = :desc if desc
134
+ end
135
+ end
136
+ end
137
+
138
+ IndexDefinition.new(
139
+ table_name,
140
+ index_name,
141
+ unique,
142
+ columns,
143
+ orders: orders,
144
+ opclasses: opclasses,
145
+ where: where,
146
+ using: using.to_sym,
147
+ comment: comment.presence
148
+ )
149
+ end
150
+ end
151
+
152
+ def table_options(table_name) # :nodoc:
153
+ if comment = table_comment(table_name)
154
+ { comment: comment }
155
+ end
156
+ end
157
+
158
+ # Returns a comment stored in database for given table
159
+ def table_comment(table_name) # :nodoc:
160
+ scope = quoted_scope(table_name, type: "BASE TABLE")
161
+ if scope[:name]
162
+ query_value(<<~SQL, "SCHEMA")
163
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
164
+ FROM pg_catalog.pg_class c
165
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
166
+ WHERE c.relname = #{scope[:name]}
167
+ AND c.relkind IN (#{scope[:type]})
168
+ AND n.nspname = #{scope[:schema]}
169
+ SQL
170
+ end
171
+ end
172
+
173
+ # Returns the current database name.
174
+ def current_database
175
+ query_value("SELECT current_database()", "SCHEMA")
176
+ end
177
+
178
+ # Returns the current schema name.
179
+ def current_schema
180
+ query_value("SELECT current_schema", "SCHEMA")
181
+ end
182
+
183
+ # Returns the current database encoding format.
184
+ def encoding
185
+ query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA")
186
+ end
187
+
188
+ # Returns the current database collation.
189
+ def collation
190
+ query_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA")
191
+ end
192
+
193
+ # Returns the current database ctype.
194
+ def ctype
195
+ query_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA")
196
+ end
197
+
198
+ # Returns an array of schema names.
199
+ def schema_names
200
+ query_values(<<~SQL, "SCHEMA")
201
+ SELECT nspname
202
+ FROM pg_namespace
203
+ WHERE nspname !~ '^pg_.*'
204
+ AND nspname NOT IN ('information_schema')
205
+ ORDER by nspname;
206
+ SQL
207
+ end
208
+
209
+ # Creates a schema for the given schema name.
210
+ def create_schema(schema_name)
211
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
212
+ end
213
+
214
+ # Drops the schema for the given schema name.
215
+ def drop_schema(schema_name, **options)
216
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
217
+ end
218
+
219
+ # Sets the schema search path to a string of comma-separated schema names.
220
+ # Names beginning with $ have to be quoted (e.g. $user => '$user').
221
+ # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
222
+ #
223
+ # This should be not be called manually but set in database.yml.
224
+ def schema_search_path=(schema_csv)
225
+ if schema_csv
226
+ execute("SET search_path TO #{schema_csv}", "SCHEMA")
227
+ @schema_search_path = schema_csv
228
+ end
229
+ end
230
+
231
+ # Returns the active schema search path.
232
+ def schema_search_path
233
+ @schema_search_path ||= query_value("SHOW search_path", "SCHEMA")
234
+ end
235
+
236
+ # Returns the current client message level.
237
+ def client_min_messages
238
+ query_value("SHOW client_min_messages", "SCHEMA")
239
+ end
240
+
241
+ # Set the client message level.
242
+ def client_min_messages=(level)
243
+ execute("SET client_min_messages TO '#{level}'", "SCHEMA")
244
+ end
245
+
246
+ # Returns the sequence name for a table's primary key or some other specified key.
247
+ def default_sequence_name(table_name, pk = "id") # :nodoc:
248
+ result = serial_sequence(table_name, pk)
249
+ return nil unless result
250
+ Utils.extract_schema_qualified_name(result).to_s
251
+ rescue ActiveRecord::StatementInvalid
252
+ YugabyteDB::Name.new(nil, "#{table_name}_#{pk}_seq").to_s
253
+ end
254
+
255
+ def serial_sequence(table, column)
256
+ query_value("SELECT pg_get_serial_sequence(#{quote(table)}, #{quote(column)})", "SCHEMA")
257
+ end
258
+
259
+ # Sets the sequence of a table's primary key to the specified value.
260
+ def set_pk_sequence!(table, value) # :nodoc:
261
+ pk, sequence = pk_and_sequence_for(table)
262
+
263
+ if pk
264
+ if sequence
265
+ quoted_sequence = quote_table_name(sequence)
266
+
267
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
268
+ else
269
+ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
270
+ end
271
+ end
272
+ end
273
+
274
+ # Resets the sequence of a table's primary key to the maximum value.
275
+ def reset_pk_sequence!(table, pk = nil, sequence = nil) # :nodoc:
276
+ unless pk && sequence
277
+ default_pk, default_sequence = pk_and_sequence_for(table)
278
+
279
+ pk ||= default_pk
280
+ sequence ||= default_sequence
281
+ end
282
+
283
+ if @logger && pk && !sequence
284
+ @logger.warn "#{table} has primary key #{pk} with no default sequence."
285
+ end
286
+
287
+ if pk && sequence
288
+ quoted_sequence = quote_table_name(sequence)
289
+ max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
290
+ if max_pk.nil?
291
+ if database_version >= 100000
292
+ minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
293
+ else
294
+ minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
295
+ end
296
+ end
297
+
298
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
299
+ end
300
+ end
301
+
302
+ # Returns a table's primary key and belonging sequence.
303
+ def pk_and_sequence_for(table) # :nodoc:
304
+ # First try looking for a sequence with a dependency on the
305
+ # given table's primary key.
306
+ result = query(<<~SQL, "SCHEMA")[0]
307
+ SELECT attr.attname, nsp.nspname, seq.relname
308
+ FROM pg_class seq,
309
+ pg_attribute attr,
310
+ pg_depend dep,
311
+ pg_constraint cons,
312
+ pg_namespace nsp
313
+ WHERE seq.oid = dep.objid
314
+ AND seq.relkind = 'S'
315
+ AND attr.attrelid = dep.refobjid
316
+ AND attr.attnum = dep.refobjsubid
317
+ AND attr.attrelid = cons.conrelid
318
+ AND attr.attnum = cons.conkey[1]
319
+ AND seq.relnamespace = nsp.oid
320
+ AND cons.contype = 'p'
321
+ AND dep.classid = 'pg_class'::regclass
322
+ AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
323
+ SQL
324
+
325
+ if result.nil? || result.empty?
326
+ result = query(<<~SQL, "SCHEMA")[0]
327
+ SELECT attr.attname, nsp.nspname,
328
+ CASE
329
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
330
+ WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
331
+ substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
332
+ strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
333
+ ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
334
+ END
335
+ FROM pg_class t
336
+ JOIN pg_attribute attr ON (t.oid = attrelid)
337
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
338
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
339
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
340
+ WHERE t.oid = #{quote(quote_table_name(table))}::regclass
341
+ AND cons.contype = 'p'
342
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
343
+ SQL
344
+ end
345
+
346
+ pk = result.shift
347
+ if result.last
348
+ [pk, YugabyteDB::Name.new(*result)]
349
+ else
350
+ [pk, nil]
351
+ end
352
+ rescue
353
+ nil
354
+ end
355
+
356
+ def primary_keys(table_name) # :nodoc:
357
+ query_values(<<~SQL, "SCHEMA")
358
+ SELECT a.attname
359
+ FROM (
360
+ SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
361
+ FROM pg_index
362
+ WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
363
+ AND indisprimary
364
+ ) i
365
+ JOIN pg_attribute a
366
+ ON a.attrelid = i.indrelid
367
+ AND a.attnum = i.indkey[i.idx]
368
+ ORDER BY i.idx
369
+ SQL
370
+ end
371
+
372
+ # Renames a table.
373
+ # Also renames a table's primary key sequence if the sequence name exists and
374
+ # matches the Active Record default.
375
+ #
376
+ # Example:
377
+ # rename_table('octopuses', 'octopi')
378
+ def rename_table(table_name, new_name)
379
+ clear_cache!
380
+ schema_cache.clear_data_source_cache!(table_name.to_s)
381
+ schema_cache.clear_data_source_cache!(new_name.to_s)
382
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
383
+ pk, seq = pk_and_sequence_for(new_name)
384
+ if pk
385
+ idx = "#{table_name}_pkey"
386
+ new_idx = "#{new_name}_pkey"
387
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
388
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
389
+ new_seq = "#{new_name}_#{pk}_seq"
390
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
391
+ end
392
+ end
393
+ rename_table_indexes(table_name, new_name)
394
+ end
395
+
396
+ def add_column(table_name, column_name, type, **options) # :nodoc:
397
+ clear_cache!
398
+ super
399
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
400
+ end
401
+
402
+ def change_column(table_name, column_name, type, **options) # :nodoc:
403
+ clear_cache!
404
+ sqls, procs = Array(change_column_for_alter(table_name, column_name, type, **options)).partition { |v| v.is_a?(String) }
405
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
406
+ procs.each(&:call)
407
+ end
408
+
409
+ # Changes the default value of a table column.
410
+ def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
411
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
412
+ end
413
+
414
+ def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
415
+ clear_cache!
416
+ unless null || default.nil?
417
+ column = column_for(table_name, column_name)
418
+ 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
419
+ end
420
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
421
+ end
422
+
423
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
424
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
425
+ clear_cache!
426
+ comment = extract_new_comment_value(comment_or_changes)
427
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
428
+ end
429
+
430
+ # Adds comment for given table or drops it if +comment+ is a +nil+
431
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
432
+ clear_cache!
433
+ comment = extract_new_comment_value(comment_or_changes)
434
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
435
+ end
436
+
437
+ # Renames a column in a table.
438
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
439
+ clear_cache!
440
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
441
+ rename_column_indexes(table_name, column_name, new_column_name)
442
+ end
443
+
444
+ def add_index(table_name, column_name, **options) # :nodoc:
445
+ index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
446
+
447
+ create_index = CreateIndexDefinition.new(index, algorithm, if_not_exists)
448
+ result = execute schema_creation.accept(create_index)
449
+
450
+ execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
451
+ result
452
+ end
453
+
454
+ def remove_index(table_name, column_name = nil, **options) # :nodoc:
455
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
456
+
457
+ if options.key?(:name)
458
+ provided_index = Utils.extract_schema_qualified_name(options[:name].to_s)
459
+
460
+ options[:name] = provided_index.identifier
461
+ table = YugabyteDB::Name.new(provided_index.schema, table.identifier) unless table.schema.present?
462
+
463
+ if provided_index.schema.present? && table.schema != provided_index.schema
464
+ raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'")
465
+ end
466
+ end
467
+
468
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
469
+
470
+ index_to_remove = YugabyteDB::Name.new(table.schema, index_name_for_remove(table.to_s, column_name, options))
471
+
472
+ execute "DROP INDEX #{index_algorithm(options[:algorithm])} #{quote_table_name(index_to_remove)}"
473
+ end
474
+
475
+ # Renames an index of a table. Raises error if length of new
476
+ # index name is greater than allowed limit.
477
+ def rename_index(table_name, old_name, new_name)
478
+ validate_index_length!(table_name, new_name)
479
+
480
+ execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
481
+ end
482
+
483
+ def foreign_keys(table_name)
484
+ scope = quoted_scope(table_name)
485
+ fk_info = exec_query(<<~SQL, "SCHEMA")
486
+ 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
487
+ FROM pg_constraint c
488
+ JOIN pg_class t1 ON c.conrelid = t1.oid
489
+ JOIN pg_class t2 ON c.confrelid = t2.oid
490
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
491
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
492
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
493
+ WHERE c.contype = 'f'
494
+ AND t1.relname = #{scope[:name]}
495
+ AND t3.nspname = #{scope[:schema]}
496
+ ORDER BY c.conname
497
+ SQL
498
+
499
+ fk_info.map do |row|
500
+ options = {
501
+ column: row["column"],
502
+ name: row["name"],
503
+ primary_key: row["primary_key"]
504
+ }
505
+
506
+ options[:on_delete] = extract_foreign_key_action(row["on_delete"])
507
+ options[:on_update] = extract_foreign_key_action(row["on_update"])
508
+ options[:deferrable] = extract_foreign_key_deferrable(row["deferrable"], row["deferred"])
509
+
510
+ options[:validate] = row["valid"]
511
+
512
+ ForeignKeyDefinition.new(table_name, row["to_table"], options)
513
+ end
514
+ end
515
+
516
+ def foreign_tables
517
+ query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
518
+ end
519
+
520
+ def foreign_table_exists?(table_name)
521
+ query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
522
+ end
523
+
524
+ def check_constraints(table_name) # :nodoc:
525
+ scope = quoted_scope(table_name)
526
+
527
+ check_info = exec_query(<<-SQL, "SCHEMA")
528
+ SELECT conname, pg_get_constraintdef(c.oid, true) AS constraintdef, c.convalidated AS valid
529
+ FROM pg_constraint c
530
+ JOIN pg_class t ON c.conrelid = t.oid
531
+ JOIN pg_namespace n ON n.oid = c.connamespace
532
+ WHERE c.contype = 'c'
533
+ AND t.relname = #{scope[:name]}
534
+ AND n.nspname = #{scope[:schema]}
535
+ SQL
536
+
537
+ check_info.map do |row|
538
+ options = {
539
+ name: row["conname"],
540
+ validate: row["valid"]
541
+ }
542
+ expression = row["constraintdef"][/CHECK \((.+)\)/m, 1]
543
+
544
+ CheckConstraintDefinition.new(table_name, expression, options)
545
+ end
546
+ end
547
+
548
+ # Maps logical Rails types to PostgreSQL-specific data types.
549
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, enum_type: nil, **) # :nodoc:
550
+ sql = \
551
+ case type.to_s
552
+ when "binary"
553
+ # PostgreSQL doesn't support limits on binary (bytea) columns.
554
+ # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
555
+ case limit
556
+ when nil, 0..0x3fffffff; super(type)
557
+ else raise ArgumentError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
558
+ end
559
+ when "text"
560
+ # PostgreSQL doesn't support limits on text columns.
561
+ # The hard limit is 1GB, according to section 8.3 in the manual.
562
+ case limit
563
+ when nil, 0..0x3fffffff; super(type)
564
+ else raise ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
565
+ end
566
+ when "integer"
567
+ case limit
568
+ when 1, 2; "smallint"
569
+ when nil, 3, 4; "integer"
570
+ when 5..8; "bigint"
571
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
572
+ end
573
+ when "enum"
574
+ raise ArgumentError, "enum_type is required for enums" if enum_type.nil?
575
+
576
+ enum_type
577
+ else
578
+ super
579
+ end
580
+
581
+ sql = "#{sql}[]" if array && type != :primary_key
582
+ sql
583
+ end
584
+
585
+ # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
586
+ # requires that the ORDER BY include the distinct column.
587
+ def columns_for_distinct(columns, orders) # :nodoc:
588
+ order_columns = orders.compact_blank.map { |s|
589
+ # Convert Arel node to string
590
+ s = visitor.compile(s) unless s.is_a?(String)
591
+ # Remove any ASC/DESC modifiers
592
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
593
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
594
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
595
+
596
+ (order_columns << super).join(", ")
597
+ end
598
+
599
+ def update_table_definition(table_name, base) # :nodoc:
600
+ YugabyteDB::Table.new(table_name, base)
601
+ end
602
+
603
+ def create_schema_dumper(options) # :nodoc:
604
+ YugabyteDB::SchemaDumper.create(self, options)
605
+ end
606
+
607
+ # Validates the given constraint.
608
+ #
609
+ # Validates the constraint named +constraint_name+ on +accounts+.
610
+ #
611
+ # validate_constraint :accounts, :constraint_name
612
+ def validate_constraint(table_name, constraint_name)
613
+ at = create_alter_table table_name
614
+ at.validate_constraint constraint_name
615
+
616
+ execute schema_creation.accept(at)
617
+ end
618
+
619
+ # Validates the given foreign key.
620
+ #
621
+ # Validates the foreign key on +accounts.branch_id+.
622
+ #
623
+ # validate_foreign_key :accounts, :branches
624
+ #
625
+ # Validates the foreign key on +accounts.owner_id+.
626
+ #
627
+ # validate_foreign_key :accounts, column: :owner_id
628
+ #
629
+ # Validates the foreign key named +special_fk_name+ on the +accounts+ table.
630
+ #
631
+ # validate_foreign_key :accounts, name: :special_fk_name
632
+ #
633
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
634
+ def validate_foreign_key(from_table, to_table = nil, **options)
635
+ fk_name_to_validate = foreign_key_for!(from_table, to_table: to_table, **options).name
636
+
637
+ validate_constraint from_table, fk_name_to_validate
638
+ end
639
+
640
+ # Validates the given check constraint.
641
+ #
642
+ # validate_check_constraint :products, name: "price_check"
643
+ #
644
+ # The +options+ hash accepts the same keys as add_check_constraint[rdoc-ref:ConnectionAdapters::SchemaStatements#add_check_constraint].
645
+ def validate_check_constraint(table_name, **options)
646
+ chk_name_to_validate = check_constraint_for!(table_name, **options).name
647
+
648
+ validate_constraint table_name, chk_name_to_validate
649
+ end
650
+
651
+ private
652
+ def schema_creation
653
+ YugabyteDB::SchemaCreation.new(self)
654
+ end
655
+
656
+ def create_table_definition(name, **options)
657
+ YugabyteDB::TableDefinition.new(self, name, **options)
658
+ end
659
+
660
+ def create_alter_table(name)
661
+ YugabyteDB::AlterTable.new create_table_definition(name)
662
+ end
663
+
664
+ def new_column_from_field(table_name, field)
665
+ column_name, type, default, notnull, oid, fmod, collation, comment, attgenerated = field
666
+ type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
667
+ default_value = extract_value_from_default(default)
668
+
669
+ if attgenerated.present?
670
+ default_function = default
671
+ else
672
+ default_function = extract_default_function(default_value, default)
673
+ end
674
+
675
+ if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
676
+ serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
677
+ end
678
+
679
+ YugabyteDB::Column.new(
680
+ column_name,
681
+ default_value,
682
+ type_metadata,
683
+ !notnull,
684
+ default_function,
685
+ collation: collation,
686
+ comment: comment.presence,
687
+ serial: serial,
688
+ generated: attgenerated
689
+ )
690
+ end
691
+
692
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
693
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
694
+ simple_type = SqlTypeMetadata.new(
695
+ sql_type: sql_type,
696
+ type: cast_type.type,
697
+ limit: cast_type.limit,
698
+ precision: cast_type.precision,
699
+ scale: cast_type.scale,
700
+ )
701
+ YugabyteDB::TypeMetadata.new(simple_type, oid: oid, fmod: fmod)
702
+ end
703
+
704
+ def sequence_name_from_parts(table_name, column_name, suffix)
705
+ over_length = [table_name, column_name, suffix].sum(&:length) + 2 - max_identifier_length
706
+
707
+ if over_length > 0
708
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
709
+ over_length -= column_name.length - column_name_length
710
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
711
+ end
712
+
713
+ if over_length > 0
714
+ table_name = table_name[0, table_name.length - over_length]
715
+ end
716
+
717
+ "#{table_name}_#{column_name}_#{suffix}"
718
+ end
719
+
720
+ def extract_foreign_key_action(specifier)
721
+ case specifier
722
+ when "c"; :cascade
723
+ when "n"; :nullify
724
+ when "r"; :restrict
725
+ end
726
+ end
727
+
728
+ def extract_foreign_key_deferrable(deferrable, deferred)
729
+ deferrable && (deferred ? :deferred : true)
730
+ end
731
+
732
+ def add_column_for_alter(table_name, column_name, type, **options)
733
+ return super unless options.key?(:comment)
734
+ [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
735
+ end
736
+
737
+ def change_column_for_alter(table_name, column_name, type, **options)
738
+ td = create_table_definition(table_name)
739
+ cd = td.new_column_definition(column_name, type, **options)
740
+ sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
741
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
742
+ sqls
743
+ end
744
+
745
+ def change_column_default_for_alter(table_name, column_name, default_or_changes)
746
+ column = column_for(table_name, column_name)
747
+ return unless column
748
+
749
+ default = extract_new_default_value(default_or_changes)
750
+ alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
751
+ if default.nil?
752
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
753
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
754
+ alter_column_query % "DROP DEFAULT"
755
+ else
756
+ alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
757
+ end
758
+ end
759
+
760
+ def change_column_null_for_alter(table_name, column_name, null, default = nil)
761
+ "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
762
+ end
763
+
764
+ def add_index_opclass(quoted_columns, **options)
765
+ opclasses = options_for_index_columns(options[:opclass])
766
+ quoted_columns.each do |name, column|
767
+ column << " #{opclasses[name]}" if opclasses[name].present?
768
+ end
769
+ end
770
+
771
+ def add_options_for_index_columns(quoted_columns, **options)
772
+ quoted_columns = add_index_opclass(quoted_columns, **options)
773
+ super
774
+ end
775
+
776
+ def data_source_sql(name = nil, type: nil)
777
+ scope = quoted_scope(name, type: type)
778
+ scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
779
+
780
+ sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
781
+ sql << " WHERE n.nspname = #{scope[:schema]}"
782
+ sql << " AND c.relname = #{scope[:name]}" if scope[:name]
783
+ sql << " AND c.relkind IN (#{scope[:type]})"
784
+ sql
785
+ end
786
+
787
+ def quoted_scope(name = nil, type: nil)
788
+ schema, name = extract_schema_qualified_name(name)
789
+ type = \
790
+ case type
791
+ when "BASE TABLE"
792
+ "'r','p'"
793
+ when "VIEW"
794
+ "'v','m'"
795
+ when "FOREIGN TABLE"
796
+ "'f'"
797
+ end
798
+ scope = {}
799
+ scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
800
+ scope[:name] = quote(name) if name
801
+ scope[:type] = type if type
802
+ scope
803
+ end
804
+
805
+ def extract_schema_qualified_name(string)
806
+ name = Utils.extract_schema_qualified_name(string.to_s)
807
+ [name.schema, name.identifier]
808
+ end
809
+ end
810
+ end
811
+ end
812
+ end