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