activerecord7-redshift-adapter-pennylane 1.0.1 → 1.0.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record/connection_adapters/redshift_7_0/oid.rb +17 -0
  3. data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/schema_statements.rb +14 -16
  4. data/lib/active_record/connection_adapters/redshift_7_0_adapter.rb +770 -0
  5. data/lib/active_record/connection_adapters/redshift_7_1/array_parser.rb +92 -0
  6. data/lib/active_record/connection_adapters/redshift_7_1/column.rb +17 -0
  7. data/lib/active_record/connection_adapters/redshift_7_1/database_statements.rb +180 -0
  8. data/lib/active_record/connection_adapters/redshift_7_1/oid/date_time.rb +36 -0
  9. data/lib/active_record/connection_adapters/redshift_7_1/oid/decimal.rb +15 -0
  10. data/lib/active_record/connection_adapters/redshift_7_1/oid/json.rb +41 -0
  11. data/lib/active_record/connection_adapters/redshift_7_1/oid/jsonb.rb +25 -0
  12. data/lib/active_record/connection_adapters/redshift_7_1/oid/type_map_initializer.rb +62 -0
  13. data/lib/active_record/connection_adapters/redshift_7_1/oid.rb +17 -0
  14. data/lib/active_record/connection_adapters/redshift_7_1/quoting.rb +161 -0
  15. data/lib/active_record/connection_adapters/redshift_7_1/referential_integrity.rb +17 -0
  16. data/lib/active_record/connection_adapters/redshift_7_1/schema_definitions.rb +70 -0
  17. data/lib/active_record/connection_adapters/redshift_7_1/schema_dumper.rb +17 -0
  18. data/lib/active_record/connection_adapters/redshift_7_1/schema_statements.rb +422 -0
  19. data/lib/active_record/connection_adapters/redshift_7_1/type_metadata.rb +43 -0
  20. data/lib/active_record/connection_adapters/redshift_7_1/utils.rb +81 -0
  21. data/lib/active_record/connection_adapters/redshift_7_1_adapter.rb +848 -0
  22. data/lib/active_record/connection_adapters/redshift_adapter.rb +7 -766
  23. metadata +38 -19
  24. data/lib/active_record/connection_adapters/redshift/oid.rb +0 -17
  25. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/array_parser.rb +0 -0
  26. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/column.rb +0 -0
  27. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/database_statements.rb +0 -0
  28. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/oid/date_time.rb +0 -0
  29. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/oid/decimal.rb +0 -0
  30. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/oid/json.rb +0 -0
  31. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/oid/jsonb.rb +0 -0
  32. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/oid/type_map_initializer.rb +0 -0
  33. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/quoting.rb +0 -0
  34. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/referential_integrity.rb +0 -0
  35. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/schema_definitions.rb +0 -0
  36. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/schema_dumper.rb +0 -0
  37. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/type_metadata.rb +0 -0
  38. /data/lib/active_record/connection_adapters/{redshift → redshift_7_0}/utils.rb +0 -0
@@ -0,0 +1,848 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract_adapter'
4
+ require 'active_record/connection_adapters/statement_pool'
5
+
6
+ require 'active_record/connection_adapters/redshift_7_1/utils'
7
+ require 'active_record/connection_adapters/redshift_7_1/column'
8
+ require 'active_record/connection_adapters/redshift_7_1/oid'
9
+ require 'active_record/connection_adapters/redshift_7_1/quoting'
10
+ require 'active_record/connection_adapters/redshift_7_1/referential_integrity'
11
+ require 'active_record/connection_adapters/redshift_7_1/schema_definitions'
12
+ require 'active_record/connection_adapters/redshift_7_1/schema_dumper'
13
+ require 'active_record/connection_adapters/redshift_7_1/schema_statements'
14
+ require 'active_record/connection_adapters/redshift_7_1/type_metadata'
15
+ require 'active_record/connection_adapters/redshift_7_1/database_statements'
16
+
17
+ require 'active_record/tasks/database_tasks'
18
+
19
+ require 'pg'
20
+
21
+ require 'ipaddr'
22
+
23
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/redshift/, 'ActiveRecord::Tasks::PostgreSQLDatabaseTasks')
24
+ module ActiveRecord
25
+ module ConnectionHandling # :nodoc:
26
+
27
+ def redshift_adapter_class
28
+ ConnectionAdapters::RedshiftAdapter
29
+ end
30
+
31
+ # Establishes a connection to the database that's used by all Active Record objects
32
+ def redshift_connection(config)
33
+ redshift_adapter_class.new(config)
34
+ end
35
+ end
36
+
37
+ module ConnectionAdapters
38
+ # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
39
+ #
40
+ # Options:
41
+ #
42
+ # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets,
43
+ # the default is to connect to localhost.
44
+ # * <tt>:port</tt> - Defaults to 5432.
45
+ # * <tt>:username</tt> - Defaults to be the same as the operating system name of the user running the application.
46
+ # * <tt>:password</tt> - Password to be used if the server demands password authentication.
47
+ # * <tt>:database</tt> - Defaults to be the same as the user name.
48
+ # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
49
+ # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
50
+ # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
51
+ # <encoding></tt> call on the connection.
52
+ # * <tt>:min_messages</tt> - An optional client min messages that is used in a
53
+ # <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
54
+ # * <tt>:variables</tt> - An optional hash of additional parameters that
55
+ # will be used in <tt>SET SESSION key = val</tt> calls on the connection.
56
+ # * <tt>:insert_returning</tt> - Does nothing for Redshift.
57
+ #
58
+ # Any further options are used as connection parameters to libpq. See
59
+ # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
60
+ # list of parameters.
61
+ #
62
+ # In addition, default connection parameters of libpq can be set per environment variables.
63
+ # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
64
+ class RedshiftAdapter < AbstractAdapter
65
+ ADAPTER_NAME = 'Redshift'
66
+
67
+ RS_VALID_CONN_PARAMS = %i[host hostaddr port dbname user password connect_timeout
68
+ client_encoding options application_name fallback_application_name
69
+ keepalives keepalives_idle keepalives_interval keepalives_count
70
+ tty sslmode requiressl sslcompression sslcert sslkey
71
+ sslrootcert sslcrl requirepeer krbsrvname gsslib service].freeze
72
+
73
+ NATIVE_DATABASE_TYPES = {
74
+ primary_key: 'integer identity primary key',
75
+ string: { name: 'varchar' },
76
+ text: { name: 'varchar' },
77
+ integer: { name: 'integer' },
78
+ float: { name: 'decimal' },
79
+ decimal: { name: 'decimal' },
80
+ datetime: { name: 'timestamp' },
81
+ time: { name: 'timestamp' },
82
+ date: { name: 'date' },
83
+ bigint: { name: 'bigint' },
84
+ boolean: { name: 'boolean' }
85
+ }.freeze
86
+
87
+ OID = Redshift::OID # :nodoc:
88
+
89
+ include Redshift::Quoting
90
+ include Redshift::ReferentialIntegrity
91
+ include Redshift::SchemaStatements
92
+ include Redshift::DatabaseStatements
93
+
94
+ def schema_creation # :nodoc:
95
+ Redshift::SchemaCreation.new self
96
+ end
97
+
98
+ def supports_index_sort_order?
99
+ false
100
+ end
101
+
102
+ def supports_partial_index?
103
+ false
104
+ end
105
+
106
+ def supports_transaction_isolation?
107
+ false
108
+ end
109
+
110
+ def supports_foreign_keys?
111
+ true
112
+ end
113
+
114
+ def supports_deferrable_constraints?
115
+ false
116
+ end
117
+
118
+ def supports_views?
119
+ true
120
+ end
121
+
122
+ def supports_virtual_columns?
123
+ false
124
+ end
125
+
126
+ def index_algorithms
127
+ { concurrently: 'CONCURRENTLY' }
128
+ end
129
+
130
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
131
+ def initialize(connection, max)
132
+ super(max)
133
+ @raw_connection = connection
134
+ @counter = 0
135
+ end
136
+
137
+ def next_key
138
+ "a#{@counter += 1}"
139
+ end
140
+
141
+ private
142
+
143
+ def dealloc(key)
144
+ # This is ugly, but safe: the statement pool is only
145
+ # accessed while holding the connection's lock. (And we
146
+ # don't need the complication of with_raw_connection because
147
+ # a reconnect would invalidate the entire statement pool.)
148
+ if conn = @connection.instance_variable_get(:@raw_connection)
149
+ conn.query "DEALLOCATE #{key}" if conn.status == PG::CONNECTION_OK
150
+ end
151
+ rescue PG::Error
152
+ end
153
+ end
154
+
155
+ class << self
156
+ def new_client(conn_params)
157
+ PG.connect(**conn_params)
158
+ rescue ::PG::Error => error
159
+ if conn_params && conn_params[:dbname] == "postgres"
160
+ raise ActiveRecord::ConnectionNotEstablished, error.message
161
+ elsif conn_params && conn_params[:dbname] && error.message.include?(conn_params[:dbname])
162
+ raise ActiveRecord::NoDatabaseError.db_error(conn_params[:dbname])
163
+ elsif conn_params && conn_params[:user] && error.message.include?(conn_params[:user])
164
+ raise ActiveRecord::DatabaseConnectionError.username_error(conn_params[:user])
165
+ elsif conn_params && conn_params[:host] && error.message.include?(conn_params[:host])
166
+ raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:host])
167
+ else
168
+ raise ActiveRecord::ConnectionNotEstablished, error.message
169
+ end
170
+ end
171
+ end
172
+
173
+ # Initializes and connects a PostgreSQL adapter.
174
+ def initialize(...)
175
+ super
176
+
177
+ conn_params = @config.compact
178
+
179
+ # Map ActiveRecords param names to PGs.
180
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
181
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
182
+
183
+ # Forward only valid config params to PG::Connection.connect.
184
+ conn_params.slice!(*RS_VALID_CONN_PARAMS)
185
+
186
+ @connection_parameters = conn_params
187
+
188
+ @max_identifier_length = nil
189
+ @type_map = nil
190
+ @raw_connection = nil
191
+ @notice_receiver_sql_warnings = []
192
+ @table_alias_length = nil
193
+
194
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
195
+ end
196
+
197
+ def truncate(table_name, name = nil)
198
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
199
+ end
200
+
201
+ # Is this connection alive and ready for queries?
202
+ def active?
203
+ @lock.synchronize do
204
+ return false unless @raw_connection
205
+ @raw_connection.query ";"
206
+ end
207
+ true
208
+ rescue PG::Error
209
+ false
210
+ end
211
+
212
+ def reload_type_map # :nodoc:
213
+ @lock.synchronize do
214
+ if @type_map
215
+ type_map.clear
216
+ else
217
+ @type_map = Type::HashLookupTypeMap.new
218
+ end
219
+
220
+ initialize_type_map
221
+ end
222
+ end
223
+
224
+ def reset!
225
+ @lock.synchronize do
226
+ return connect! unless @raw_connection
227
+
228
+ unless @raw_connection.transaction_status == ::PG::PQTRANS_IDLE
229
+ @raw_connection.query "ROLLBACK"
230
+ end
231
+
232
+ super
233
+ end
234
+ end
235
+
236
+ # Disconnects from the database if already connected. Otherwise, this
237
+ # method does nothing.
238
+ def disconnect!
239
+ @lock.synchronize do
240
+ super
241
+ @raw_connection&.close rescue nil
242
+ @raw_connection = nil
243
+ end
244
+ end
245
+
246
+ def discard! # :nodoc:
247
+ super
248
+ @raw_connection&.socket_io&.reopen(IO::NULL) rescue nil
249
+ @raw_connection = nil
250
+ end
251
+
252
+ def native_database_types # :nodoc:
253
+ NATIVE_DATABASE_TYPES
254
+ end
255
+
256
+ # Returns true, since this connection adapter supports migrations.
257
+ def supports_migrations?
258
+ true
259
+ end
260
+
261
+ # Does PostgreSQL support finding primary key on non-Active Record tables?
262
+ def supports_primary_key? # :nodoc:
263
+ true
264
+ end
265
+
266
+ def supports_ddl_transactions?
267
+ true
268
+ end
269
+
270
+ def supports_explain?
271
+ true
272
+ end
273
+
274
+ def supports_extensions?
275
+ false
276
+ end
277
+
278
+ def supports_ranges?
279
+ false
280
+ end
281
+
282
+ def supports_materialized_views?
283
+ false
284
+ end
285
+
286
+ def supports_import?
287
+ true
288
+ end
289
+
290
+ def enable_extension(name)
291
+ ;
292
+ end
293
+
294
+ def disable_extension(name)
295
+ ;
296
+ end
297
+
298
+ def extension_enabled?(_name)
299
+ false
300
+ end
301
+
302
+ def supports_common_table_expressions?
303
+ true
304
+ end
305
+
306
+ # Returns the configured supported identifier length supported by PostgreSQL
307
+ def table_alias_length
308
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
309
+ end
310
+
311
+ # Set the authorized user for this session
312
+ def session_auth=(user)
313
+ clear_cache!
314
+ exec_query "SET SESSION AUTHORIZATION #{user}"
315
+ end
316
+
317
+ def use_insert_returning?
318
+ false
319
+ end
320
+
321
+ def valid_type?(type)
322
+ !native_database_types[type].nil?
323
+ end
324
+
325
+ def update_table_definition(table_name, base)
326
+ # :nodoc:
327
+ Redshift::Table.new(table_name, base)
328
+ end
329
+
330
+ def lookup_cast_type(sql_type)
331
+ # :nodoc:
332
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", 'SCHEMA').first['oid'].to_i
333
+ super(oid)
334
+ end
335
+
336
+ def column_name_for_operation(operation, _node)
337
+ # :nodoc:
338
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
339
+ end
340
+
341
+ OPERATION_ALIASES = { # :nodoc:
342
+ 'maximum' => 'max',
343
+ 'minimum' => 'min',
344
+ 'average' => 'avg'
345
+ }.freeze
346
+
347
+ protected
348
+
349
+ # Returns the version of the connected PostgreSQL server.
350
+ def redshift_version
351
+ @raw_connection.server_version
352
+ end
353
+
354
+ def translate_exception(exception, message:, sql:, binds:)
355
+ return exception unless exception.respond_to?(:result)
356
+
357
+ case exception.message
358
+ when /duplicate key value violates unique constraint/
359
+ RecordNotUnique.new(message, exception)
360
+ when /violates foreign key constraint/
361
+ InvalidForeignKey.new(message, exception)
362
+ else
363
+ super
364
+ end
365
+ end
366
+
367
+ class << self
368
+ def initialize_type_map(m)
369
+ # :nodoc:
370
+ m.register_type 'int2', Type::Integer.new(limit: 2)
371
+ m.register_type 'int4', Type::Integer.new(limit: 4)
372
+ m.register_type 'int8', Type::Integer.new(limit: 8)
373
+ m.alias_type 'oid', 'int2'
374
+ m.register_type 'float4', Type::Float.new
375
+ m.alias_type 'float8', 'float4'
376
+ m.register_type 'text', Type::Text.new
377
+ register_class_with_limit m, 'varchar', Type::String
378
+ m.alias_type 'char', 'varchar'
379
+ m.alias_type 'name', 'varchar'
380
+ m.alias_type 'bpchar', 'varchar'
381
+ m.register_type 'bool', Type::Boolean.new
382
+ m.alias_type 'timestamptz', 'timestamp'
383
+ m.register_type 'date', Type::Date.new
384
+ m.register_type 'time', Type::Time.new
385
+
386
+ m.register_type 'timestamp' do |_, _, sql_type|
387
+ precision = extract_precision(sql_type)
388
+ OID::DateTime.new(precision: precision)
389
+ end
390
+
391
+ m.register_type 'numeric' do |_, fmod, sql_type|
392
+ precision = extract_precision(sql_type)
393
+ scale = extract_scale(sql_type)
394
+
395
+ # The type for the numeric depends on the width of the field,
396
+ # so we'll do something special here.
397
+ #
398
+ # When dealing with decimal columns:
399
+ #
400
+ # places after decimal = fmod - 4 & 0xffff
401
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
402
+ if fmod && (fmod - 4 & 0xffff) == 0
403
+ # FIXME: Remove this class, and the second argument to
404
+ # lookups on PG
405
+ Type::DecimalWithoutScale.new(precision: precision)
406
+ else
407
+ OID::Decimal.new(precision: precision, scale: scale)
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ private
414
+
415
+ def get_oid_type(oid, fmod, column_name, sql_type = '')
416
+ # :nodoc:
417
+ load_additional_types(type_map, [oid]) unless type_map.key?(oid)
418
+
419
+ type_map.fetch(oid, fmod, sql_type) do
420
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
421
+ Type::Value.new.tap do |cast_type|
422
+ type_map.register_type(oid, cast_type)
423
+ end
424
+ end
425
+ end
426
+
427
+ def type_map
428
+ @type_map ||= Type::HashLookupTypeMap.new
429
+ end
430
+
431
+ def initialize_type_map(m = type_map)
432
+ self.class.initialize_type_map(m)
433
+ load_additional_types(m)
434
+ end
435
+
436
+ def extract_limit(sql_type)
437
+ # :nodoc:
438
+ case sql_type
439
+ when /^bigint/i, /^int8/i
440
+ 8
441
+ when /^smallint/i
442
+ 2
443
+ else
444
+ super
445
+ end
446
+ end
447
+
448
+ # Extracts the value from a PostgreSQL column default definition.
449
+ def extract_value_from_default(default)
450
+ # :nodoc:
451
+ case default
452
+ # Quoted types
453
+ when /\A[(B]?'(.*)'::/m
454
+ Regexp.last_match(1).gsub(/''/, "'")
455
+ # Boolean types
456
+ when 'true', 'false'
457
+ default
458
+ # Numeric types
459
+ when /\A\(?(-?\d+(\.\d*)?)\)?\z/
460
+ Regexp.last_match(1)
461
+ # Object identifier types
462
+ when /\A-?\d+\z/
463
+ Regexp.last_match(1)
464
+ else # rubocop:disable Style/EmptyElse
465
+ # Anything else is blank, some user type, or some function
466
+ # and we can't know the value of that, so return nil.
467
+ nil
468
+ end
469
+ end
470
+
471
+ def extract_default_function(default_value, default)
472
+ # :nodoc:
473
+ default if has_default_function?(default_value, default)
474
+ end
475
+
476
+ def has_default_function?(default_value, default)
477
+ # :nodoc:
478
+ !default_value && (/\w+\(.*\)/ === default)
479
+ end
480
+
481
+ def load_additional_types(type_map, oids = nil)
482
+ # :nodoc:
483
+ initializer = OID::TypeMapInitializer.new(type_map)
484
+
485
+ load_types_queries(initializer, oids) do |query|
486
+ execute_and_clear(query, 'SCHEMA', [], allow_retry: true, materialize_transactions: false) do |records|
487
+ initializer.run(records)
488
+ end
489
+ end
490
+ end
491
+
492
+ def load_types_queries(_initializer, oids)
493
+ query =
494
+ if supports_ranges?
495
+ <<-SQL
496
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
497
+ FROM pg_type as t
498
+ LEFT JOIN pg_range as r ON oid = rngtypid
499
+ SQL
500
+ else
501
+ <<-SQL
502
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
503
+ FROM pg_type as t
504
+ SQL
505
+ end
506
+
507
+ if oids
508
+ yield query + 'WHERE t.oid::integer IN (%s)' % oids.join(', ')
509
+ else
510
+ yield query
511
+ end
512
+ end
513
+
514
+ FEATURE_NOT_SUPPORTED = '0A000' # :nodoc:
515
+
516
+ def execute_and_clear(sql, name, binds, prepare: false, async: false, allow_retry: false, materialize_transactions: true)
517
+ sql = transform_query(sql)
518
+ check_if_write_query(sql)
519
+
520
+ if !prepare || without_prepared_statement?(binds)
521
+ result = exec_no_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
522
+ else
523
+ result = exec_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
524
+ end
525
+ begin
526
+ ret = yield result
527
+ ensure
528
+ result.clear
529
+ end
530
+ ret
531
+ end
532
+
533
+ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
534
+ mark_transaction_written_if_write(sql)
535
+
536
+ # make sure we carry over any changes to ActiveRecord.default_timezone that have been
537
+ # made since we established the connection
538
+ update_typemap_for_default_timezone
539
+
540
+ type_casted_binds = type_casted_binds(binds)
541
+ log(sql, name, binds, type_casted_binds, async: async) do
542
+ with_raw_connection do |conn|
543
+ result = conn.exec_params(sql, type_casted_binds)
544
+ verified!
545
+ result
546
+ end
547
+ end
548
+ end
549
+
550
+ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
551
+ mark_transaction_written_if_write(sql)
552
+
553
+ update_typemap_for_default_timezone
554
+
555
+ with_raw_connection do |conn|
556
+ stmt_key = prepare_statement(sql, binds, conn)
557
+ type_casted_binds = type_casted_binds(binds)
558
+
559
+ log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
560
+ result = conn.exec_prepared(stmt_key, type_casted_binds)
561
+ verified!
562
+ result
563
+ end
564
+ end
565
+ rescue ActiveRecord::StatementInvalid => e
566
+ raise unless is_cached_plan_failure?(e)
567
+
568
+ # Nothing we can do if we are in a transaction because all commands
569
+ # will raise InFailedSQLTransaction
570
+ if in_transaction?
571
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
572
+ else
573
+ @lock.synchronize do
574
+ # outside of transactions we can simply flush this query and retry
575
+ @statements.delete sql_key(sql)
576
+ end
577
+ retry
578
+ end
579
+ end
580
+
581
+ # Annoyingly, the code for prepared statements whose return value may
582
+ # have changed is FEATURE_NOT_SUPPORTED.
583
+ #
584
+ # This covers various different error types so we need to do additional
585
+ # work to classify the exception definitively as a
586
+ # ActiveRecord::PreparedStatementCacheExpired
587
+ #
588
+ # Check here for more details:
589
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
590
+ CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'
591
+
592
+ def is_cached_plan_failure?(e)
593
+ pgerror = e.cause
594
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
595
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
596
+ rescue StandardError
597
+ false
598
+ end
599
+
600
+ # Returns the statement identifier for the client side cache
601
+ # of statements
602
+ def sql_key(sql)
603
+ "#{schema_search_path}-#{sql}"
604
+ end
605
+
606
+ # Prepare the statement if it hasn't been prepared, return
607
+ # the statement key.
608
+ def prepare_statement(sql, binds, conn)
609
+ sql_key = sql_key(sql)
610
+ unless @statements.key? sql_key
611
+ nextkey = @statements.next_key
612
+ begin
613
+ conn.prepare nextkey, sql
614
+ rescue => e
615
+ raise translate_exception_class(e, sql, binds)
616
+ end
617
+ # Clear the queue
618
+ conn.get_last_result
619
+ @statements[sql_key] = nextkey
620
+ end
621
+ @statements[sql_key]
622
+ end
623
+
624
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
625
+ # connected server's characteristics.
626
+ def connect
627
+ @raw_connection = self.class.new_client(@connection_parameters)
628
+ rescue ConnectionNotEstablished => ex
629
+ raise ex.set_pool(@pool)
630
+ end
631
+
632
+ def reconnect
633
+ begin
634
+ @raw_connection&.reset
635
+ rescue PG::ConnectionBad
636
+ @raw_connection = nil
637
+ end
638
+
639
+ connect unless @raw_connection
640
+ end
641
+
642
+ def reconnect
643
+ begin
644
+ @raw_connection&.reset
645
+ rescue PG::ConnectionBad
646
+ @raw_connection = nil
647
+ end
648
+
649
+ connect unless @raw_connection
650
+ end
651
+
652
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
653
+ # This is called by #connect and should not be called manually.
654
+ def configure_connection
655
+ if @config[:encoding]
656
+ @raw_connection.set_client_encoding(@config[:encoding])
657
+ end
658
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
659
+
660
+ variables = @config.fetch(:variables, {}).stringify_keys
661
+
662
+ # SET statements from :variables config hash
663
+ # https://www.postgresql.org/docs/current/static/sql-set.html
664
+ variables.map do |k, v|
665
+ if [':default', :default].include?(v)
666
+ # Sets the value to the global or compile default
667
+ execute("SET #{k} TO DEFAULT", 'SCHEMA')
668
+ elsif !v.nil?
669
+ execute("SET #{k} TO #{quote(v)}", 'SCHEMA')
670
+ end
671
+ end
672
+
673
+ add_pg_encoders
674
+ add_pg_decoders
675
+
676
+ reload_type_map
677
+ end
678
+
679
+ def reconfigure_connection_timezone
680
+ variables = @config.fetch(:variables, {}).stringify_keys
681
+
682
+ # If it's been directly configured as a connection variable, we don't
683
+ # need to do anything here; it will be set up by configure_connection
684
+ # and then never changed.
685
+ return if variables["timezone"]
686
+
687
+ # If using Active Record's time zone support configure the connection
688
+ # to return TIMESTAMP WITH ZONE types in UTC.
689
+ if default_timezone == :utc
690
+ internal_execute("SET timezone TO 'UTC'")
691
+ else
692
+ internal_execute("SET timezone TO DEFAULT")
693
+ end
694
+ end
695
+
696
+ def last_insert_id_result(sequence_name)
697
+ # :nodoc:
698
+ exec_query("SELECT currval('#{sequence_name}')", 'SQL')
699
+ end
700
+
701
+ # Returns the list of a table's column names, data types, and default values.
702
+ #
703
+ # The underlying query is roughly:
704
+ # SELECT column.name, column.type, default.value
705
+ # FROM column LEFT JOIN default
706
+ # ON column.table_id = default.table_id
707
+ # AND column.num = default.column_num
708
+ # WHERE column.table_id = get_table_id('table_name')
709
+ # AND column.num > 0
710
+ # AND NOT column.is_dropped
711
+ # ORDER BY column.num
712
+ #
713
+ # If the table name is not prefixed with a schema, the database will
714
+ # take the first match from the schema search path.
715
+ #
716
+ # Query implementation notes:
717
+ # - format_type includes the column size constraint, e.g. varchar(50)
718
+ # - ::regclass is a function that gives the id for a table name
719
+ def column_definitions(table_name)
720
+ # :nodoc:
721
+ query(<<-END_SQL, 'SCHEMA')
722
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
723
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
724
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
725
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
726
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
727
+ AND a.attnum > 0 AND NOT a.attisdropped
728
+ ORDER BY a.attnum
729
+ END_SQL
730
+ end
731
+
732
+ def extract_table_ref_from_insert_sql(sql)
733
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
734
+ Regexp.last_match(1)&.strip
735
+ end
736
+
737
+ def arel_visitor
738
+ Arel::Visitors::PostgreSQL.new(self)
739
+ end
740
+
741
+ def build_statement_pool
742
+ StatementPool.new(@raw_connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
743
+ end
744
+
745
+ def can_perform_case_insensitive_comparison_for?(column)
746
+ @case_insensitive_cache ||= {}
747
+ @case_insensitive_cache[column.sql_type] ||= begin
748
+ sql = <<~SQL
749
+ SELECT exists(
750
+ SELECT * FROM pg_proc
751
+ WHERE proname = 'lower'
752
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
753
+ ) OR exists(
754
+ SELECT * FROM pg_proc
755
+ INNER JOIN pg_cast
756
+ ON ARRAY[casttarget]::oidvector = proargtypes
757
+ WHERE proname = 'lower'
758
+ AND castsource = #{quote column.sql_type}::regtype
759
+ )
760
+ SQL
761
+ execute_and_clear(sql, 'SCHEMA', []) do |result|
762
+ result.getvalue(0, 0)
763
+ end
764
+ end
765
+ end
766
+
767
+ def add_pg_encoders
768
+ map = PG::TypeMapByClass.new
769
+ map[Integer] = PG::TextEncoder::Integer.new
770
+ map[TrueClass] = PG::TextEncoder::Boolean.new
771
+ map[FalseClass] = PG::TextEncoder::Boolean.new
772
+ @raw_connection.type_map_for_queries = map
773
+ end
774
+
775
+ def update_typemap_for_default_timezone
776
+ if @raw_connection && @mapped_default_timezone != default_timezone && @timestamp_decoder
777
+ decoder_class = default_timezone == :utc ?
778
+ PG::TextDecoder::TimestampUtc :
779
+ PG::TextDecoder::TimestampWithoutTimeZone
780
+
781
+ @timestamp_decoder = decoder_class.new(**@timestamp_decoder.to_h)
782
+ @raw_connection.type_map_for_results.add_coder(@timestamp_decoder)
783
+
784
+ @mapped_default_timezone = default_timezone
785
+
786
+ # if default timezone has changed, we need to reconfigure the connection
787
+ # (specifically, the session time zone)
788
+ reconfigure_connection_timezone
789
+
790
+ true
791
+ end
792
+ end
793
+
794
+ def add_pg_decoders
795
+ @default_timezone = nil
796
+ @timestamp_decoder = nil
797
+
798
+ coders_by_name = {
799
+ 'int2' => PG::TextDecoder::Integer,
800
+ 'int4' => PG::TextDecoder::Integer,
801
+ 'int8' => PG::TextDecoder::Integer,
802
+ 'oid' => PG::TextDecoder::Integer,
803
+ 'float4' => PG::TextDecoder::Float,
804
+ 'float8' => PG::TextDecoder::Float,
805
+ 'bool' => PG::TextDecoder::Boolean
806
+ }
807
+
808
+ if defined?(PG::TextDecoder::TimestampUtc)
809
+ # Use native PG encoders available since pg-1.1
810
+ coders_by_name['timestamp'] = PG::TextDecoder::TimestampUtc
811
+ coders_by_name['timestamptz'] = PG::TextDecoder::TimestampWithTimeZone
812
+ end
813
+
814
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
815
+ query = <<~SQL % known_coder_types.join(', ')
816
+ SELECT t.oid, t.typname
817
+ FROM pg_type as t
818
+ WHERE t.typname IN (%s)
819
+ SQL
820
+ coders = execute_and_clear(query, 'SCHEMA', [], allow_retry: true, materialize_transactions: false) do |result|
821
+ result.filter_map { |row| construct_coder(row, coders_by_name[row['typname']]) }
822
+ end
823
+
824
+ map = PG::TypeMapByOid.new
825
+ coders.each { |coder| map.add_coder(coder) }
826
+ @raw_connection.type_map_for_results = map
827
+
828
+ @type_map_for_results = PG::TypeMapByOid.new
829
+ @type_map_for_results.default_type_map = map
830
+
831
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
832
+ @timestamp_decoder = coders.find { |coder| coder.name == 'timestamp' }
833
+ update_typemap_for_default_timezone
834
+ end
835
+
836
+ def construct_coder(row, coder_class)
837
+ return unless coder_class
838
+
839
+ coder_class.new(oid: row['oid'].to_i, name: row['typname'])
840
+ end
841
+
842
+ def create_table_definition(*args, **options)
843
+ # :nodoc:
844
+ Redshift::TableDefinition.new(self, *args, **options)
845
+ end
846
+ end
847
+ end
848
+ end