activerecord-redshift-adapter 0.9.10 → 8.0.0.beta1

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 +7 -0
  2. data/LICENSE +25 -1
  3. data/README.md +28 -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 +768 -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 +847 -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 +847 -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 +846 -0
  72. data/lib/active_record/connection_adapters/redshift_adapter.rb +13 -1282
  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 +112 -98
  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,847 @@
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
+ case exception.message
357
+ when /duplicate key value violates unique constraint/
358
+ RecordNotUnique.new(message, exception)
359
+ when /violates foreign key constraint/
360
+ InvalidForeignKey.new(message, exception)
361
+ else
362
+ super
363
+ end
364
+ end
365
+
366
+ class << self
367
+ def initialize_type_map(m)
368
+ # :nodoc:
369
+ m.register_type 'int2', Type::Integer.new(limit: 2)
370
+ m.register_type 'int4', Type::Integer.new(limit: 4)
371
+ m.register_type 'int8', Type::Integer.new(limit: 8)
372
+ m.alias_type 'oid', 'int2'
373
+ m.register_type 'float4', Type::Float.new
374
+ m.alias_type 'float8', 'float4'
375
+ m.register_type 'text', Type::Text.new
376
+ register_class_with_limit m, 'varchar', Type::String
377
+ m.alias_type 'char', 'varchar'
378
+ m.alias_type 'name', 'varchar'
379
+ m.alias_type 'bpchar', 'varchar'
380
+ m.register_type 'bool', Type::Boolean.new
381
+ m.alias_type 'timestamptz', 'timestamp'
382
+ m.register_type 'date', Type::Date.new
383
+ m.register_type 'time', Type::Time.new
384
+
385
+ m.register_type 'timestamp' do |_, _, sql_type|
386
+ precision = extract_precision(sql_type)
387
+ OID::DateTime.new(precision: precision)
388
+ end
389
+
390
+ m.register_type 'numeric' do |_, fmod, sql_type|
391
+ precision = extract_precision(sql_type)
392
+ scale = extract_scale(sql_type)
393
+
394
+ # The type for the numeric depends on the width of the field,
395
+ # so we'll do something special here.
396
+ #
397
+ # When dealing with decimal columns:
398
+ #
399
+ # places after decimal = fmod - 4 & 0xffff
400
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
401
+ if fmod && (fmod - 4 & 0xffff) == 0
402
+ # FIXME: Remove this class, and the second argument to
403
+ # lookups on PG
404
+ Type::DecimalWithoutScale.new(precision: precision)
405
+ else
406
+ OID::Decimal.new(precision: precision, scale: scale)
407
+ end
408
+ end
409
+ end
410
+ end
411
+
412
+ private
413
+
414
+ def get_oid_type(oid, fmod, column_name, sql_type = '')
415
+ # :nodoc:
416
+ load_additional_types(type_map, [oid]) unless type_map.key?(oid)
417
+
418
+ type_map.fetch(oid, fmod, sql_type) do
419
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
420
+ Type::Value.new.tap do |cast_type|
421
+ type_map.register_type(oid, cast_type)
422
+ end
423
+ end
424
+ end
425
+
426
+ def type_map
427
+ @type_map ||= Type::HashLookupTypeMap.new
428
+ end
429
+
430
+ def initialize_type_map(m = type_map)
431
+ self.class.initialize_type_map(m)
432
+ load_additional_types(m)
433
+ end
434
+
435
+ def extract_limit(sql_type)
436
+ # :nodoc:
437
+ case sql_type
438
+ when /^bigint/i, /^int8/i
439
+ 8
440
+ when /^smallint/i
441
+ 2
442
+ else
443
+ super
444
+ end
445
+ end
446
+
447
+ # Extracts the value from a PostgreSQL column default definition.
448
+ def extract_value_from_default(default)
449
+ # :nodoc:
450
+ case default
451
+ # Quoted types
452
+ when /\A[(B]?'(.*)'::/m
453
+ Regexp.last_match(1).gsub(/''/, "'")
454
+ # Boolean types
455
+ when 'true', 'false'
456
+ default
457
+ # Numeric types
458
+ when /\A\(?(-?\d+(\.\d*)?)\)?\z/
459
+ Regexp.last_match(1)
460
+ # Object identifier types
461
+ when /\A-?\d+\z/
462
+ Regexp.last_match(1)
463
+ else # rubocop:disable Style/EmptyElse
464
+ # Anything else is blank, some user type, or some function
465
+ # and we can't know the value of that, so return nil.
466
+ nil
467
+ end
468
+ end
469
+
470
+ def extract_default_function(default_value, default)
471
+ # :nodoc:
472
+ default if has_default_function?(default_value, default)
473
+ end
474
+
475
+ def has_default_function?(default_value, default)
476
+ # :nodoc:
477
+ !default_value && (/\w+\(.*\)/ === default)
478
+ end
479
+
480
+ def load_additional_types(type_map, oids = nil)
481
+ # :nodoc:
482
+ initializer = OID::TypeMapInitializer.new(type_map)
483
+
484
+ load_types_queries(initializer, oids) do |query|
485
+ execute_and_clear(query, 'SCHEMA', [], allow_retry: true, materialize_transactions: false) do |records|
486
+ initializer.run(records)
487
+ end
488
+ end
489
+ end
490
+
491
+ def load_types_queries(_initializer, oids)
492
+ query =
493
+ if supports_ranges?
494
+ <<-SQL
495
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
496
+ FROM pg_type as t
497
+ LEFT JOIN pg_range as r ON oid = rngtypid
498
+ SQL
499
+ else
500
+ <<-SQL
501
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
502
+ FROM pg_type as t
503
+ SQL
504
+ end
505
+
506
+ if oids
507
+ yield query + 'WHERE t.oid::integer IN (%s)' % oids.join(', ')
508
+ else
509
+ yield query
510
+ end
511
+ end
512
+
513
+ FEATURE_NOT_SUPPORTED = '0A000' # :nodoc:
514
+
515
+ def execute_and_clear(sql, name, binds, prepare: false, async: false, allow_retry: false, materialize_transactions: true)
516
+ sql = transform_query(sql)
517
+ check_if_write_query(sql)
518
+
519
+ if !prepare || without_prepared_statement?(binds)
520
+ result = exec_no_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
521
+ else
522
+ result = exec_cache(sql, name, binds, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
523
+ end
524
+ begin
525
+ ret = yield result
526
+ ensure
527
+ result.clear
528
+ end
529
+ ret
530
+ end
531
+
532
+ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
533
+ mark_transaction_written_if_write(sql)
534
+
535
+ # make sure we carry over any changes to ActiveRecord.default_timezone that have been
536
+ # made since we established the connection
537
+ update_typemap_for_default_timezone
538
+
539
+ type_casted_binds = type_casted_binds(binds)
540
+ log(sql, name, binds, type_casted_binds, async: async) do
541
+ with_raw_connection do |conn|
542
+ result = conn.exec_params(sql, type_casted_binds)
543
+ verified!
544
+ result
545
+ end
546
+ end
547
+ end
548
+
549
+ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:)
550
+ mark_transaction_written_if_write(sql)
551
+
552
+ update_typemap_for_default_timezone
553
+
554
+ with_raw_connection do |conn|
555
+ stmt_key = prepare_statement(sql, binds, conn)
556
+ type_casted_binds = type_casted_binds(binds)
557
+
558
+ log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
559
+ result = conn.exec_prepared(stmt_key, type_casted_binds)
560
+ verified!
561
+ result
562
+ end
563
+ end
564
+ rescue ActiveRecord::StatementInvalid => e
565
+ raise unless is_cached_plan_failure?(e)
566
+
567
+ # Nothing we can do if we are in a transaction because all commands
568
+ # will raise InFailedSQLTransaction
569
+ if in_transaction?
570
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
571
+ else
572
+ @lock.synchronize do
573
+ # outside of transactions we can simply flush this query and retry
574
+ @statements.delete sql_key(sql)
575
+ end
576
+ retry
577
+ end
578
+ end
579
+
580
+ # Annoyingly, the code for prepared statements whose return value may
581
+ # have changed is FEATURE_NOT_SUPPORTED.
582
+ #
583
+ # This covers various different error types so we need to do additional
584
+ # work to classify the exception definitively as a
585
+ # ActiveRecord::PreparedStatementCacheExpired
586
+ #
587
+ # Check here for more details:
588
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
589
+ CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'
590
+
591
+ def is_cached_plan_failure?(e)
592
+ pgerror = e.cause
593
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
594
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
595
+ rescue StandardError
596
+ false
597
+ end
598
+
599
+ # Returns the statement identifier for the client side cache
600
+ # of statements
601
+ def sql_key(sql)
602
+ "#{schema_search_path}-#{sql}"
603
+ end
604
+
605
+ # Prepare the statement if it hasn't been prepared, return
606
+ # the statement key.
607
+ def prepare_statement(sql, binds, conn)
608
+ sql_key = sql_key(sql)
609
+ unless @statements.key? sql_key
610
+ nextkey = @statements.next_key
611
+ begin
612
+ conn.prepare nextkey, sql
613
+ rescue => e
614
+ raise translate_exception_class(e, sql, binds)
615
+ end
616
+ # Clear the queue
617
+ conn.get_last_result
618
+ @statements[sql_key] = nextkey
619
+ end
620
+ @statements[sql_key]
621
+ end
622
+
623
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
624
+ # connected server's characteristics.
625
+ def connect
626
+ @raw_connection = self.class.new_client(@connection_parameters)
627
+ rescue ConnectionNotEstablished => ex
628
+ raise ex.set_pool(@pool)
629
+ end
630
+
631
+ def reconnect
632
+ begin
633
+ @raw_connection&.reset
634
+ rescue PG::ConnectionBad
635
+ @raw_connection = nil
636
+ end
637
+
638
+ connect unless @raw_connection
639
+ end
640
+
641
+ def reconnect
642
+ begin
643
+ @raw_connection&.reset
644
+ rescue PG::ConnectionBad
645
+ @raw_connection = nil
646
+ end
647
+
648
+ connect unless @raw_connection
649
+ end
650
+
651
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
652
+ # This is called by #connect and should not be called manually.
653
+ def configure_connection
654
+ if @config[:encoding]
655
+ @raw_connection.set_client_encoding(@config[:encoding])
656
+ end
657
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
658
+
659
+ variables = @config.fetch(:variables, {}).stringify_keys
660
+
661
+ # SET statements from :variables config hash
662
+ # https://www.postgresql.org/docs/current/static/sql-set.html
663
+ variables.map do |k, v|
664
+ if [':default', :default].include?(v)
665
+ # Sets the value to the global or compile default
666
+ execute("SET #{k} TO DEFAULT", 'SCHEMA')
667
+ elsif !v.nil?
668
+ execute("SET #{k} TO #{quote(v)}", 'SCHEMA')
669
+ end
670
+ end
671
+
672
+ add_pg_encoders
673
+ add_pg_decoders
674
+
675
+ reload_type_map
676
+ end
677
+
678
+ def reconfigure_connection_timezone
679
+ variables = @config.fetch(:variables, {}).stringify_keys
680
+
681
+ # If it's been directly configured as a connection variable, we don't
682
+ # need to do anything here; it will be set up by configure_connection
683
+ # and then never changed.
684
+ return if variables["timezone"]
685
+
686
+ # If using Active Record's time zone support configure the connection
687
+ # to return TIMESTAMP WITH ZONE types in UTC.
688
+ if default_timezone == :utc
689
+ internal_execute("SET timezone TO 'UTC'")
690
+ else
691
+ internal_execute("SET timezone TO DEFAULT")
692
+ end
693
+ end
694
+
695
+ def last_insert_id_result(sequence_name)
696
+ # :nodoc:
697
+ exec_query("SELECT currval('#{sequence_name}')", 'SQL')
698
+ end
699
+
700
+ # Returns the list of a table's column names, data types, and default values.
701
+ #
702
+ # The underlying query is roughly:
703
+ # SELECT column.name, column.type, default.value
704
+ # FROM column LEFT JOIN default
705
+ # ON column.table_id = default.table_id
706
+ # AND column.num = default.column_num
707
+ # WHERE column.table_id = get_table_id('table_name')
708
+ # AND column.num > 0
709
+ # AND NOT column.is_dropped
710
+ # ORDER BY column.num
711
+ #
712
+ # If the table name is not prefixed with a schema, the database will
713
+ # take the first match from the schema search path.
714
+ #
715
+ # Query implementation notes:
716
+ # - format_type includes the column size constraint, e.g. varchar(50)
717
+ # - ::regclass is a function that gives the id for a table name
718
+ def column_definitions(table_name)
719
+ # :nodoc:
720
+ query(<<-END_SQL, 'SCHEMA')
721
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
722
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
723
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
724
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
725
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
726
+ AND a.attnum > 0 AND NOT a.attisdropped
727
+ ORDER BY a.attnum
728
+ END_SQL
729
+ end
730
+
731
+ def extract_table_ref_from_insert_sql(sql)
732
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
733
+ Regexp.last_match(1)&.strip
734
+ end
735
+
736
+ def arel_visitor
737
+ Arel::Visitors::PostgreSQL.new(self)
738
+ end
739
+
740
+ def build_statement_pool
741
+ StatementPool.new(@raw_connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
742
+ end
743
+
744
+ def can_perform_case_insensitive_comparison_for?(column)
745
+ @case_insensitive_cache ||= {}
746
+ @case_insensitive_cache[column.sql_type] ||= begin
747
+ sql = <<~SQL
748
+ SELECT exists(
749
+ SELECT * FROM pg_proc
750
+ WHERE proname = 'lower'
751
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
752
+ ) OR exists(
753
+ SELECT * FROM pg_proc
754
+ INNER JOIN pg_cast
755
+ ON ARRAY[casttarget]::oidvector = proargtypes
756
+ WHERE proname = 'lower'
757
+ AND castsource = #{quote column.sql_type}::regtype
758
+ )
759
+ SQL
760
+ execute_and_clear(sql, 'SCHEMA', []) do |result|
761
+ result.getvalue(0, 0)
762
+ end
763
+ end
764
+ end
765
+
766
+ def add_pg_encoders
767
+ map = PG::TypeMapByClass.new
768
+ map[Integer] = PG::TextEncoder::Integer.new
769
+ map[TrueClass] = PG::TextEncoder::Boolean.new
770
+ map[FalseClass] = PG::TextEncoder::Boolean.new
771
+ @raw_connection.type_map_for_queries = map
772
+ end
773
+
774
+ def update_typemap_for_default_timezone
775
+ if @raw_connection && @mapped_default_timezone != default_timezone && @timestamp_decoder
776
+ decoder_class = default_timezone == :utc ?
777
+ PG::TextDecoder::TimestampUtc :
778
+ PG::TextDecoder::TimestampWithoutTimeZone
779
+
780
+ @timestamp_decoder = decoder_class.new(**@timestamp_decoder.to_h)
781
+ @raw_connection.type_map_for_results.add_coder(@timestamp_decoder)
782
+
783
+ @mapped_default_timezone = default_timezone
784
+
785
+ # if default timezone has changed, we need to reconfigure the connection
786
+ # (specifically, the session time zone)
787
+ reconfigure_connection_timezone
788
+
789
+ true
790
+ end
791
+ end
792
+
793
+ def add_pg_decoders
794
+ @default_timezone = nil
795
+ @timestamp_decoder = nil
796
+
797
+ coders_by_name = {
798
+ 'int2' => PG::TextDecoder::Integer,
799
+ 'int4' => PG::TextDecoder::Integer,
800
+ 'int8' => PG::TextDecoder::Integer,
801
+ 'oid' => PG::TextDecoder::Integer,
802
+ 'float4' => PG::TextDecoder::Float,
803
+ 'float8' => PG::TextDecoder::Float,
804
+ 'bool' => PG::TextDecoder::Boolean
805
+ }
806
+
807
+ if defined?(PG::TextDecoder::TimestampUtc)
808
+ # Use native PG encoders available since pg-1.1
809
+ coders_by_name['timestamp'] = PG::TextDecoder::TimestampUtc
810
+ coders_by_name['timestamptz'] = PG::TextDecoder::TimestampWithTimeZone
811
+ end
812
+
813
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
814
+ query = <<~SQL % known_coder_types.join(', ')
815
+ SELECT t.oid, t.typname
816
+ FROM pg_type as t
817
+ WHERE t.typname IN (%s)
818
+ SQL
819
+ coders = execute_and_clear(query, 'SCHEMA', [], allow_retry: true, materialize_transactions: false) do |result|
820
+ result.filter_map { |row| construct_coder(row, coders_by_name[row['typname']]) }
821
+ end
822
+
823
+ map = PG::TypeMapByOid.new
824
+ coders.each { |coder| map.add_coder(coder) }
825
+ @raw_connection.type_map_for_results = map
826
+
827
+ @type_map_for_results = PG::TypeMapByOid.new
828
+ @type_map_for_results.default_type_map = map
829
+
830
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
831
+ @timestamp_decoder = coders.find { |coder| coder.name == 'timestamp' }
832
+ update_typemap_for_default_timezone
833
+ end
834
+
835
+ def construct_coder(row, coder_class)
836
+ return unless coder_class
837
+
838
+ coder_class.new(oid: row['oid'].to_i, name: row['typname'])
839
+ end
840
+
841
+ def create_table_definition(*args, **options)
842
+ # :nodoc:
843
+ Redshift::TableDefinition.new(self, *args, **options)
844
+ end
845
+ end
846
+ end
847
+ end