activerecord7-redshift-adapter-pennylane 1.0.1 → 1.0.2

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