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