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,844 @@
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
+ 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
+ sql = transform_query(sql)
514
+ check_if_write_query(sql)
515
+
516
+ if !prepare || without_prepared_statement?(binds)
517
+ result = exec_no_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
518
+ else
519
+ result = exec_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
520
+ end
521
+ begin
522
+ ret = yield result
523
+ ensure
524
+ result.clear
525
+ end
526
+ ret
527
+ end
528
+
529
+ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
530
+ mark_transaction_written_if_write(sql)
531
+
532
+ # make sure we carry over any changes to ActiveRecord.default_timezone that have been
533
+ # made since we established the connection
534
+ update_typemap_for_default_timezone
535
+
536
+ type_casted_binds = type_casted_binds(binds)
537
+ log(sql, name, binds, type_casted_binds, async: async) do
538
+ with_raw_connection do |conn|
539
+ result = conn.exec_params(sql, type_casted_binds)
540
+ verified!
541
+ result
542
+ end
543
+ end
544
+ end
545
+
546
+ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
547
+ mark_transaction_written_if_write(sql)
548
+
549
+ update_typemap_for_default_timezone
550
+
551
+ with_raw_connection do |conn|
552
+ stmt_key = prepare_statement(sql, binds, conn)
553
+ type_casted_binds = type_casted_binds(binds)
554
+
555
+ log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
556
+ result = conn.exec_prepared(stmt_key, type_casted_binds)
557
+ verified!
558
+ result
559
+ end
560
+ end
561
+ rescue ActiveRecord::StatementInvalid => e
562
+ raise unless is_cached_plan_failure?(e)
563
+
564
+ # Nothing we can do if we are in a transaction because all commands
565
+ # will raise InFailedSQLTransaction
566
+ if in_transaction?
567
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
568
+ else
569
+ @lock.synchronize do
570
+ # outside of transactions we can simply flush this query and retry
571
+ @statements.delete sql_key(sql)
572
+ end
573
+ retry
574
+ end
575
+ end
576
+
577
+ # Annoyingly, the code for prepared statements whose return value may
578
+ # have changed is FEATURE_NOT_SUPPORTED.
579
+ #
580
+ # This covers various different error types so we need to do additional
581
+ # work to classify the exception definitively as a
582
+ # ActiveRecord::PreparedStatementCacheExpired
583
+ #
584
+ # Check here for more details:
585
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
586
+ CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'
587
+
588
+ def is_cached_plan_failure?(e)
589
+ pgerror = e.cause
590
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
591
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
592
+ rescue StandardError
593
+ false
594
+ end
595
+
596
+ # Returns the statement identifier for the client side cache
597
+ # of statements
598
+ def sql_key(sql)
599
+ "#{schema_search_path}-#{sql}"
600
+ end
601
+
602
+ # Prepare the statement if it hasn't been prepared, return
603
+ # the statement key.
604
+ def prepare_statement(sql, binds, conn)
605
+ sql_key = sql_key(sql)
606
+ unless @statements.key? sql_key
607
+ nextkey = @statements.next_key
608
+ begin
609
+ conn.prepare nextkey, sql
610
+ rescue => e
611
+ raise translate_exception_class(e, sql, binds)
612
+ end
613
+ # Clear the queue
614
+ conn.get_last_result
615
+ @statements[sql_key] = nextkey
616
+ end
617
+ @statements[sql_key]
618
+ end
619
+
620
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
621
+ # connected server's characteristics.
622
+ def connect
623
+ @raw_connection = self.class.new_client(@connection_parameters)
624
+ rescue ConnectionNotEstablished => ex
625
+ raise ex.set_pool(@pool)
626
+ end
627
+
628
+ def reconnect
629
+ begin
630
+ @raw_connection&.reset
631
+ rescue PG::ConnectionBad
632
+ @raw_connection = nil
633
+ end
634
+
635
+ connect unless @raw_connection
636
+ end
637
+
638
+ def reconnect
639
+ begin
640
+ @raw_connection&.reset
641
+ rescue PG::ConnectionBad
642
+ @raw_connection = nil
643
+ end
644
+
645
+ connect unless @raw_connection
646
+ end
647
+
648
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
649
+ # This is called by #connect and should not be called manually.
650
+ def configure_connection
651
+ if @config[:encoding]
652
+ @raw_connection.set_client_encoding(@config[:encoding])
653
+ end
654
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
655
+
656
+ variables = @config.fetch(:variables, {}).stringify_keys
657
+
658
+ # SET statements from :variables config hash
659
+ # https://www.postgresql.org/docs/current/static/sql-set.html
660
+ variables.map do |k, v|
661
+ if [':default', :default].include?(v)
662
+ # Sets the value to the global or compile default
663
+ execute("SET #{k} TO DEFAULT", 'SCHEMA')
664
+ elsif !v.nil?
665
+ execute("SET #{k} TO #{quote(v)}", 'SCHEMA')
666
+ end
667
+ end
668
+
669
+ add_pg_encoders
670
+ add_pg_decoders
671
+
672
+ reload_type_map
673
+ end
674
+
675
+ def reconfigure_connection_timezone
676
+ variables = @config.fetch(:variables, {}).stringify_keys
677
+
678
+ # If it's been directly configured as a connection variable, we don't
679
+ # need to do anything here; it will be set up by configure_connection
680
+ # and then never changed.
681
+ return if variables["timezone"]
682
+
683
+ # If using Active Record's time zone support configure the connection
684
+ # to return TIMESTAMP WITH ZONE types in UTC.
685
+ if default_timezone == :utc
686
+ internal_execute("SET timezone TO 'UTC'")
687
+ else
688
+ internal_execute("SET timezone TO DEFAULT")
689
+ end
690
+ end
691
+
692
+ def last_insert_id_result(sequence_name)
693
+ # :nodoc:
694
+ exec_query("SELECT currval('#{sequence_name}')", 'SQL')
695
+ end
696
+
697
+ # Returns the list of a table's column names, data types, and default values.
698
+ #
699
+ # The underlying query is roughly:
700
+ # SELECT column.name, column.type, default.value
701
+ # FROM column LEFT JOIN default
702
+ # ON column.table_id = default.table_id
703
+ # AND column.num = default.column_num
704
+ # WHERE column.table_id = get_table_id('table_name')
705
+ # AND column.num > 0
706
+ # AND NOT column.is_dropped
707
+ # ORDER BY column.num
708
+ #
709
+ # If the table name is not prefixed with a schema, the database will
710
+ # take the first match from the schema search path.
711
+ #
712
+ # Query implementation notes:
713
+ # - format_type includes the column size constraint, e.g. varchar(50)
714
+ # - ::regclass is a function that gives the id for a table name
715
+ def column_definitions(table_name)
716
+ # :nodoc:
717
+ query(<<-END_SQL, 'SCHEMA')
718
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
719
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
720
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
721
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
722
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
723
+ AND a.attnum > 0 AND NOT a.attisdropped
724
+ ORDER BY a.attnum
725
+ END_SQL
726
+ end
727
+
728
+ def extract_table_ref_from_insert_sql(sql)
729
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
730
+ Regexp.last_match(1)&.strip
731
+ end
732
+
733
+ def arel_visitor
734
+ Arel::Visitors::PostgreSQL.new(self)
735
+ end
736
+
737
+ def build_statement_pool
738
+ StatementPool.new(@raw_connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
739
+ end
740
+
741
+ def can_perform_case_insensitive_comparison_for?(column)
742
+ @case_insensitive_cache ||= {}
743
+ @case_insensitive_cache[column.sql_type] ||= begin
744
+ sql = <<~SQL
745
+ SELECT exists(
746
+ SELECT * FROM pg_proc
747
+ WHERE proname = 'lower'
748
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
749
+ ) OR exists(
750
+ SELECT * FROM pg_proc
751
+ INNER JOIN pg_cast
752
+ ON ARRAY[casttarget]::oidvector = proargtypes
753
+ WHERE proname = 'lower'
754
+ AND castsource = #{quote column.sql_type}::regtype
755
+ )
756
+ SQL
757
+ execute_and_clear(sql, 'SCHEMA', []) do |result|
758
+ result.getvalue(0, 0)
759
+ end
760
+ end
761
+ end
762
+
763
+ def add_pg_encoders
764
+ map = PG::TypeMapByClass.new
765
+ map[Integer] = PG::TextEncoder::Integer.new
766
+ map[TrueClass] = PG::TextEncoder::Boolean.new
767
+ map[FalseClass] = PG::TextEncoder::Boolean.new
768
+ @raw_connection.type_map_for_queries = map
769
+ end
770
+
771
+ def update_typemap_for_default_timezone
772
+ if @raw_connection && @mapped_default_timezone != default_timezone && @timestamp_decoder
773
+ decoder_class = default_timezone == :utc ?
774
+ PG::TextDecoder::TimestampUtc :
775
+ PG::TextDecoder::TimestampWithoutTimeZone
776
+
777
+ @timestamp_decoder = decoder_class.new(**@timestamp_decoder.to_h)
778
+ @raw_connection.type_map_for_results.add_coder(@timestamp_decoder)
779
+
780
+ @mapped_default_timezone = default_timezone
781
+
782
+ # if default timezone has changed, we need to reconfigure the connection
783
+ # (specifically, the session time zone)
784
+ reconfigure_connection_timezone
785
+
786
+ true
787
+ end
788
+ end
789
+
790
+ def add_pg_decoders
791
+ @default_timezone = nil
792
+ @timestamp_decoder = nil
793
+
794
+ coders_by_name = {
795
+ 'int2' => PG::TextDecoder::Integer,
796
+ 'int4' => PG::TextDecoder::Integer,
797
+ 'int8' => PG::TextDecoder::Integer,
798
+ 'oid' => PG::TextDecoder::Integer,
799
+ 'float4' => PG::TextDecoder::Float,
800
+ 'float8' => PG::TextDecoder::Float,
801
+ 'bool' => PG::TextDecoder::Boolean
802
+ }
803
+
804
+ if defined?(PG::TextDecoder::TimestampUtc)
805
+ # Use native PG encoders available since pg-1.1
806
+ coders_by_name['timestamp'] = PG::TextDecoder::TimestampUtc
807
+ coders_by_name['timestamptz'] = PG::TextDecoder::TimestampWithTimeZone
808
+ end
809
+
810
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
811
+ query = <<~SQL % known_coder_types.join(', ')
812
+ SELECT t.oid, t.typname
813
+ FROM pg_type as t
814
+ WHERE t.typname IN (%s)
815
+ SQL
816
+ coders = execute_and_clear(query, 'SCHEMA', [], allow_retry: true, materialize_transactions: false) do |result|
817
+ result.filter_map { |row| construct_coder(row, coders_by_name[row['typname']]) }
818
+ end
819
+
820
+ map = PG::TypeMapByOid.new
821
+ coders.each { |coder| map.add_coder(coder) }
822
+ @raw_connection.type_map_for_results = map
823
+
824
+ @type_map_for_results = PG::TypeMapByOid.new
825
+ @type_map_for_results.default_type_map = map
826
+
827
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
828
+ @timestamp_decoder = coders.find { |coder| coder.name == 'timestamp' }
829
+ update_typemap_for_default_timezone
830
+ end
831
+
832
+ def construct_coder(row, coder_class)
833
+ return unless coder_class
834
+
835
+ coder_class.new(oid: row['oid'].to_i, name: row['typname'])
836
+ end
837
+
838
+ def create_table_definition(*args, **options)
839
+ # :nodoc:
840
+ Redshift::TableDefinition.new(self, *args, **options)
841
+ end
842
+ end
843
+ end
844
+ end