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