activerecord-redshift-adapter 8.0.0.beta2 → 8.1.0

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