activerecord-redshift-adapter 0.9.12 → 8.0.0.beta2

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