activerecord-yugabytedb-adapter 7.0.4.1 → 7.1.3.4

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.
@@ -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