activerecord-yugabytedb-adapter 7.0.4.1 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "yugabyte_ysql", "~> 0.3"
4
- require "yugabyte_ysql"
3
+ gem "yugabytedb-ysql", "~> 0.3"
4
+ require "ysql"
5
5
 
6
6
  require_relative "../../arel/visitors/yugabytedb"
7
7
  require "active_support/core_ext/object/try"
@@ -22,28 +22,19 @@ require "active_record/connection_adapters/yugabytedb/utils"
22
22
 
23
23
  module ActiveRecord
24
24
  module ConnectionHandling # :nodoc:
25
+ def yugabytedb_adapter_class
26
+ ConnectionAdapters::YugabyteDBAdapter
27
+ end
28
+
25
29
  # Establishes a connection to the database that's used by all Active Record objects
26
30
  def yugabytedb_connection(config)
27
- conn_params = config.symbolize_keys.compact
28
-
29
- # Map ActiveRecords param names to PGs.
30
- conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
31
- conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
32
-
33
- # Forward only valid config params to PG::Connection.connect.
34
- valid_conn_param_keys = YugabyteYSQL::Connection.conndefaults_hash.keys + [:requiressl]
35
- conn_params.slice!(*valid_conn_param_keys)
36
-
37
- ConnectionAdapters::YugabyteDBAdapter.new(
38
- ConnectionAdapters::YugabyteDBAdapter.new_client(conn_params),
39
- logger,
40
- conn_params,
41
- config,
42
- )
31
+ yugabytedb_adapter_class.new(config)
43
32
  end
44
33
  end
45
34
 
46
35
  module ConnectionAdapters
36
+ # = Active Record YugabyteDB Adapter
37
+ #
47
38
  # The PostgreSQL adapter works with the native C (https://github.com/ged/ruby-pg) driver.
48
39
  #
49
40
  # Options:
@@ -76,18 +67,39 @@ module ActiveRecord
76
67
 
77
68
  class << self
78
69
  def new_client(conn_params)
79
- YugabyteYSQL.connect(**conn_params)
80
- rescue ::YugabyteYSQL::Error => error
81
- if conn_params && conn_params[:dbname] && error.message.include?(conn_params[:dbname])
70
+ YSQL.connect(**conn_params)
71
+ rescue ::YSQL::Error => error
72
+ if conn_params && conn_params[:dbname] == "postgres"
73
+ raise ActiveRecord::ConnectionNotEstablished, error.message
74
+ elsif conn_params && conn_params[:dbname] && error.message.include?(conn_params[:dbname])
82
75
  raise ActiveRecord::NoDatabaseError.db_error(conn_params[:dbname])
83
76
  elsif conn_params && conn_params[:user] && error.message.include?(conn_params[:user])
84
77
  raise ActiveRecord::DatabaseConnectionError.username_error(conn_params[:user])
85
- elsif conn_params && conn_params[:hostname] && error.message.include?(conn_params[:hostname])
86
- raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:hostname])
78
+ elsif conn_params && conn_params[:host] && error.message.include?(conn_params[:host])
79
+ raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:host])
87
80
  else
88
81
  raise ActiveRecord::ConnectionNotEstablished, error.message
89
82
  end
90
83
  end
84
+
85
+ def dbconsole(config, options = {})
86
+ pg_config = config.configuration_hash
87
+
88
+ ENV["PGUSER"] = pg_config[:username] if pg_config[:username]
89
+ ENV["PGHOST"] = pg_config[:host] if pg_config[:host]
90
+ ENV["PGPORT"] = pg_config[:port].to_s if pg_config[:port]
91
+ ENV["PGPASSWORD"] = pg_config[:password].to_s if pg_config[:password] && options[:include_password]
92
+ ENV["PGSSLMODE"] = pg_config[:sslmode].to_s if pg_config[:sslmode]
93
+ ENV["PGSSLCERT"] = pg_config[:sslcert].to_s if pg_config[:sslcert]
94
+ ENV["PGSSLKEY"] = pg_config[:sslkey].to_s if pg_config[:sslkey]
95
+ ENV["PGSSLROOTCERT"] = pg_config[:sslrootcert].to_s if pg_config[:sslrootcert]
96
+ if pg_config[:variables]
97
+ ENV["PGOPTIONS"] = pg_config[:variables].filter_map do |name, value|
98
+ "-c #{name}=#{value.to_s.gsub(/[ \\]/, '\\\\\0')}" unless value == ":default" || value == :default
99
+ end.join(" ")
100
+ end
101
+ find_cmd_and_exec("psql", config.database)
102
+ end
91
103
  end
92
104
 
93
105
  ##
@@ -97,10 +109,11 @@ module ActiveRecord
97
109
  # but significantly increases the risk of data loss if the database
98
110
  # crashes. As a result, this should not be used in production
99
111
  # environments. If you would like all created tables to be unlogged in
100
- # the test environment you can add the following line to your test.rb
101
- # file:
112
+ # the test environment you can add the following to your test.rb file:
102
113
  #
103
- # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
114
+ # ActiveSupport.on_load(:active_record_postgresqladapter) do
115
+ # self.create_unlogged_tables = true
116
+ # end
104
117
  class_attribute :create_unlogged_tables, default: false
105
118
 
106
119
  ##
@@ -184,13 +197,17 @@ module ActiveRecord
184
197
  end
185
198
 
186
199
  def supports_partitioned_indexes?
187
- database_version >= 110_000 # >= 11.0
200
+ database_version >= 11_00_00 # >= 11.0
188
201
  end
189
202
 
190
203
  def supports_partial_index?
191
204
  true
192
205
  end
193
206
 
207
+ def supports_index_include?
208
+ database_version >= 11_00_00 # >= 11.0
209
+ end
210
+
194
211
  def supports_expression_index?
195
212
  true
196
213
  end
@@ -207,6 +224,14 @@ module ActiveRecord
207
224
  true
208
225
  end
209
226
 
227
+ def supports_exclusion_constraints?
228
+ true
229
+ end
230
+
231
+ def supports_unique_constraints?
232
+ true
233
+ end
234
+
210
235
  def supports_validate_constraints?
211
236
  true
212
237
  end
@@ -235,25 +260,41 @@ module ActiveRecord
235
260
  true
236
261
  end
237
262
 
263
+ def supports_restart_db_transaction?
264
+ database_version >= 12_00_00 # >= 12.0
265
+ end
266
+
238
267
  def supports_insert_returning?
239
268
  true
240
269
  end
241
270
 
242
271
  def supports_insert_on_conflict?
243
- database_version >= 90500 # >= 9.5
272
+ database_version >= 9_05_00 # >= 9.5
244
273
  end
245
274
  alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
246
275
  alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
247
276
  alias supports_insert_conflict_target? supports_insert_on_conflict?
248
277
 
249
278
  def supports_virtual_columns?
250
- database_version >= 120_000 # >= 12.0
279
+ database_version >= 12_00_00 # >= 12.0
280
+ end
281
+
282
+ def supports_identity_columns? # :nodoc:
283
+ database_version >= 10_00_00 # >= 10.0
284
+ end
285
+
286
+ def supports_nulls_not_distinct?
287
+ database_version >= 15_00_00 # >= 15.0
251
288
  end
252
289
 
253
290
  def index_algorithms
254
291
  { concurrently: "CONCURRENTLY" }
255
292
  end
256
293
 
294
+ def return_value_after_insert?(column) # :nodoc:
295
+ column.auto_populated?
296
+ end
297
+
257
298
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
258
299
  def initialize(connection, max)
259
300
  super(max)
@@ -266,80 +307,75 @@ module ActiveRecord
266
307
  end
267
308
 
268
309
  private
269
- def dealloc(key)
270
- @connection.query "DEALLOCATE #{key}" if connection_active?
271
- rescue YugabyteYSQL::Error
272
- end
273
-
274
- def connection_active?
275
- @connection.status == YugabyteYSQL::CONNECTION_OK
276
- rescue YugabyteYSQL::Error
277
- false
278
- end
310
+ def dealloc(key)
311
+ # This is ugly, but safe: the statement pool is only
312
+ # accessed while holding the connection's lock. (And we
313
+ # don't need the complication of with_raw_connection because
314
+ # a reconnect would invalidate the entire statement pool.)
315
+ if conn = @connection.instance_variable_get(:@raw_connection)
316
+ conn.query "DEALLOCATE #{key}" if conn.status == YSQL::CONNECTION_OK
317
+ end
318
+ rescue YSQL::Error
319
+ end
279
320
  end
280
321
 
281
322
  # Initializes and connects a YugabyteDB adapter.
282
- def initialize(connection, logger, connection_parameters, config)
283
- super(connection, logger, config)
323
+ def initialize(...)
324
+ super
284
325
 
285
- @connection_parameters = connection_parameters || {}
326
+ conn_params = @config.compact
286
327
 
287
- # @local_tz is initialized as nil to avoid warnings when connect tries to use it
288
- @local_tz = nil
289
- @max_identifier_length = nil
328
+ # Map ActiveRecords param names to PGs.
329
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
330
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
290
331
 
291
- configure_connection
292
- add_pg_encoders
293
- add_pg_decoders
332
+ # Forward only valid config params to PG::Connection.connect.
333
+ valid_conn_param_keys = YSQL::Connection.conndefaults_hash.keys + [:requiressl]
334
+ conn_params.slice!(*valid_conn_param_keys)
294
335
 
295
- @type_map = Type::HashLookupTypeMap.new
296
- initialize_type_map
297
- @local_tz = execute("SHOW TIME ZONE", "SCHEMA").first["TimeZone"]
298
- @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
299
- end
336
+ @connection_parameters = conn_params
300
337
 
301
- def self.database_exists?(config)
302
- !!ActiveRecord::Base.yugabytedb_connection(config)
303
- rescue ActiveRecord::NoDatabaseError
304
- false
338
+ @max_identifier_length = nil
339
+ @type_map = nil
340
+ @raw_connection = nil
341
+ @notice_receiver_sql_warnings = []
342
+
343
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
305
344
  end
306
345
 
307
346
  # Is this connection alive and ready for queries?
308
347
  def active?
309
348
  @lock.synchronize do
310
- @connection.query ";"
349
+ return false unless @raw_connection
350
+ @raw_connection.query ";"
311
351
  end
312
352
  true
313
- rescue YugabyteYSQL::Error
353
+ rescue YSQL::Error
314
354
  false
315
355
  end
316
356
 
317
357
  def reload_type_map # :nodoc:
318
- type_map.clear
319
- initialize_type_map
320
- end
321
-
322
- # Close then reopen the connection.
323
- def reconnect!
324
358
  @lock.synchronize do
325
- super
326
- @connection.reset
327
- configure_connection
328
- reload_type_map
329
- rescue YugabyteYSQL::ConnectionBad
330
- connect
359
+ if @type_map
360
+ type_map.clear
361
+ else
362
+ @type_map = Type::HashLookupTypeMap.new
363
+ end
364
+
365
+ initialize_type_map
331
366
  end
332
367
  end
333
368
 
334
369
  def reset!
335
370
  @lock.synchronize do
336
- clear_cache!
337
- reset_transaction
338
- unless @connection.transaction_status == ::YugabyteYSQL::PQTRANS_IDLE
339
- @connection.query "ROLLBACK"
371
+ return connect! unless @raw_connection
372
+
373
+ unless @raw_connection.transaction_status == ::YSQL::PQTRANS_IDLE
374
+ @raw_connection.query "ROLLBACK"
340
375
  end
341
- @connection.query "DISCARD ALL"
342
- configure_connection
376
+ @raw_connection.query "DISCARD ALL"
377
+
378
+ super
343
379
  end
344
380
  end
345
381
 
@@ -348,14 +384,15 @@ module ActiveRecord
348
384
  def disconnect!
349
385
  @lock.synchronize do
350
386
  super
351
- @connection.close rescue nil
387
+ @raw_connection&.close rescue nil
388
+ @raw_connection = nil
352
389
  end
353
390
  end
354
391
 
355
392
  def discard! # :nodoc:
356
393
  super
357
- @connection.socket_io.reopen(IO::NULL) rescue nil
358
- @connection = nil
394
+ @raw_connection&.socket_io&.reopen(IO::NULL) rescue nil
395
+ @raw_connection = nil
359
396
  end
360
397
 
361
398
  def native_database_types # :nodoc:
@@ -364,14 +401,14 @@ module ActiveRecord
364
401
 
365
402
  def self.native_database_types # :nodoc:
366
403
  @native_database_types ||= begin
367
- types = NATIVE_DATABASE_TYPES.dup
368
- types[:datetime] = types[datetime_type]
369
- types
370
- end
404
+ types = NATIVE_DATABASE_TYPES.dup
405
+ types[:datetime] = types[datetime_type]
406
+ types
407
+ end
371
408
  end
372
409
 
373
410
  def set_standard_conforming_strings
374
- execute("SET standard_conforming_strings = on", "SCHEMA")
411
+ internal_execute("SET standard_conforming_strings = on")
375
412
  end
376
413
 
377
414
  def supports_ddl_transactions?
@@ -399,7 +436,7 @@ module ActiveRecord
399
436
  end
400
437
 
401
438
  def supports_pgcrypto_uuid?
402
- database_version >= 90400 # >= 9.4
439
+ database_version >= 9_04_00 # >= 9.4
403
440
  end
404
441
 
405
442
  def supports_optimizer_hints?
@@ -431,14 +468,21 @@ module ActiveRecord
431
468
  query_value("SELECT pg_advisory_unlock(#{lock_id})")
432
469
  end
433
470
 
434
- def enable_extension(name)
435
- exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
436
- reload_type_map
437
- }
471
+ def enable_extension(name, **)
472
+ schema, name = name.to_s.split(".").values_at(-2, -1)
473
+ sql = +"CREATE EXTENSION IF NOT EXISTS \"#{name}\""
474
+ sql << " SCHEMA #{schema}" if schema
475
+
476
+ internal_exec_query(sql).tap { reload_type_map }
438
477
  end
439
478
 
440
- def disable_extension(name)
441
- exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap {
479
+ # Removes an extension from the database.
480
+ #
481
+ # [<tt>:force</tt>]
482
+ # Set to +:cascade+ to drop dependent objects as well.
483
+ # Defaults to false.
484
+ def disable_extension(name, force: false)
485
+ internal_exec_query("DROP EXTENSION IF EXISTS \"#{name}\"#{' CASCADE' if force == :cascade}").tap {
442
486
  reload_type_map
443
487
  }
444
488
  end
@@ -452,7 +496,7 @@ module ActiveRecord
452
496
  end
453
497
 
454
498
  def extensions
455
- exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
499
+ internal_exec_query("SELECT extname FROM pg_extension", "SCHEMA", allow_retry: true, materialize_transactions: false).cast_values
456
500
  end
457
501
 
458
502
  # Returns a list of defined enum types, and their values.
@@ -460,31 +504,97 @@ module ActiveRecord
460
504
  query = <<~SQL
461
505
  SELECT
462
506
  type.typname AS name,
507
+ type.OID AS oid,
508
+ n.nspname AS schema,
463
509
  string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
464
510
  FROM pg_enum AS enum
465
- JOIN pg_type AS type
466
- ON (type.oid = enum.enumtypid)
467
- GROUP BY type.typname;
511
+ JOIN pg_type AS type ON (type.oid = enum.enumtypid)
512
+ JOIN pg_namespace n ON type.typnamespace = n.oid
513
+ WHERE n.nspname = ANY (current_schemas(false))
514
+ GROUP BY type.OID, n.nspname, type.typname;
468
515
  SQL
469
- exec_query(query, "SCHEMA").cast_values
516
+
517
+ internal_exec_query(query, "SCHEMA", allow_retry: true, materialize_transactions: false).cast_values.each_with_object({}) do |row, memo|
518
+ name, schema = row[0], row[2]
519
+ schema = nil if schema == current_schema
520
+ full_name = [schema, name].compact.join(".")
521
+ memo[full_name] = row.last
522
+ end.to_a
470
523
  end
471
524
 
472
525
  # Given a name and an array of values, creates an enum type.
473
- def create_enum(name, values)
474
- sql_values = values.map { |s| "'#{s}'" }.join(", ")
526
+ def create_enum(name, values, **options)
527
+ sql_values = values.map { |s| quote(s) }.join(", ")
528
+ scope = quoted_scope(name)
475
529
  query = <<~SQL
476
530
  DO $$
477
531
  BEGIN
478
532
  IF NOT EXISTS (
479
- SELECT 1 FROM pg_type t
480
- WHERE t.typname = '#{name}'
533
+ SELECT 1
534
+ FROM pg_type t
535
+ JOIN pg_namespace n ON t.typnamespace = n.oid
536
+ WHERE t.typname = #{scope[:name]}
537
+ AND n.nspname = #{scope[:schema]}
481
538
  ) THEN
482
- CREATE TYPE \"#{name}\" AS ENUM (#{sql_values});
539
+ CREATE TYPE #{quote_table_name(name)} AS ENUM (#{sql_values});
483
540
  END IF;
484
541
  END
485
542
  $$;
486
543
  SQL
487
- exec_query(query)
544
+ internal_exec_query(query).tap { reload_type_map }
545
+ end
546
+
547
+ # Drops an enum type.
548
+ #
549
+ # If the <tt>if_exists: true</tt> option is provided, the enum is dropped
550
+ # only if it exists. Otherwise, if the enum doesn't exist, an error is
551
+ # raised.
552
+ #
553
+ # The +values+ parameter will be ignored if present. It can be helpful
554
+ # to provide this in a migration's +change+ method so it can be reverted.
555
+ # In that case, +values+ will be used by #create_enum.
556
+ def drop_enum(name, values = nil, **options)
557
+ query = <<~SQL
558
+ DROP TYPE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(name)};
559
+ SQL
560
+ internal_exec_query(query).tap { reload_type_map }
561
+ end
562
+
563
+ # Rename an existing enum type to something else.
564
+ def rename_enum(name, options = {})
565
+ to = options.fetch(:to) { raise ArgumentError, ":to is required" }
566
+
567
+ exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{to}").tap { reload_type_map }
568
+ end
569
+
570
+ # Add enum value to an existing enum type.
571
+ def add_enum_value(type_name, value, options = {})
572
+ before, after = options.values_at(:before, :after)
573
+ sql = +"ALTER TYPE #{quote_table_name(type_name)} ADD VALUE '#{value}'"
574
+
575
+ if before && after
576
+ raise ArgumentError, "Cannot have both :before and :after at the same time"
577
+ elsif before
578
+ sql << " BEFORE '#{before}'"
579
+ elsif after
580
+ sql << " AFTER '#{after}'"
581
+ end
582
+
583
+ execute(sql).tap { reload_type_map }
584
+ end
585
+
586
+ # Rename enum value on an existing enum type.
587
+ def rename_enum_value(type_name, options = {})
588
+ unless database_version >= 10_00_00 # >= 10.0
589
+ raise ArgumentError, "Renaming enum values is only supported in PostgreSQL 10 or later"
590
+ end
591
+
592
+ from = options.fetch(:from) { raise ArgumentError, ":from is required" }
593
+ to = options.fetch(:to) { raise ArgumentError, ":to is required" }
594
+
595
+ execute("ALTER TYPE #{quote_table_name(type_name)} RENAME VALUE '#{from}' TO '#{to}'").tap {
596
+ reload_type_map
597
+ }
488
598
  end
489
599
 
490
600
  # Returns the configured supported identifier length supported by PostgreSQL
@@ -495,16 +605,16 @@ module ActiveRecord
495
605
  # Set the authorized user for this session
496
606
  def session_auth=(user)
497
607
  clear_cache!
498
- execute("SET SESSION AUTHORIZATION #{user}")
608
+ internal_execute("SET SESSION AUTHORIZATION #{user}", nil, materialize_transactions: true)
499
609
  end
500
610
 
501
611
  def use_insert_returning?
502
612
  @use_insert_returning
503
613
  end
504
614
 
505
- # Returns the version of the connected PostgreSQL server.
615
+ # Returns the version of the connected YugabyteDB server.
506
616
  def get_database_version # :nodoc:
507
- @connection.server_version
617
+ valid_raw_connection.server_version
508
618
  end
509
619
  alias :yugabytedb_version :database_version
510
620
 
@@ -532,8 +642,8 @@ module ActiveRecord
532
642
  end
533
643
 
534
644
  def check_version # :nodoc:
535
- if database_version < 90300 # < 9.3
536
- raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
645
+ if database_version < 9_03_00 # < 9.3
646
+ raise "Your version of YugabyteDB (#{database_version}) is too old. Active Record supports YugabyteDB >= 9.3."
537
647
  end
538
648
  end
539
649
 
@@ -576,10 +686,6 @@ module ActiveRecord
576
686
  m.register_type "polygon", OID::SpecializedString.new(:polygon)
577
687
  m.register_type "circle", OID::SpecializedString.new(:circle)
578
688
 
579
- register_class_with_precision m, "time", Type::Time
580
- register_class_with_precision m, "timestamp", OID::Timestamp
581
- register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
582
-
583
689
  m.register_type "numeric" do |_, fmod, sql_type|
584
690
  precision = extract_precision(sql_type)
585
691
  scale = extract_scale(sql_type)
@@ -608,316 +714,369 @@ module ActiveRecord
608
714
  end
609
715
 
610
716
  private
611
- def type_map
612
- @type_map ||= Type::HashLookupTypeMap.new
613
- end
717
+ attr_reader :type_map
614
718
 
615
- def initialize_type_map(m = type_map)
616
- self.class.initialize_type_map(m)
617
- load_additional_types
618
- end
719
+ def initialize_type_map(m = type_map)
720
+ self.class.initialize_type_map(m)
619
721
 
620
- # Extracts the value from a PostgreSQL column default definition.
621
- def extract_value_from_default(default)
622
- case default
623
- # Quoted types
624
- when /\A[(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
625
- # The default 'now'::date is CURRENT_DATE
626
- if $1 == "now" && $2 == "date"
627
- nil
722
+ self.class.register_class_with_precision m, "time", Type::Time, timezone: @default_timezone
723
+ self.class.register_class_with_precision m, "timestamp", OID::Timestamp, timezone: @default_timezone
724
+ self.class.register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
725
+
726
+ load_additional_types
727
+ end
728
+
729
+ # Extracts the value from a PostgreSQL column default definition.
730
+ def extract_value_from_default(default)
731
+ case default
732
+ # Quoted types
733
+ when /\A[(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
734
+ # The default 'now'::date is CURRENT_DATE
735
+ if $1 == "now" && $2 == "date"
736
+ nil
737
+ else
738
+ $1.gsub("''", "'")
739
+ end
740
+ # Boolean types
741
+ when "true", "false"
742
+ default
743
+ # Numeric types
744
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
745
+ $1
746
+ # Object identifier types
747
+ when /\A-?\d+\z/
748
+ $1
628
749
  else
629
- $1.gsub("''", "'")
750
+ # Anything else is blank, some user type, or some function
751
+ # and we can't know the value of that, so return nil.
752
+ nil
630
753
  end
631
- # Boolean types
632
- when "true", "false"
633
- default
634
- # Numeric types
635
- when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
636
- $1
637
- # Object identifier types
638
- when /\A-?\d+\z/
639
- $1
640
- else
641
- # Anything else is blank, some user type, or some function
642
- # and we can't know the value of that, so return nil.
643
- nil
644
754
  end
645
- end
646
-
647
- def extract_default_function(default_value, default)
648
- default if has_default_function?(default_value, default)
649
- end
650
-
651
- def has_default_function?(default_value, default)
652
- !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default)
653
- end
654
755
 
655
- # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
656
- VALUE_LIMIT_VIOLATION = "22001"
657
- NUMERIC_VALUE_OUT_OF_RANGE = "22003"
658
- NOT_NULL_VIOLATION = "23502"
659
- FOREIGN_KEY_VIOLATION = "23503"
660
- UNIQUE_VIOLATION = "23505"
661
- SERIALIZATION_FAILURE = "40001"
662
- DEADLOCK_DETECTED = "40P01"
663
- DUPLICATE_DATABASE = "42P04"
664
- LOCK_NOT_AVAILABLE = "55P03"
665
- QUERY_CANCELED = "57014"
756
+ def extract_default_function(default_value, default)
757
+ default if has_default_function?(default_value, default)
758
+ end
666
759
 
667
- def translate_exception(exception, message:, sql:, binds:)
668
- return exception unless exception.respond_to?(:result)
760
+ def has_default_function?(default_value, default)
761
+ !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default)
762
+ end
669
763
 
670
- case exception.result.try(:error_field, YugabyteYSQL::PG_DIAG_SQLSTATE)
671
- when nil
672
- if exception.message.match?(/connection is closed/i)
673
- ConnectionNotEstablished.new(exception)
764
+ # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
765
+ VALUE_LIMIT_VIOLATION = "22001"
766
+ NUMERIC_VALUE_OUT_OF_RANGE = "22003"
767
+ NOT_NULL_VIOLATION = "23502"
768
+ FOREIGN_KEY_VIOLATION = "23503"
769
+ UNIQUE_VIOLATION = "23505"
770
+ SERIALIZATION_FAILURE = "40001"
771
+ DEADLOCK_DETECTED = "40P01"
772
+ DUPLICATE_DATABASE = "42P04"
773
+ LOCK_NOT_AVAILABLE = "55P03"
774
+ QUERY_CANCELED = "57014"
775
+
776
+ def translate_exception(exception, message:, sql:, binds:)
777
+ return exception unless exception.respond_to?(:result)
778
+
779
+ case exception.result.try(:error_field, YSQL::PG_DIAG_SQLSTATE)
780
+ when nil
781
+ if exception.message.match?(/connection is closed/i)
782
+ ConnectionNotEstablished.new(exception, connection_pool: @pool)
783
+ elsif exception.is_a?(YSQL::ConnectionBad)
784
+ # libpq message style always ends with a newline; the pg gem's internal
785
+ # errors do not. We separate these cases because a pg-internal
786
+ # ConnectionBad means it failed before it managed to send the query,
787
+ # whereas a libpq failure could have occurred at any time (meaning the
788
+ # server may have already executed part or all of the query).
789
+ if exception.message.end_with?("\n")
790
+ ConnectionFailed.new(exception, connection_pool: @pool)
791
+ else
792
+ ConnectionNotEstablished.new(exception, connection_pool: @pool)
793
+ end
794
+ else
795
+ super
796
+ end
797
+ when UNIQUE_VIOLATION
798
+ RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
799
+ when FOREIGN_KEY_VIOLATION
800
+ InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
801
+ when VALUE_LIMIT_VIOLATION
802
+ ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
803
+ when NUMERIC_VALUE_OUT_OF_RANGE
804
+ RangeError.new(message, sql: sql, binds: binds, connection_pool: @pool)
805
+ when NOT_NULL_VIOLATION
806
+ NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
807
+ when SERIALIZATION_FAILURE
808
+ SerializationFailure.new(message, sql: sql, binds: binds, connection_pool: @pool)
809
+ when DEADLOCK_DETECTED
810
+ Deadlocked.new(message, sql: sql, binds: binds, connection_pool: @pool)
811
+ when DUPLICATE_DATABASE
812
+ DatabaseAlreadyExists.new(message, sql: sql, binds: binds, connection_pool: @pool)
813
+ when LOCK_NOT_AVAILABLE
814
+ LockWaitTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
815
+ when QUERY_CANCELED
816
+ QueryCanceled.new(message, sql: sql, binds: binds, connection_pool: @pool)
674
817
  else
675
818
  super
676
819
  end
677
- when UNIQUE_VIOLATION
678
- RecordNotUnique.new(message, sql: sql, binds: binds)
679
- when FOREIGN_KEY_VIOLATION
680
- InvalidForeignKey.new(message, sql: sql, binds: binds)
681
- when VALUE_LIMIT_VIOLATION
682
- ValueTooLong.new(message, sql: sql, binds: binds)
683
- when NUMERIC_VALUE_OUT_OF_RANGE
684
- RangeError.new(message, sql: sql, binds: binds)
685
- when NOT_NULL_VIOLATION
686
- NotNullViolation.new(message, sql: sql, binds: binds)
687
- when SERIALIZATION_FAILURE
688
- SerializationFailure.new(message, sql: sql, binds: binds)
689
- when DEADLOCK_DETECTED
690
- Deadlocked.new(message, sql: sql, binds: binds)
691
- when DUPLICATE_DATABASE
692
- DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
693
- when LOCK_NOT_AVAILABLE
694
- LockWaitTimeout.new(message, sql: sql, binds: binds)
695
- when QUERY_CANCELED
696
- QueryCanceled.new(message, sql: sql, binds: binds)
697
- else
698
- super
699
820
  end
700
- end
701
821
 
702
- def get_oid_type(oid, fmod, column_name, sql_type = "")
703
- if !type_map.key?(oid)
704
- load_additional_types([oid])
822
+ def retryable_query_error?(exception)
823
+ # We cannot retry anything if we're inside a broken transaction; we need to at
824
+ # least raise until the innermost savepoint is rolled back
825
+ @raw_connection&.transaction_status != ::YSQL::PQTRANS_INERROR &&
826
+ super
705
827
  end
706
828
 
707
- type_map.fetch(oid, fmod, sql_type) {
708
- warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
709
- Type.default_value.tap do |cast_type|
710
- type_map.register_type(oid, cast_type)
829
+ def get_oid_type(oid, fmod, column_name, sql_type = "")
830
+ if !type_map.key?(oid)
831
+ load_additional_types([oid])
711
832
  end
712
- }
713
- end
714
833
 
715
- def load_additional_types(oids = nil)
716
- initializer = OID::TypeMapInitializer.new(type_map)
717
- load_types_queries(initializer, oids) do |query|
718
- execute_and_clear(query, "SCHEMA", []) do |records|
719
- initializer.run(records)
834
+ type_map.fetch(oid, fmod, sql_type) {
835
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
836
+ Type.default_value.tap do |cast_type|
837
+ type_map.register_type(oid, cast_type)
838
+ end
839
+ }
840
+ end
841
+
842
+ def load_additional_types(oids = nil)
843
+ initializer = OID::TypeMapInitializer.new(type_map)
844
+ load_types_queries(initializer, oids) do |query|
845
+ execute_and_clear(query, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |records|
846
+ initializer.run(records)
847
+ end
720
848
  end
721
849
  end
722
- end
723
850
 
724
- def load_types_queries(initializer, oids)
725
- query = <<~SQL
851
+ def load_types_queries(initializer, oids)
852
+ query = <<~SQL
726
853
  SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
727
854
  FROM pg_type as t
728
855
  LEFT JOIN pg_range as r ON oid = rngtypid
729
856
  SQL
730
- if oids
731
- yield query + "WHERE t.oid IN (%s)" % oids.join(", ")
732
- else
733
- yield query + initializer.query_conditions_for_known_type_names
734
- yield query + initializer.query_conditions_for_known_type_types
735
- yield query + initializer.query_conditions_for_array_types
857
+ if oids
858
+ yield query + "WHERE t.oid IN (%s)" % oids.join(", ")
859
+ else
860
+ yield query + initializer.query_conditions_for_known_type_names
861
+ yield query + initializer.query_conditions_for_known_type_types
862
+ yield query + initializer.query_conditions_for_array_types
863
+ end
736
864
  end
737
- end
738
865
 
739
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
866
+ FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
740
867
 
741
- def execute_and_clear(sql, name, binds, prepare: false, async: false)
742
- sql = transform_query(sql)
743
- check_if_write_query(sql)
868
+ def execute_and_clear(sql, name, binds, prepare: false, async: false, allow_retry: false, materialize_transactions: true)
869
+ sql = transform_query(sql)
870
+ check_if_write_query(sql)
744
871
 
745
- if !prepare || without_prepared_statement?(binds)
746
- result = exec_no_cache(sql, name, binds, async: async)
747
- else
748
- result = exec_cache(sql, name, binds, async: async)
749
- end
750
- begin
751
- ret = yield result
752
- ensure
753
- result.clear
872
+ if !prepare || without_prepared_statement?(binds)
873
+ result = exec_no_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
874
+ else
875
+ result = exec_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
876
+ end
877
+ begin
878
+ ret = yield result
879
+ ensure
880
+ result.clear
881
+ end
882
+ ret
754
883
  end
755
- ret
756
- end
757
884
 
758
- def exec_no_cache(sql, name, binds, async: false)
759
- materialize_transactions
760
- mark_transaction_written_if_write(sql)
885
+ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
886
+ mark_transaction_written_if_write(sql)
761
887
 
762
- # make sure we carry over any changes to ActiveRecord.default_timezone that have been
763
- # made since we established the connection
764
- update_typemap_for_default_timezone
888
+ # make sure we carry over any changes to ActiveRecord.default_timezone that have been
889
+ # made since we established the connection
890
+ update_typemap_for_default_timezone
765
891
 
766
- type_casted_binds = type_casted_binds(binds)
767
- log(sql, name, binds, type_casted_binds, async: async) do
768
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
769
- @connection.exec_params(sql, type_casted_binds)
892
+ type_casted_binds = type_casted_binds(binds)
893
+ log(sql, name, binds, type_casted_binds, async: async) do
894
+ with_raw_connection do |conn|
895
+ result = conn.exec_params(sql, type_casted_binds)
896
+ verified!
897
+ result
898
+ end
770
899
  end
771
900
  end
772
- end
773
901
 
774
- def exec_cache(sql, name, binds, async: false)
775
- materialize_transactions
776
- mark_transaction_written_if_write(sql)
777
- update_typemap_for_default_timezone
902
+ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
903
+ mark_transaction_written_if_write(sql)
904
+
905
+ update_typemap_for_default_timezone
778
906
 
779
- stmt_key = prepare_statement(sql, binds)
780
- type_casted_binds = type_casted_binds(binds)
907
+ with_raw_connection do |conn|
908
+ stmt_key = prepare_statement(sql, binds, conn)
909
+ type_casted_binds = type_casted_binds(binds)
781
910
 
782
- log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
783
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
784
- @connection.exec_prepared(stmt_key, type_casted_binds)
911
+ log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
912
+ result = conn.exec_prepared(stmt_key, type_casted_binds)
913
+ verified!
914
+ result
915
+ end
785
916
  end
786
- end
787
- rescue ActiveRecord::StatementInvalid => e
788
- raise unless is_cached_plan_failure?(e)
789
-
790
- # Nothing we can do if we are in a transaction because all commands
791
- # will raise InFailedSQLTransaction
792
- if in_transaction?
793
- raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
794
- else
795
- @lock.synchronize do
796
- # outside of transactions we can simply flush this query and retry
797
- @statements.delete sql_key(sql)
917
+ rescue ActiveRecord::StatementInvalid => e
918
+ raise unless is_cached_plan_failure?(e)
919
+
920
+ # Nothing we can do if we are in a transaction because all commands
921
+ # will raise InFailedSQLTransaction
922
+ if in_transaction?
923
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
924
+ else
925
+ @lock.synchronize do
926
+ # outside of transactions we can simply flush this query and retry
927
+ @statements.delete sql_key(sql)
928
+ end
929
+ retry
798
930
  end
799
- retry
800
931
  end
801
- end
802
932
 
803
- # Annoyingly, the code for prepared statements whose return value may
804
- # have changed is FEATURE_NOT_SUPPORTED.
805
- #
806
- # This covers various different error types so we need to do additional
807
- # work to classify the exception definitively as a
808
- # ActiveRecord::PreparedStatementCacheExpired
809
- #
810
- # Check here for more details:
811
- # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
812
- def is_cached_plan_failure?(e)
813
- pgerror = e.cause
814
- pgerror.result.result_error_field(YugabyteYSQL::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
815
- pgerror.result.result_error_field(YugabyteYSQL::PG_DIAG_SOURCE_FUNCTION) == "RevalidateCachedQuery"
816
- rescue
817
- false
818
- end
933
+ # Annoyingly, the code for prepared statements whose return value may
934
+ # have changed is FEATURE_NOT_SUPPORTED.
935
+ #
936
+ # This covers various different error types so we need to do additional
937
+ # work to classify the exception definitively as a
938
+ # ActiveRecord::PreparedStatementCacheExpired
939
+ #
940
+ # Check here for more details:
941
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
942
+ def is_cached_plan_failure?(e)
943
+ pgerror = e.cause
944
+ pgerror.result.result_error_field(YSQL::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
945
+ pgerror.result.result_error_field(YSQL::PG_DIAG_SOURCE_FUNCTION) == "RevalidateCachedQuery"
946
+ rescue
947
+ false
948
+ end
819
949
 
820
- def in_transaction?
821
- open_transactions > 0
822
- end
950
+ def in_transaction?
951
+ open_transactions > 0
952
+ end
823
953
 
824
- # Returns the statement identifier for the client side cache
825
- # of statements
826
- def sql_key(sql)
827
- "#{schema_search_path}-#{sql}"
828
- end
954
+ # Returns the statement identifier for the client side cache
955
+ # of statements
956
+ def sql_key(sql)
957
+ "#{schema_search_path}-#{sql}"
958
+ end
829
959
 
830
- # Prepare the statement if it hasn't been prepared, return
831
- # the statement key.
832
- def prepare_statement(sql, binds)
833
- @lock.synchronize do
960
+ # Prepare the statement if it hasn't been prepared, return
961
+ # the statement key.
962
+ def prepare_statement(sql, binds, conn)
834
963
  sql_key = sql_key(sql)
835
964
  unless @statements.key? sql_key
836
965
  nextkey = @statements.next_key
837
966
  begin
838
- @connection.prepare nextkey, sql
967
+ conn.prepare nextkey, sql
839
968
  rescue => e
840
969
  raise translate_exception_class(e, sql, binds)
841
970
  end
842
971
  # Clear the queue
843
- @connection.get_last_result
972
+ conn.get_last_result
844
973
  @statements[sql_key] = nextkey
845
974
  end
846
975
  @statements[sql_key]
847
976
  end
848
- end
849
977
 
850
- # Connects to a PostgreSQL server and sets up the adapter depending on the
851
- # connected server's characteristics.
852
- def connect
853
- @connection = self.class.new_client(@connection_parameters)
854
- configure_connection
855
- add_pg_encoders
856
- add_pg_decoders
857
- end
978
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
979
+ # connected server's characteristics.
980
+ def connect
981
+ @raw_connection = self.class.new_client(@connection_parameters)
982
+ rescue ConnectionNotEstablished => ex
983
+ raise ex.set_pool(@pool)
984
+ end
985
+
986
+ def reconnect
987
+ begin
988
+ @raw_connection&.reset
989
+ rescue YSQL::ConnectionBad
990
+ @raw_connection = nil
991
+ end
858
992
 
859
- # Configures the encoding, verbosity, schema search path, and time zone of the connection.
860
- # This is called by #connect and should not be called manually.
861
- def configure_connection
862
- if @config[:encoding]
863
- @connection.set_client_encoding(@config[:encoding])
993
+ connect unless @raw_connection
864
994
  end
865
- self.client_min_messages = @config[:min_messages] || "warning"
866
- self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
867
995
 
868
- # Use standard-conforming strings so we don't have to do the E'...' dance.
869
- set_standard_conforming_strings
996
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
997
+ # This is called by #connect and should not be called manually.
998
+ def configure_connection
999
+ if @config[:encoding]
1000
+ @raw_connection.set_client_encoding(@config[:encoding])
1001
+ end
1002
+ self.client_min_messages = @config[:min_messages] || "warning"
1003
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
1004
+
1005
+ unless ActiveRecord.db_warnings_action.nil?
1006
+ @raw_connection.set_notice_receiver do |result|
1007
+ message = result.error_field(YSQL::Result::PG_DIAG_MESSAGE_PRIMARY)
1008
+ code = result.error_field(YSQL::Result::PG_DIAG_SQLSTATE)
1009
+ level = result.error_field(YSQL::Result::PG_DIAG_SEVERITY)
1010
+ @notice_receiver_sql_warnings << SQLWarning.new(message, code, level, nil, @pool)
1011
+ end
1012
+ end
870
1013
 
871
- variables = @config.fetch(:variables, {}).stringify_keys
1014
+ # Use standard-conforming strings so we don't have to do the E'...' dance.
1015
+ set_standard_conforming_strings
872
1016
 
873
- # If using Active Record's time zone support configure the connection to return
874
- # TIMESTAMP WITH ZONE types in UTC.
875
- unless variables["timezone"]
876
- if ActiveRecord.default_timezone == :utc
877
- variables["timezone"] = "UTC"
878
- elsif @local_tz
879
- variables["timezone"] = @local_tz
1017
+ variables = @config.fetch(:variables, {}).stringify_keys
1018
+
1019
+ # Set interval output format to ISO 8601 for ease of parsing by ActiveSupport::Duration.parse
1020
+ internal_execute("SET intervalstyle = iso_8601")
1021
+
1022
+ # SET statements from :variables config hash
1023
+ # https://www.postgresql.org/docs/current/static/sql-set.html
1024
+ variables.map do |k, v|
1025
+ if v == ":default" || v == :default
1026
+ # Sets the value to the global or compile default
1027
+ internal_execute("SET SESSION #{k} TO DEFAULT")
1028
+ elsif !v.nil?
1029
+ internal_execute("SET SESSION #{k} TO #{quote(v)}")
1030
+ end
880
1031
  end
1032
+
1033
+ add_pg_encoders
1034
+ add_pg_decoders
1035
+
1036
+ reload_type_map
881
1037
  end
882
1038
 
883
- # Set interval output format to ISO 8601 for ease of parsing by ActiveSupport::Duration.parse
884
- execute("SET intervalstyle = iso_8601", "SCHEMA")
885
-
886
- # SET statements from :variables config hash
887
- # https://www.postgresql.org/docs/current/static/sql-set.html
888
- variables.map do |k, v|
889
- if v == ":default" || v == :default
890
- # Sets the value to the global or compile default
891
- execute("SET SESSION #{k} TO DEFAULT", "SCHEMA")
892
- elsif !v.nil?
893
- execute("SET SESSION #{k} TO #{quote(v)}", "SCHEMA")
1039
+ def reconfigure_connection_timezone
1040
+ variables = @config.fetch(:variables, {}).stringify_keys
1041
+
1042
+ # If it's been directly configured as a connection variable, we don't
1043
+ # need to do anything here; it will be set up by configure_connection
1044
+ # and then never changed.
1045
+ return if variables["timezone"]
1046
+
1047
+ # If using Active Record's time zone support configure the connection
1048
+ # to return TIMESTAMP WITH ZONE types in UTC.
1049
+ if default_timezone == :utc
1050
+ internal_execute("SET SESSION timezone TO 'UTC'")
1051
+ else
1052
+ internal_execute("SET SESSION timezone TO DEFAULT")
894
1053
  end
895
1054
  end
896
- end
897
1055
 
898
- # Returns the list of a table's column names, data types, and default values.
899
- #
900
- # The underlying query is roughly:
901
- # SELECT column.name, column.type, default.value, column.comment
902
- # FROM column LEFT JOIN default
903
- # ON column.table_id = default.table_id
904
- # AND column.num = default.column_num
905
- # WHERE column.table_id = get_table_id('table_name')
906
- # AND column.num > 0
907
- # AND NOT column.is_dropped
908
- # ORDER BY column.num
909
- #
910
- # If the table name is not prefixed with a schema, the database will
911
- # take the first match from the schema search path.
912
- #
913
- # Query implementation notes:
914
- # - format_type includes the column size constraint, e.g. varchar(50)
915
- # - ::regclass is a function that gives the id for a table name
916
- def column_definitions(table_name)
917
- query(<<~SQL, "SCHEMA")
1056
+ # Returns the list of a table's column names, data types, and default values.
1057
+ #
1058
+ # The underlying query is roughly:
1059
+ # SELECT column.name, column.type, default.value, column.comment
1060
+ # FROM column LEFT JOIN default
1061
+ # ON column.table_id = default.table_id
1062
+ # AND column.num = default.column_num
1063
+ # WHERE column.table_id = get_table_id('table_name')
1064
+ # AND column.num > 0
1065
+ # AND NOT column.is_dropped
1066
+ # ORDER BY column.num
1067
+ #
1068
+ # If the table name is not prefixed with a schema, the database will
1069
+ # take the first match from the schema search path.
1070
+ #
1071
+ # Query implementation notes:
1072
+ # - format_type includes the column size constraint, e.g. varchar(50)
1073
+ # - ::regclass is a function that gives the id for a table name
1074
+ def column_definitions(table_name)
1075
+ query(<<~SQL, "SCHEMA")
918
1076
  SELECT a.attname, format_type(a.atttypid, a.atttypmod),
919
1077
  pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
920
1078
  c.collname, col_description(a.attrelid, a.attnum) AS comment,
1079
+ #{supports_identity_columns? ? 'attidentity' : quote('')} AS identity,
921
1080
  #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated
922
1081
  FROM pg_attribute a
923
1082
  LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
@@ -927,142 +1086,144 @@ module ActiveRecord
927
1086
  AND a.attnum > 0 AND NOT a.attisdropped
928
1087
  ORDER BY a.attnum
929
1088
  SQL
930
- end
1089
+ end
931
1090
 
932
- def extract_table_ref_from_insert_sql(sql)
933
- sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
934
- $1.strip if $1
935
- end
1091
+ def arel_visitor
1092
+ Arel::Visitors::YugabyteDB.new(self)
1093
+ end
936
1094
 
937
- def arel_visitor # todo
938
- Arel::Visitors::YugabyteDB.new(self)
939
- end
1095
+ def build_statement_pool
1096
+ StatementPool.new(self, self.class.type_cast_config_to_integer(@config[:statement_limit]))
1097
+ end
940
1098
 
941
- def build_statement_pool
942
- StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
943
- end
1099
+ def can_perform_case_insensitive_comparison_for?(column)
1100
+ # NOTE: citext is an exception. It is possible to perform a
1101
+ # case-insensitive comparison using `LOWER()`, but it is
1102
+ # unnecessary, as `citext` is case-insensitive by definition.
1103
+ @case_insensitive_cache ||= { "citext" => false }
1104
+ @case_insensitive_cache.fetch(column.sql_type) do
1105
+ @case_insensitive_cache[column.sql_type] = begin
1106
+ sql = <<~SQL
1107
+ SELECT exists(
1108
+ SELECT * FROM pg_proc
1109
+ WHERE proname = 'lower'
1110
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
1111
+ ) OR exists(
1112
+ SELECT * FROM pg_proc
1113
+ INNER JOIN pg_cast
1114
+ ON ARRAY[casttarget]::oidvector = proargtypes
1115
+ WHERE proname = 'lower'
1116
+ AND castsource = #{quote column.sql_type}::regtype
1117
+ )
1118
+ SQL
1119
+ execute_and_clear(sql, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |result|
1120
+ result.getvalue(0, 0)
1121
+ end
1122
+ end
1123
+ end
1124
+ end
944
1125
 
945
- def can_perform_case_insensitive_comparison_for?(column)
946
- @case_insensitive_cache ||= {}
947
- @case_insensitive_cache[column.sql_type] ||= begin
948
- sql = <<~SQL
949
- SELECT exists(
950
- SELECT * FROM pg_proc
951
- WHERE proname = 'lower'
952
- AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
953
- ) OR exists(
954
- SELECT * FROM pg_proc
955
- INNER JOIN pg_cast
956
- ON ARRAY[casttarget]::oidvector = proargtypes
957
- WHERE proname = 'lower'
958
- AND castsource = #{quote column.sql_type}::regtype
959
- )
960
- SQL
961
- execute_and_clear(sql, "SCHEMA", []) do |result|
962
- result.getvalue(0, 0)
963
- end
964
- end
965
- end
1126
+ def add_pg_encoders
1127
+ map = YSQL::TypeMapByClass.new
1128
+ map[Integer] = YSQL::TextEncoder::Integer.new
1129
+ map[TrueClass] = YSQL::TextEncoder::Boolean.new
1130
+ map[FalseClass] = YSQL::TextEncoder::Boolean.new
1131
+ @raw_connection.type_map_for_queries = map
1132
+ end
966
1133
 
967
- def add_pg_encoders
968
- map = YugabyteYSQL::TypeMapByClass.new
969
- map[Integer] = YugabyteYSQL::TextEncoder::Integer.new
970
- map[TrueClass] = YugabyteYSQL::TextEncoder::Boolean.new
971
- map[FalseClass] = YugabyteYSQL::TextEncoder::Boolean.new
972
- @connection.type_map_for_queries = map
973
- end
1134
+ def update_typemap_for_default_timezone
1135
+ if @raw_connection && @mapped_default_timezone != default_timezone && @timestamp_decoder
1136
+ decoder_class = default_timezone == :utc ?
1137
+ YSQL::TextDecoder::TimestampUtc :
1138
+ YSQL::TextDecoder::TimestampWithoutTimeZone
974
1139
 
975
- def update_typemap_for_default_timezone
976
- if @default_timezone != ActiveRecord.default_timezone && @timestamp_decoder
977
- decoder_class = ActiveRecord.default_timezone == :utc ?
978
- YugabyteYSQL::TextDecoder::TimestampUtc :
979
- YugabyteYSQL::TextDecoder::TimestampWithoutTimeZone
1140
+ @timestamp_decoder = decoder_class.new(**@timestamp_decoder.to_h)
1141
+ @raw_connection.type_map_for_results.add_coder(@timestamp_decoder)
980
1142
 
981
- @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
982
- @connection.type_map_for_results.add_coder(@timestamp_decoder)
1143
+ @mapped_default_timezone = default_timezone
983
1144
 
984
- @default_timezone = ActiveRecord.default_timezone
1145
+ # if default timezone has changed, we need to reconfigure the connection
1146
+ # (specifically, the session time zone)
1147
+ reconfigure_connection_timezone
985
1148
 
986
- # if default timezone has changed, we need to reconfigure the connection
987
- # (specifically, the session time zone)
988
- configure_connection
1149
+ true
1150
+ end
989
1151
  end
990
- end
991
-
992
- def add_pg_decoders
993
- @default_timezone = nil
994
- @timestamp_decoder = nil
995
-
996
- coders_by_name = {
997
- "int2" => YugabyteYSQL::TextDecoder::Integer,
998
- "int4" => YugabyteYSQL::TextDecoder::Integer,
999
- "int8" => YugabyteYSQL::TextDecoder::Integer,
1000
- "oid" => YugabyteYSQL::TextDecoder::Integer,
1001
- "float4" => YugabyteYSQL::TextDecoder::Float,
1002
- "float8" => YugabyteYSQL::TextDecoder::Float,
1003
- "numeric" => YugabyteYSQL::TextDecoder::Numeric,
1004
- "bool" => YugabyteYSQL::TextDecoder::Boolean,
1005
- "timestamp" => YugabyteYSQL::TextDecoder::TimestampUtc,
1006
- "timestamptz" => YugabyteYSQL::TextDecoder::TimestampWithTimeZone,
1007
- }
1008
1152
 
1009
- known_coder_types = coders_by_name.keys.map { |n| quote(n) }
1010
- query = <<~SQL % known_coder_types.join(", ")
1153
+ def add_pg_decoders
1154
+ @mapped_default_timezone = nil
1155
+ @timestamp_decoder = nil
1156
+
1157
+ coders_by_name = {
1158
+ "int2" => YSQL::TextDecoder::Integer,
1159
+ "int4" => YSQL::TextDecoder::Integer,
1160
+ "int8" => YSQL::TextDecoder::Integer,
1161
+ "oid" => YSQL::TextDecoder::Integer,
1162
+ "float4" => YSQL::TextDecoder::Float,
1163
+ "float8" => YSQL::TextDecoder::Float,
1164
+ "numeric" => YSQL::TextDecoder::Numeric,
1165
+ "bool" => YSQL::TextDecoder::Boolean,
1166
+ "timestamp" => YSQL::TextDecoder::TimestampUtc,
1167
+ "timestamptz" => YSQL::TextDecoder::TimestampWithTimeZone,
1168
+ }
1169
+
1170
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
1171
+ query = <<~SQL % known_coder_types.join(", ")
1011
1172
  SELECT t.oid, t.typname
1012
1173
  FROM pg_type as t
1013
1174
  WHERE t.typname IN (%s)
1014
1175
  SQL
1015
- coders = execute_and_clear(query, "SCHEMA", []) do |result|
1016
- result.filter_map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
1017
- end
1176
+ coders = execute_and_clear(query, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |result|
1177
+ result.filter_map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
1178
+ end
1018
1179
 
1019
- map = YugabyteYSQL::TypeMapByOid.new
1020
- coders.each { |coder| map.add_coder(coder) }
1021
- @connection.type_map_for_results = map
1180
+ map = YSQL::TypeMapByOid.new
1181
+ coders.each { |coder| map.add_coder(coder) }
1182
+ @raw_connection.type_map_for_results = map
1022
1183
 
1023
- @type_map_for_results = YugabyteYSQL::TypeMapByOid.new
1024
- @type_map_for_results.default_type_map = map
1025
- @type_map_for_results.add_coder(YugabyteYSQL::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
1026
- @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
1184
+ @type_map_for_results = YSQL::TypeMapByOid.new
1185
+ @type_map_for_results.default_type_map = map
1186
+ @type_map_for_results.add_coder(YSQL::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
1187
+ @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
1027
1188
 
1028
- # extract timestamp decoder for use in update_typemap_for_default_timezone
1029
- @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
1030
- update_typemap_for_default_timezone
1031
- end
1189
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
1190
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
1191
+ update_typemap_for_default_timezone
1192
+ end
1032
1193
 
1033
- def construct_coder(row, coder_class)
1034
- return unless coder_class
1035
- coder_class.new(oid: row["oid"].to_i, name: row["typname"])
1036
- end
1194
+ def construct_coder(row, coder_class)
1195
+ return unless coder_class
1196
+ coder_class.new(oid: row["oid"].to_i, name: row["typname"])
1197
+ end
1037
1198
 
1038
- class MoneyDecoder < YugabyteYSQL::SimpleDecoder # :nodoc:
1039
- TYPE = OID::Money.new
1199
+ class MoneyDecoder < YSQL::SimpleDecoder # :nodoc:
1200
+ TYPE = OID::Money.new
1040
1201
 
1041
- def decode(value, tuple = nil, field = nil)
1042
- TYPE.deserialize(value)
1202
+ def decode(value, tuple = nil, field = nil)
1203
+ TYPE.deserialize(value)
1204
+ end
1043
1205
  end
1044
- end
1045
1206
 
1046
- ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :yugabytedb)
1047
- ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :yugabytedb)
1048
- ActiveRecord::Type.register(:bit, OID::Bit, adapter: :yugabytedb)
1049
- ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :yugabytedb)
1050
- ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :yugabytedb)
1051
- ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :yugabytedb)
1052
- ActiveRecord::Type.register(:date, OID::Date, adapter: :yugabytedb)
1053
- ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :yugabytedb)
1054
- ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :yugabytedb)
1055
- ActiveRecord::Type.register(:enum, OID::Enum, adapter: :yugabytedb)
1056
- ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :yugabytedb)
1057
- ActiveRecord::Type.register(:inet, OID::Inet, adapter: :yugabytedb)
1058
- ActiveRecord::Type.register(:interval, OID::Interval, adapter: :yugabytedb)
1059
- ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :yugabytedb)
1060
- ActiveRecord::Type.register(:money, OID::Money, adapter: :yugabytedb)
1061
- ActiveRecord::Type.register(:point, OID::Point, adapter: :yugabytedb)
1062
- ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :yugabytedb)
1063
- ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :yugabytedb)
1064
- ActiveRecord::Type.register(:vector, OID::Vector, adapter: :yugabytedb)
1065
- ActiveRecord::Type.register(:xml, OID::Xml, adapter: :yugabytedb)
1207
+ ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :yugabytedb)
1208
+ ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :yugabytedb)
1209
+ ActiveRecord::Type.register(:bit, OID::Bit, adapter: :yugabytedb)
1210
+ ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :yugabytedb)
1211
+ ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :yugabytedb)
1212
+ ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :yugabytedb)
1213
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :yugabytedb)
1214
+ ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :yugabytedb)
1215
+ ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :yugabytedb)
1216
+ ActiveRecord::Type.register(:enum, OID::Enum, adapter: :yugabytedb)
1217
+ ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :yugabytedb)
1218
+ ActiveRecord::Type.register(:inet, OID::Inet, adapter: :yugabytedb)
1219
+ ActiveRecord::Type.register(:interval, OID::Interval, adapter: :yugabytedb)
1220
+ ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :yugabytedb)
1221
+ ActiveRecord::Type.register(:money, OID::Money, adapter: :yugabytedb)
1222
+ ActiveRecord::Type.register(:point, OID::Point, adapter: :yugabytedb)
1223
+ ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :yugabytedb)
1224
+ ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :yugabytedb)
1225
+ ActiveRecord::Type.register(:vector, OID::Vector, adapter: :yugabytedb)
1226
+ ActiveRecord::Type.register(:xml, OID::Xml, adapter: :yugabytedb)
1066
1227
  end
1067
1228
  ActiveSupport.run_load_hooks(:active_record_yugabytedbadapter, YugabyteDBAdapter)
1068
1229
  end