activerecord-redshift-adapter 0.9.12 → 8.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +5 -13
  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 -1286
  73. data/lib/active_record/tasks/redshift_7_0_tasks.rb +148 -0
  74. data/lib/active_record/tasks/redshift_7_1_tasks.rb +151 -0
  75. data/lib/active_record/tasks/redshift_7_2_tasks.rb +151 -0
  76. data/lib/active_record/tasks/redshift_8_0_tasks.rb +151 -0
  77. data/lib/active_record/tasks/redshift_tasks.rb +13 -0
  78. data/lib/activerecord-redshift-adapter.rb +13 -0
  79. metadata +110 -84
  80. data/.gitignore +0 -26
  81. data/Gemfile +0 -14
  82. data/Rakefile +0 -26
  83. data/activerecord-redshift-adapter.gemspec +0 -24
  84. data/lib/activerecord_redshift/table_manager.rb +0 -230
  85. data/lib/activerecord_redshift_adapter/version.rb +0 -4
  86. data/lib/activerecord_redshift_adapter.rb +0 -4
  87. data/lib/monkeypatch_activerecord.rb +0 -195
  88. data/lib/monkeypatch_arel.rb +0 -96
  89. data/spec/active_record/base_spec.rb +0 -37
  90. data/spec/active_record/connection_adapters/redshift_adapter_spec.rb +0 -97
  91. data/spec/dummy/config/database.example.yml +0 -12
  92. data/spec/spec_helper.rb +0 -33
@@ -0,0 +1,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