activerecord-yugabytedb-adapter 7.0.4.1 → 7.1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|