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.
- checksums.yaml +4 -4
- data/Gemfile.lock +21 -9
- data/README.md +8 -2
- data/activerecord-yugabytedb-adapter.gemspec +2 -2
- data/lib/active_record/connection_adapters/yugabytedb/column.rb +16 -3
- data/lib/active_record/connection_adapters/yugabytedb/database_statements.rb +75 -45
- data/lib/active_record/connection_adapters/yugabytedb/oid/array.rb +3 -3
- data/lib/active_record/connection_adapters/yugabytedb/oid/bytea.rb +1 -1
- data/lib/active_record/connection_adapters/yugabytedb/oid/money.rb +3 -2
- data/lib/active_record/connection_adapters/yugabytedb/oid/range.rb +11 -2
- data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp_with_time_zone.rb +2 -2
- data/lib/active_record/connection_adapters/yugabytedb/quoting.rb +42 -9
- data/lib/active_record/connection_adapters/yugabytedb/referential_integrity.rb +3 -9
- data/lib/active_record/connection_adapters/yugabytedb/schema_creation.rb +76 -6
- data/lib/active_record/connection_adapters/yugabytedb/schema_definitions.rb +131 -2
- data/lib/active_record/connection_adapters/yugabytedb/schema_dumper.rb +53 -0
- data/lib/active_record/connection_adapters/yugabytedb/schema_statements.rb +362 -60
- data/lib/active_record/connection_adapters/yugabytedb/utils.rb +11 -12
- data/lib/active_record/connection_adapters/yugabytedb_adapter.rb +625 -464
- data/lib/arel/visitors/yugabytedb.rb +12 -1
- data/lib/version.rb +1 -1
- metadata +5 -5
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
gem "
|
4
|
-
require "
|
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
|
-
|
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
|
-
|
80
|
-
rescue ::
|
81
|
-
if conn_params && 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[:
|
86
|
-
raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:
|
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
|
101
|
-
# file:
|
112
|
+
# the test environment you can add the following to your test.rb file:
|
102
113
|
#
|
103
|
-
#
|
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 >=
|
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 >=
|
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 >=
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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(
|
283
|
-
super
|
323
|
+
def initialize(...)
|
324
|
+
super
|
284
325
|
|
285
|
-
|
326
|
+
conn_params = @config.compact
|
286
327
|
|
287
|
-
#
|
288
|
-
|
289
|
-
|
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
|
-
|
292
|
-
|
293
|
-
|
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
|
-
@
|
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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
-
@
|
349
|
+
return false unless @raw_connection
|
350
|
+
@raw_connection.query ";"
|
311
351
|
end
|
312
352
|
true
|
313
|
-
rescue
|
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
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
337
|
-
|
338
|
-
unless @
|
339
|
-
@
|
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
|
-
@
|
342
|
-
|
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
|
-
@
|
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
|
-
@
|
358
|
-
@
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
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 >=
|
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
|
-
|
436
|
-
|
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
|
-
|
441
|
-
|
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
|
-
|
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
|
-
|
467
|
-
|
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
|
-
|
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|
|
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
|
480
|
-
|
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
|
539
|
+
CREATE TYPE #{quote_table_name(name)} AS ENUM (#{sql_values});
|
483
540
|
END IF;
|
484
541
|
END
|
485
542
|
$$;
|
486
543
|
SQL
|
487
|
-
|
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
|
-
|
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
|
615
|
+
# Returns the version of the connected YugabyteDB server.
|
506
616
|
def get_database_version # :nodoc:
|
507
|
-
|
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 <
|
536
|
-
raise "Your version of
|
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
|
-
|
612
|
-
@type_map ||= Type::HashLookupTypeMap.new
|
613
|
-
end
|
717
|
+
attr_reader :type_map
|
614
718
|
|
615
|
-
|
616
|
-
|
617
|
-
load_additional_types
|
618
|
-
end
|
719
|
+
def initialize_type_map(m = type_map)
|
720
|
+
self.class.initialize_type_map(m)
|
619
721
|
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
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
|
-
|
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
|
-
|
656
|
-
|
657
|
-
|
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
|
-
|
668
|
-
|
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
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
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
|
-
|
703
|
-
|
704
|
-
|
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
|
-
|
708
|
-
|
709
|
-
|
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
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
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
|
-
|
725
|
-
|
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
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
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
|
-
|
866
|
+
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
|
740
867
|
|
741
|
-
|
742
|
-
|
743
|
-
|
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
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
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
|
-
|
759
|
-
|
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
|
-
|
763
|
-
|
764
|
-
|
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
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
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
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
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
|
-
|
780
|
-
|
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
|
-
|
783
|
-
|
784
|
-
|
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
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
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
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
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
|
-
|
821
|
-
|
822
|
-
|
950
|
+
def in_transaction?
|
951
|
+
open_transactions > 0
|
952
|
+
end
|
823
953
|
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
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
|
-
|
831
|
-
|
832
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
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
|
-
|
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
|
-
#
|
869
|
-
|
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
|
-
|
1014
|
+
# Use standard-conforming strings so we don't have to do the E'...' dance.
|
1015
|
+
set_standard_conforming_strings
|
872
1016
|
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
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
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
if
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
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
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
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
|
-
|
1089
|
+
end
|
931
1090
|
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
end
|
1091
|
+
def arel_visitor
|
1092
|
+
Arel::Visitors::YugabyteDB.new(self)
|
1093
|
+
end
|
936
1094
|
|
937
|
-
|
938
|
-
|
939
|
-
|
1095
|
+
def build_statement_pool
|
1096
|
+
StatementPool.new(self, self.class.type_cast_config_to_integer(@config[:statement_limit]))
|
1097
|
+
end
|
940
1098
|
|
941
|
-
|
942
|
-
|
943
|
-
|
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
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
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
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
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
|
-
|
976
|
-
|
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
|
-
|
982
|
-
@connection.type_map_for_results.add_coder(@timestamp_decoder)
|
1143
|
+
@mapped_default_timezone = default_timezone
|
983
1144
|
|
984
|
-
|
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
|
-
|
987
|
-
|
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
|
-
|
1010
|
-
|
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
|
-
|
1016
|
-
|
1017
|
-
|
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
|
-
|
1020
|
-
|
1021
|
-
|
1180
|
+
map = YSQL::TypeMapByOid.new
|
1181
|
+
coders.each { |coder| map.add_coder(coder) }
|
1182
|
+
@raw_connection.type_map_for_results = map
|
1022
1183
|
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
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
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
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
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
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
|
-
|
1039
|
-
|
1199
|
+
class MoneyDecoder < YSQL::SimpleDecoder # :nodoc:
|
1200
|
+
TYPE = OID::Money.new
|
1040
1201
|
|
1041
|
-
|
1042
|
-
|
1202
|
+
def decode(value, tuple = nil, field = nil)
|
1203
|
+
TYPE.deserialize(value)
|
1204
|
+
end
|
1043
1205
|
end
|
1044
|
-
end
|
1045
1206
|
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
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
|