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
@@ -1,1286 +1,17 @@
1
- require 'active_record/connection_adapters/abstract_adapter'
2
- require 'active_support/core_ext/object/blank'
3
- require 'active_record/connection_adapters/statement_pool'
4
- require 'arel/visitors/bind_visitor'
1
+ # frozen_string_literal: true
5
2
 
6
- # Make sure we're using pg high enough for PGResult#values
7
- gem 'pg', '~> 0.11'
8
3
  require 'pg'
9
4
 
10
- module ActiveRecord
11
- class Base
12
- # Establishes a connection to the database that's used by all Active Record objects
13
- def self.redshift_connection(config) # :nodoc:
14
- config = config.symbolize_keys
15
- host = config[:host]
16
- port = config[:port] || 5432
17
- username = config[:username].to_s if config[:username]
18
- password = config[:password].to_s if config[:password]
19
-
20
- if config.key?(:database)
21
- database = config[:database]
22
- else
23
- raise ArgumentError, "No database specified. Missing argument: database."
24
- end
25
-
26
- # The postgres drivers don't allow the creation of an unconnected PGconn object,
27
- # so just pass a nil connection object for the time being.
28
- ConnectionAdapters::RedshiftAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
29
- end
30
- end
31
-
32
- module ConnectionAdapters
33
- # Redshift-specific extensions to column definitions in a table.
34
- class RedshiftColumn < Column #:nodoc:
35
- attr_accessor :timezone
36
- # Instantiates a new Redshift column definition in a table.
37
- def initialize(name, default, sql_type = nil, null = true, timezone = nil)
38
- self.timezone = timezone
39
- super(name, self.class.extract_value_from_default(default), sql_type, null)
40
- end
41
-
42
- def ==(other)
43
- name == other.name && default == other.default && sql_type == other.sql_type && null == other.null
44
- end
45
-
46
- def type_cast(value)
47
- return nil if value.nil?
48
- return coder.load(value) if encoded?
49
-
50
- if timezone && [:datetime, :timestamp].include?(type)
51
- self.class.string_to_time_with_timezone(string, timezone)
52
- else
53
- super
54
- end
55
- end
56
-
57
- def type_cast_code(var_name)
58
- if timezone && [:datetime, :timestamp].include?(type)
59
- "#{self.class.name}.string_to_time_with_timezone(#{var_name},'#{timezone}')"
60
- else
61
- super
62
- end
63
- end
64
-
65
- # :stopdoc:
66
- class << self
67
- attr_accessor :money_precision
68
- def string_to_time(string)
69
- return string unless String === string
70
-
71
- case string
72
- when 'infinity' then 1.0 / 0.0
73
- when '-infinity' then -1.0 / 0.0
74
- else
75
- super
76
- end
77
- end
78
-
79
- def string_to_time_with_timezone(string, timezone)
80
- if string =~ self::Format::ISO_DATETIME
81
- Time.parse("#{string} #{timezone}")
82
- else
83
- string_to_time(string)
84
- end
85
- end
86
- end
87
- # :startdoc:
88
-
89
- private
90
- def extract_limit(sql_type)
91
- case sql_type
92
- when /^bigint/i; 8
93
- when /^smallint/i; 2
94
- else super
95
- end
96
- end
97
-
98
- # Extracts the scale from Redshift-specific data types.
99
- def extract_scale(sql_type)
100
- # Money type has a fixed scale of 2.
101
- sql_type =~ /^money/ ? 2 : super
102
- end
103
-
104
- # Extracts the precision from Redshift-specific data types.
105
- def extract_precision(sql_type)
106
- if sql_type == 'money'
107
- self.class.money_precision
108
- else
109
- super
110
- end
111
- end
112
-
113
- # Maps Redshift-specific data types to logical Rails types.
114
- def simplified_type(field_type)
115
- case field_type
116
- # Numeric and monetary types
117
- when /^(?:real|double precision)$/
118
- :float
119
- # Monetary types
120
- when 'money'
121
- :decimal
122
- # Character types
123
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
124
- :string
125
- # Binary data types
126
- when 'bytea'
127
- :binary
128
- # Date/time types
129
- when /^timestamp with(?:out)? time zone$/
130
- :datetime
131
- when 'interval'
132
- :string
133
- # Geometric types
134
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
135
- :string
136
- # Network address types
137
- when /^(?:cidr|inet|macaddr)$/
138
- :string
139
- # Bit strings
140
- when /^bit(?: varying)?(?:\(\d+\))?$/
141
- :string
142
- # XML type
143
- when 'xml'
144
- :xml
145
- # tsvector type
146
- when 'tsvector'
147
- :tsvector
148
- # Arrays
149
- when /^\D+\[\]$/
150
- :string
151
- # Object identifier types
152
- when 'oid'
153
- :integer
154
- # UUID type
155
- when 'uuid'
156
- :string
157
- # Small and big integer types
158
- when /^(?:small|big)int$/
159
- :integer
160
- # Pass through all types that are not specific to Redshift.
161
- else
162
- super
163
- end
164
- end
165
-
166
- # Extracts the value from a Redshift column default definition.
167
- def self.extract_value_from_default(default)
168
- case default
169
- # This is a performance optimization for Ruby 1.9.2 in development.
170
- # If the value is nil, we return nil straight away without checking
171
- # the regular expressions. If we check each regular expression,
172
- # Regexp#=== will call NilClass#to_str, which will trigger
173
- # method_missing (defined by whiny nil in ActiveSupport) which
174
- # makes this method very very slow.
175
- when NilClass
176
- nil
177
- # Numeric types
178
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
179
- $1
180
- # Character types
181
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
182
- $1
183
- # Binary data types
184
- when /\A'(.*)'::bytea\z/m
185
- $1
186
- # Date/time types
187
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
188
- $1
189
- when /\A'(.*)'::interval\z/
190
- $1
191
- # Boolean type
192
- when 'true'
193
- true
194
- when 'false'
195
- false
196
- # Geometric types
197
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
198
- $1
199
- # Network address types
200
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
201
- $1
202
- # Bit string types
203
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
204
- $1
205
- # XML type
206
- when /\A'(.*)'::xml\z/m
207
- $1
208
- # Arrays
209
- when /\A'(.*)'::"?\D+"?\[\]\z/
210
- $1
211
- # Object identifier types
212
- when /\A-?\d+\z/
213
- $1
214
- else
215
- # Anything else is blank, some user type, or some function
216
- # and we can't know the value of that, so return nil.
217
- nil
218
- end
219
- end
220
- end
221
-
222
- # The Redshift adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
223
- # Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
224
- #
225
- # Options:
226
- #
227
- # * <tt>:host</tt> - Defaults to "localhost".
228
- # * <tt>:port</tt> - Defaults to 5432.
229
- # * <tt>:username</tt> - Defaults to nothing.
230
- # * <tt>:password</tt> - Defaults to nothing.
231
- # * <tt>:database</tt> - The name of the database. No default, must be provided.
232
- # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
233
- # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
234
- # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
235
- # <encoding></tt> call on the connection.
236
- class RedshiftAdapter < AbstractAdapter
237
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
238
- def xml(*args)
239
- options = args.extract_options!
240
- column(args[0], 'xml', options)
241
- end
242
-
243
- def tsvector(*args)
244
- options = args.extract_options!
245
- column(args[0], 'tsvector', options)
246
- end
247
- end
248
-
249
- ADAPTER_NAME = 'Redshift'
250
-
251
- NATIVE_DATABASE_TYPES = {
252
- :primary_key => "bigint primary key",
253
- :identity => "bigint identity primary key",
254
- :string => { :name => "character varying", :limit => 255 },
255
- :text => { :name => "text" },
256
- :integer => { :name => "integer" },
257
- :float => { :name => "float" },
258
- :decimal => { :name => "decimal" },
259
- :datetime => { :name => "timestamp" },
260
- :timestamp => { :name => "timestamp" },
261
- :time => { :name => "time" },
262
- :date => { :name => "date" },
263
- :binary => { :name => "bytea" },
264
- :boolean => { :name => "boolean" },
265
- :xml => { :name => "xml" },
266
- :tsvector => { :name => "tsvector" }
267
- }
268
-
269
- # Returns 'Redshift' as adapter name for identification purposes.
270
- def adapter_name
271
- ADAPTER_NAME
272
- end
273
-
274
- # Returns +true+, since this connection adapter supports prepared statement
275
- # caching.
276
- def supports_statement_cache?
277
- true
278
- end
279
-
280
- def supports_index_sort_order?
281
- true
282
- end
283
-
284
- class StatementPool < ConnectionAdapters::StatementPool
285
- def initialize(connection, max)
286
- super
287
- @counter = 0
288
- @cache = Hash.new { |h,pid| h[pid] = {} }
289
- end
290
-
291
- def each(&block); cache.each(&block); end
292
- def key?(key); cache.key?(key); end
293
- def [](key); cache[key]; end
294
- def length; cache.length; end
295
-
296
- def next_key
297
- "a#{@counter + 1}"
298
- end
299
-
300
- def []=(sql, key)
301
- while @max <= cache.size
302
- dealloc(cache.shift.last)
303
- end
304
- @counter += 1
305
- cache[sql] = key
306
- end
307
-
308
- def clear
309
- cache.each_value do |stmt_key|
310
- dealloc stmt_key
311
- end
312
- cache.clear
313
- end
314
-
315
- def delete(sql_key)
316
- dealloc cache[sql_key]
317
- cache.delete sql_key
318
- end
319
-
320
- private
321
- def cache
322
- @cache[$$]
323
- end
324
-
325
- def dealloc(key)
326
- @connection.query "DEALLOCATE #{key}" if connection_active?
327
- end
328
-
329
- def connection_active?
330
- @connection.status == PGconn::CONNECTION_OK
331
- rescue PGError
332
- false
333
- end
334
- end
335
-
336
- class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
337
- include Arel::Visitors::BindVisitor
338
- end
339
-
340
- # Initializes and connects a Redshift adapter.
341
- def initialize(connection, logger, connection_parameters, config)
342
- super(connection, logger)
343
-
344
- if config.fetch(:prepared_statements) { true }
345
- @visitor = Arel::Visitors::PostgreSQL.new self
346
- else
347
- @visitor = BindSubstitution.new self
348
- end
349
-
350
- connection_parameters.delete :prepared_statements
351
-
352
- @connection_parameters, @config = connection_parameters, config
353
-
354
- # @local_tz is initialized as nil to avoid warnings when connect tries to use it
355
- @local_tz = nil
356
- @table_alias_length = nil
357
-
358
- connect
359
- @statements = StatementPool.new @connection,
360
- config.fetch(:statement_limit) { 1000 }
361
-
362
- if redshift_version < 80002
363
- raise "Your version of Redshift (#{redshift_version}) is too old, please upgrade!"
364
- end
365
-
366
- @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
367
- end
368
-
369
- # Clears the prepared statements cache.
370
- def clear_cache!
371
- @statements.clear
372
- end
373
-
374
- # Is this connection alive and ready for queries?
375
- def active?
376
- @connection.query 'SELECT 1'
377
- true
378
- rescue PGError
379
- false
380
- end
381
-
382
- # Close then reopen the connection.
383
- def reconnect!
384
- clear_cache!
385
- @connection.reset
386
- @open_transactions = 0
387
- configure_connection
388
- end
389
-
390
- def reset!
391
- clear_cache!
392
- super
393
- end
394
-
395
- # Disconnects from the database if already connected. Otherwise, this
396
- # method does nothing.
397
- def disconnect!
398
- clear_cache!
399
- @connection.close rescue nil
400
- end
401
-
402
- def native_database_types #:nodoc:
403
- NATIVE_DATABASE_TYPES
404
- end
405
-
406
- # Returns true, since this connection adapter supports migrations.
407
- def supports_migrations?
408
- true
409
- end
410
-
411
- # Does Redshift support finding primary key on non-Active Record tables?
412
- def supports_primary_key? #:nodoc:
413
- true
414
- end
415
-
416
- def supports_insert_with_returning?
417
- false
418
- end
419
-
420
- def supports_ddl_transactions?
421
- true
422
- end
423
-
424
- # Returns true, since this connection adapter supports savepoints.
425
- def supports_savepoints?
426
- true
427
- end
428
-
429
- # Returns true.
430
- def supports_explain?
431
- true
432
- end
433
-
434
- # Returns the configured supported identifier length supported by Redshift
435
- def table_alias_length
436
- @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
437
- end
438
-
439
- # QUOTING ==================================================
440
-
441
- # Escapes binary strings for bytea input to the database.
442
- def escape_bytea(value)
443
- @connection.escape_bytea(value) if value
444
- end
445
-
446
- # Unescapes bytea output from a database to the binary string it represents.
447
- # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
448
- # on escaped binary output from database drive.
449
- def unescape_bytea(value)
450
- @connection.unescape_bytea(value) if value
451
- end
452
-
453
- # Quotes Redshift-specific data types for SQL input.
454
- def quote(value, column = nil) #:nodoc:
455
- return super unless column
456
-
457
- case value
458
- when Float
459
- return super unless value.infinite? && column.type == :datetime
460
- "'#{value.to_s.downcase}'"
461
- when Numeric
462
- return super unless column.sql_type == 'money'
463
- # Not truly string input, so doesn't require (or allow) escape string syntax.
464
- "'#{value}'"
465
- when String
466
- case column.sql_type
467
- when 'bytea' then "'#{escape_bytea(value)}'"
468
- when 'xml' then "xml '#{quote_string(value)}'"
469
- when /^bit/
470
- case value
471
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
472
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
473
- end
474
- else
475
- super
476
- end
477
- else
478
- super
479
- end
480
- end
481
-
482
- def type_cast(value, column)
483
- return super unless column
484
-
485
- case value
486
- when String
487
- return super unless 'bytea' == column.sql_type
488
- { :value => value, :format => 1 }
489
- else
490
- super
491
- end
492
- end
493
-
494
- # Quotes strings for use in SQL input.
495
- def quote_string(s) #:nodoc:
496
- @connection.escape(s)
497
- end
498
-
499
- # Checks the following cases:
500
- #
501
- # - table_name
502
- # - "table.name"
503
- # - schema_name.table_name
504
- # - schema_name."table.name"
505
- # - "schema.name".table_name
506
- # - "schema.name"."table.name"
507
- def quote_table_name(name)
508
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
509
-
510
- unless name_part
511
- quote_column_name(schema)
512
- else
513
- table_name, name_part = extract_pg_identifier_from_name(name_part)
514
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
515
- end
516
- end
517
-
518
- # Quotes column names for use in SQL queries.
519
- def quote_column_name(name) #:nodoc:
520
- PGconn.quote_ident(name.to_s)
521
- end
522
-
523
- # Quote date/time values for use in SQL input. Includes microseconds
524
- # if the value is a Time responding to usec.
525
- def quoted_date(value) #:nodoc:
526
- if value.acts_like?(:time) && value.respond_to?(:usec)
527
- "#{super}.#{sprintf("%06d", value.usec)}"
528
- else
529
- super
530
- end
531
- end
532
-
533
- # Set the authorized user for this session
534
- def session_auth=(user)
535
- clear_cache!
536
- exec_query "SET SESSION AUTHORIZATION #{user}"
537
- end
538
-
539
- # REFERENTIAL INTEGRITY ====================================
540
-
541
- def supports_disable_referential_integrity? #:nodoc:
542
- false
543
- end
544
-
545
- def disable_referential_integrity #:nodoc:
546
- if supports_disable_referential_integrity? then
547
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
548
- end
549
- yield
550
- ensure
551
- if supports_disable_referential_integrity? then
552
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
553
- end
554
- end
555
-
556
- # DATABASE STATEMENTS ======================================
557
-
558
- def explain(arel, binds = [])
559
- sql = "EXPLAIN #{to_sql(arel, binds)}"
560
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
561
- end
562
-
563
- class ExplainPrettyPrinter # :nodoc:
564
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
565
- # Redshift shell:
566
- #
567
- # QUERY PLAN
568
- # ------------------------------------------------------------------------------
569
- # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
570
- # Join Filter: (posts.user_id = users.id)
571
- # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
572
- # Index Cond: (id = 1)
573
- # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
574
- # Filter: (posts.user_id = 1)
575
- # (6 rows)
576
- #
577
- def pp(result)
578
- header = result.columns.first
579
- lines = result.rows.map(&:first)
580
-
581
- # We add 2 because there's one char of padding at both sides, note
582
- # the extra hyphens in the example above.
583
- width = [header, *lines].map(&:length).max + 2
584
-
585
- pp = []
586
-
587
- pp << header.center(width).rstrip
588
- pp << '-' * width
589
-
590
- pp += lines.map {|line| " #{line}"}
591
-
592
- nrows = result.rows.length
593
- rows_label = nrows == 1 ? 'row' : 'rows'
594
- pp << "(#{nrows} #{rows_label})"
595
-
596
- pp.join("\n") + "\n"
597
- end
598
- end
599
-
600
- # Executes a SELECT query and returns an array of rows. Each row is an
601
- # array of field values.
602
- def select_rows(sql, name = nil)
603
- select_raw(sql, name).last
604
- end
605
-
606
- # Executes an INSERT query and returns the new record's ID
607
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
608
- unless pk
609
- # Extract the table from the insert sql. Yuck.
610
- table_ref = extract_table_ref_from_insert_sql(sql)
611
- pk = primary_key(table_ref) if table_ref
612
- end
613
-
614
- if pk && use_insert_returning?
615
- select_value("#{sql} RETURNING #{quote_column_name(pk)}")
616
- elsif pk
617
- super
618
- last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk))
619
- else
620
- super
621
- end
622
- end
623
- alias :create :insert
624
-
625
- # create a 2D array representing the result set
626
- def result_as_array(res) #:nodoc:
627
- # check if we have any binary column and if they need escaping
628
- ftypes = Array.new(res.nfields) do |i|
629
- [i, res.ftype(i)]
630
- end
631
-
632
- rows = res.values
633
- return rows unless ftypes.any? { |_, x|
634
- x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
635
- }
636
-
637
- typehash = ftypes.group_by { |_, type| type }
638
- binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
639
- monies = typehash[MONEY_COLUMN_TYPE_OID] || []
640
-
641
- rows.each do |row|
642
- # unescape string passed BYTEA field (OID == 17)
643
- binaries.each do |index, _|
644
- row[index] = unescape_bytea(row[index])
645
- end
646
-
647
- # If this is a money type column and there are any currency symbols,
648
- # then strip them off. Indeed it would be prettier to do this in
649
- # RedshiftColumn.string_to_decimal but would break form input
650
- # fields that call value_before_type_cast.
651
- monies.each do |index, _|
652
- data = row[index]
653
- # Because money output is formatted according to the locale, there are two
654
- # cases to consider (note the decimal separators):
655
- # (1) $12,345,678.12
656
- # (2) $12.345.678,12
657
- case data
658
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
659
- data.gsub!(/[^-\d.]/, '')
660
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
661
- data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
662
- end
663
- end
664
- end
665
- end
666
-
667
-
668
- # Queries the database and returns the results in an Array-like object
669
- def query(sql, name = nil) #:nodoc:
670
- log(sql, name) do
671
- result_as_array @connection.async_exec(sql)
672
- end
673
- end
674
-
675
- # Executes an SQL statement, returning a PGresult object on success
676
- # or raising a PGError exception otherwise.
677
- def execute(sql, name = nil)
678
- log(sql, name) do
679
- @connection.async_exec(sql)
680
- end
681
- end
682
-
683
- def substitute_at(column, index)
684
- Arel::Nodes::BindParam.new "$#{index + 1}"
685
- end
686
-
687
- def exec_query(sql, name = 'SQL', binds = [])
688
- log(sql, name, binds) do
689
- result = binds.empty? ? exec_no_cache(sql, binds) :
690
- exec_cache(sql, binds)
691
-
692
- ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
693
- result.clear
694
- return ret
695
- end
696
- end
697
-
698
- def exec_delete(sql, name = 'SQL', binds = [])
699
- log(sql, name, binds) do
700
- result = binds.empty? ? exec_no_cache(sql, binds) :
701
- exec_cache(sql, binds)
702
- affected = result.cmd_tuples
703
- result.clear
704
- affected
705
- end
706
- end
707
- alias :exec_update :exec_delete
708
-
709
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
710
- unless pk
711
- # Extract the table from the insert sql. Yuck.
712
- table_ref = extract_table_ref_from_insert_sql(sql)
713
- pk = primary_key(table_ref) if table_ref
714
- end
715
-
716
- sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk && use_insert_returning?
717
-
718
- [sql, binds]
719
- end
720
-
721
- # Executes an UPDATE query and returns the number of affected tuples.
722
- def update_sql(sql, name = nil)
723
- super.cmd_tuples
724
- end
725
-
726
- # Begins a transaction.
727
- def begin_db_transaction
728
- execute "BEGIN"
729
- end
730
-
731
- # Commits a transaction.
732
- def commit_db_transaction
733
- execute "COMMIT"
734
- end
735
-
736
- # Aborts a transaction.
737
- def rollback_db_transaction
738
- execute "ROLLBACK"
739
- end
740
-
741
- def outside_transaction?
742
- @connection.transaction_status == PGconn::PQTRANS_IDLE
743
- end
744
-
745
- def create_savepoint
746
- execute("SAVEPOINT #{current_savepoint_name}")
747
- end
748
-
749
- def rollback_to_savepoint
750
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
751
- end
752
-
753
- def release_savepoint
754
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
755
- end
756
-
757
- def use_insert_returning?
758
- @use_insert_returning
759
- end
760
-
761
- # SCHEMA STATEMENTS ========================================
762
-
763
- # Drops the database specified on the +name+ attribute
764
- # and creates it again using the provided +options+.
765
- def recreate_database(name, options = {}) #:nodoc:
766
- drop_database(name)
767
- create_database(name, options)
768
- end
769
-
770
- # Create a new Redshift database. Options include <tt>:owner</tt>, <tt>:template</tt>,
771
- # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
772
- # <tt>:charset</tt> while Redshift uses <tt>:encoding</tt>).
773
- #
774
- # Example:
775
- # create_database config[:database], config
776
- # create_database 'foo_development', :encoding => 'unicode'
777
- def create_database(name, options = {})
778
- options = options.reverse_merge(:encoding => "utf8")
779
-
780
- option_string = options.symbolize_keys.sum do |key, value|
781
- case key
782
- when :owner
783
- " OWNER = \"#{value}\""
784
- when :template
785
- " TEMPLATE = \"#{value}\""
786
- when :encoding
787
- " ENCODING = '#{value}'"
788
- when :tablespace
789
- " TABLESPACE = \"#{value}\""
790
- when :connection_limit
791
- " CONNECTION LIMIT = #{value}"
792
- else
793
- ""
794
- end
795
- end
796
-
797
- execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
798
- end
799
-
800
- # Drops a Redshift database.
801
- #
802
- # Example:
803
- # drop_database 'matt_development'
804
- def drop_database(name) #:nodoc:
805
- execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
806
- end
807
-
808
- # Returns the list of all tables in the schema search path or a specified schema.
809
- def tables(name = nil)
810
- query(<<-SQL, 'SCHEMA').map { |row| "#{row[0]}.#{row[1]}" }
811
- SELECT schemaname, tablename
812
- FROM pg_tables
813
- WHERE schemaname = ANY (current_schemas(false))
814
- SQL
815
- end
816
-
817
- # Returns true if table exists.
818
- # If the schema is not specified as part of +name+ then it will only find tables within
819
- # the current schema search path (regardless of permissions to access tables in other schemas)
820
- def table_exists?(name)
821
- schema, table = Utils.extract_schema_and_table(name.to_s)
822
- return false unless table
823
-
824
- binds = [[nil, table]]
825
- binds << [nil, schema] if schema
826
-
827
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
828
- SELECT COUNT(*)
829
- FROM pg_class c
830
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
831
- WHERE c.relkind in ('v','r')
832
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
833
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
834
- SQL
835
- end
836
-
837
- # Returns true if schema exists.
838
- def schema_exists?(name)
839
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
840
- SELECT COUNT(*)
841
- FROM pg_namespace
842
- WHERE nspname = '#{name}'
843
- SQL
844
- end
845
-
846
- # Returns an array of indexes for the given table.
847
- def indexes(table_name, name = nil)
848
- []
849
- end
850
-
851
- # Returns the list of all column definitions for a table.
852
- def columns(table_name, name = nil)
853
- # Limit, precision, and scale are all handled by the superclass.
854
- column_definitions(table_name).collect do |column_name, type, default, notnull|
855
- RedshiftColumn.new(column_name, default, type, notnull == 'f', @config[:read_timezone])
856
- end
857
- end
858
-
859
- # Returns the current database name.
860
- def current_database
861
- query('select current_database()', 'SCHEMA')[0][0]
862
- end
863
-
864
- # Returns the current schema name.
865
- def current_schema
866
- query('SELECT current_schema', 'SCHEMA')[0][0]
867
- end
868
-
869
- # Returns the current database encoding format.
870
- def encoding
871
- query(<<-end_sql, 'SCHEMA')[0][0]
872
- SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
873
- WHERE pg_database.datname LIKE '#{current_database}'
874
- end_sql
875
- end
876
-
877
- # Sets the schema search path to a string of comma-separated schema names.
878
- # Names beginning with $ have to be quoted (e.g. $user => '$user').
879
- # See: http://www.redshift.org/docs/current/static/ddl-schemas.html
880
- #
881
- # This should be not be called manually but set in database.yml.
882
- def schema_search_path=(schema_csv)
883
- if schema_csv
884
- execute("SET search_path TO #{schema_csv}", 'SCHEMA')
885
- @schema_search_path = schema_csv
886
- end
887
- end
888
-
889
- # Returns the active schema search path.
890
- def schema_search_path
891
- @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
892
- end
893
-
894
- # Returns the sequence name for a table's primary key or some other specified key.
895
- def default_sequence_name(table_name, pk = nil) #:nodoc:
896
- serial_sequence(table_name, pk || 'id').split('.').last
897
- rescue ActiveRecord::StatementInvalid
898
- "#{table_name}_#{pk || 'id'}_seq"
899
- end
900
-
901
- def serial_sequence(table, column)
902
- result = exec_query(<<-eosql, 'SCHEMA')
903
- SELECT pg_get_serial_sequence('#{table}', '#{column}')
904
- eosql
905
- result.rows.first.first
906
- end
907
-
908
- # Resets the sequence of a table's primary key to the maximum value.
909
- def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
910
- unless pk and sequence
911
- default_pk, default_sequence = pk_and_sequence_for(table)
912
-
913
- pk ||= default_pk
914
- sequence ||= default_sequence
915
- end
916
-
917
- if @logger && pk && !sequence
918
- @logger.warn "#{table} has primary key #{pk} with no default sequence"
919
- end
920
-
921
- if pk && sequence
922
- quoted_sequence = quote_table_name(sequence)
923
-
924
- select_value <<-end_sql, 'SCHEMA'
925
- SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
926
- end_sql
927
- end
928
- end
929
-
930
- # Returns a table's primary key and belonging sequence.
931
- def pk_and_sequence_for(table) #:nodoc:
932
- # First try looking for a sequence with a dependency on the
933
- # given table's primary key.
934
- result = query(<<-end_sql, 'SCHEMA')[0]
935
- SELECT attr.attname, seq.relname
936
- FROM pg_class seq,
937
- pg_attribute attr,
938
- pg_depend dep,
939
- pg_namespace name,
940
- pg_constraint cons
941
- WHERE seq.oid = dep.objid
942
- AND seq.relkind = 'S'
943
- AND attr.attrelid = dep.refobjid
944
- AND attr.attnum = dep.refobjsubid
945
- AND attr.attrelid = cons.conrelid
946
- AND attr.attnum = cons.conkey[1]
947
- AND cons.contype = 'p'
948
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
949
- end_sql
950
-
951
- if result.nil? or result.empty?
952
- result = query(<<-end_sql, 'SCHEMA')[0]
953
- SELECT attr.attname,
954
- CASE
955
- WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
956
- substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
957
- strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
958
- ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
959
- END
960
- FROM pg_class t
961
- JOIN pg_attribute attr ON (t.oid = attrelid)
962
- JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
963
- JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
964
- WHERE t.oid = '#{quote_table_name(table)}'::regclass
965
- AND cons.contype = 'p'
966
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
967
- end_sql
968
- end
969
-
970
- [result.first, result.last]
971
- rescue
972
- nil
973
- end
974
-
975
- # Returns just a table's primary key
976
- def primary_key(table)
977
- row = exec_query(<<-end_sql, 'SCHEMA').rows.first
978
- SELECT DISTINCT(attr.attname)
979
- FROM pg_attribute attr
980
- INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
981
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
982
- WHERE cons.contype = 'p'
983
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
984
- end_sql
985
-
986
- row && row.first
987
- end
988
-
989
- # Renames a table.
990
- # Also renames a table's primary key sequence if the sequence name matches the
991
- # Active Record default.
992
- #
993
- # Example:
994
- # rename_table('octopuses', 'octopi')
995
- def rename_table(name, new_name)
996
- clear_cache!
997
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
998
- pk, seq = pk_and_sequence_for(new_name)
999
- if seq == "#{name}_#{pk}_seq"
1000
- new_seq = "#{new_name}_#{pk}_seq"
1001
- execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
1002
- end
1003
- end
1004
-
1005
- # Adds a new column to the named table.
1006
- # See TableDefinition#column for details of the options you can use.
1007
- def add_column(table_name, column_name, type, options = {})
1008
- clear_cache!
1009
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
1010
- add_column_options!(add_column_sql, options)
1011
-
1012
- execute add_column_sql
1013
- end
1014
-
1015
- # Changes the column of a table.
1016
- def change_column(table_name, column_name, type, options = {})
1017
- clear_cache!
1018
- quoted_table_name = quote_table_name(table_name)
1019
-
1020
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
1021
-
1022
- change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
1023
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
1024
- end
1025
-
1026
- # Changes the default value of a table column.
1027
- def change_column_default(table_name, column_name, default)
1028
- clear_cache!
1029
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
1030
- end
1031
-
1032
- def change_column_null(table_name, column_name, null, default = nil)
1033
- clear_cache!
1034
- unless null || default.nil?
1035
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
1036
- end
1037
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
1038
- end
1039
-
1040
- # Renames a column in a table.
1041
- def rename_column(table_name, column_name, new_column_name)
1042
- clear_cache!
1043
- execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
1044
- end
1045
-
1046
- def add_index(*)
1047
- # XXX nothing to do
1048
- end
1049
-
1050
- def remove_index!(table_name, index_name) #:nodoc:
1051
- end
1052
-
1053
- def rename_index(table_name, old_name, new_name)
1054
- end
1055
-
1056
- def index_name_length
1057
- 63
1058
- end
1059
-
1060
- # Maps logical Rails types to Redshift-specific data types.
1061
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
1062
- case type.to_s
1063
- when 'binary'
1064
- # Redshift doesn't support limits on binary (bytea) columns.
1065
- # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
1066
- case limit
1067
- when nil, 0..0x3fffffff; super(type)
1068
- else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
1069
- end
1070
- when 'integer'
1071
- return 'integer' unless limit
1072
-
1073
- case limit
1074
- when 1, 2; 'smallint'
1075
- when 3, 4; 'integer'
1076
- when 5..8; 'bigint'
1077
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
1078
- end
1079
- else
1080
- super
1081
- end
1082
- end
1083
-
1084
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
1085
- #
1086
- # Redshift requires the ORDER BY columns in the select list for distinct queries, and
1087
- # requires that the ORDER BY include the distinct column.
1088
- #
1089
- # distinct("posts.id", "posts.created_at desc")
1090
- def distinct(columns, orders) #:nodoc:
1091
- return "DISTINCT #{columns}" if orders.empty?
1092
-
1093
- # Construct a clean list of column names from the ORDER BY clause, removing
1094
- # any ASC/DESC modifiers
1095
- order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') }
1096
- order_columns.delete_if { |c| c.blank? }
1097
- order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
1098
-
1099
- "DISTINCT #{columns}, #{order_columns * ', '}"
1100
- end
1101
-
1102
- module Utils
1103
- extend self
1104
-
1105
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
1106
- # +schema_name+ is nil if not specified in +name+.
1107
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
1108
- # +name+ supports the range of schema/table references understood by Redshift, for example:
1109
- #
1110
- # * <tt>table_name</tt>
1111
- # * <tt>"table.name"</tt>
1112
- # * <tt>schema_name.table_name</tt>
1113
- # * <tt>schema_name."table.name"</tt>
1114
- # * <tt>"schema.name"."table name"</tt>
1115
- def extract_schema_and_table(name)
1116
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
1117
- [schema, table]
1118
- end
1119
- end
1120
-
1121
- protected
1122
- # Returns the version of the connected Redshift server.
1123
- def redshift_version
1124
- @connection.server_version
1125
- end
1126
-
1127
- def translate_exception(exception, message)
1128
- case exception.message
1129
- when /duplicate key value violates unique constraint/
1130
- RecordNotUnique.new(message, exception)
1131
- when /violates foreign key constraint/
1132
- InvalidForeignKey.new(message, exception)
1133
- else
1134
- super
1135
- end
1136
- end
1137
-
1138
- private
1139
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
1140
-
1141
- def exec_no_cache(sql, binds)
1142
- @connection.async_exec(sql)
1143
- end
1144
-
1145
- def exec_cache(sql, binds)
1146
- begin
1147
- stmt_key = prepare_statement sql
1148
-
1149
- # Clear the queue
1150
- @connection.get_last_result
1151
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
1152
- type_cast(val, col)
1153
- })
1154
- @connection.block
1155
- @connection.get_last_result
1156
- rescue PGError => e
1157
- # Get the PG code for the failure. Annoyingly, the code for
1158
- # prepared statements whose return value may have changed is
1159
- # FEATURE_NOT_SUPPORTED. Check here for more details:
1160
- # http://git.redshift.org/gitweb/?p=redshift.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
1161
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
1162
- if FEATURE_NOT_SUPPORTED == code
1163
- @statements.delete sql_key(sql)
1164
- retry
1165
- else
1166
- raise e
1167
- end
1168
- end
1169
- end
1170
-
1171
- # Returns the statement identifier for the client side cache
1172
- # of statements
1173
- def sql_key(sql)
1174
- "#{schema_search_path}-#{sql}"
1175
- end
1176
-
1177
- # Prepare the statement if it hasn't been prepared, return
1178
- # the statement key.
1179
- def prepare_statement(sql)
1180
- sql_key = sql_key(sql)
1181
- unless @statements.key? sql_key
1182
- nextkey = @statements.next_key
1183
- @connection.prepare nextkey, sql
1184
- @statements[sql_key] = nextkey
1185
- end
1186
- @statements[sql_key]
1187
- end
1188
-
1189
- # The internal Redshift identifier of the money data type.
1190
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
1191
- # The internal Redshift identifier of the BYTEA data type.
1192
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
1193
-
1194
- # Connects to a Redshift server and sets up the adapter depending on the
1195
- # connected server's characteristics.
1196
- def connect
1197
- @connection = PGconn.connect(*@connection_parameters)
1198
-
1199
- # Money type has a fixed precision of 10 in Redshift 8.2 and below, and as of
1200
- # Redshift 8.3 it has a fixed precision of 19. RedshiftColumn.extract_precision
1201
- # should know about this but can't detect it there, so deal with it here.
1202
- RedshiftColumn.money_precision = (redshift_version >= 80300) ? 19 : 10
1203
-
1204
- configure_connection
1205
- end
1206
-
1207
- # Configures the encoding, verbosity, schema search path, and time zone of the connection.
1208
- # This is called by #connect and should not be called manually.
1209
- def configure_connection
1210
- if @config[:encoding]
1211
- @connection.set_client_encoding(@config[:encoding])
1212
- end
1213
- self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
1214
- end
1215
-
1216
- # Returns the current ID of a table's sequence.
1217
- def last_insert_id(sequence_name) #:nodoc:
1218
- r = exec_query("SELECT currval('#{sequence_name}')", 'SQL')
1219
- Integer(r.rows.first.first)
1220
- end
1221
-
1222
- # Executes a SELECT query and returns the results, performing any data type
1223
- # conversions that are required to be performed here instead of in RedshiftColumn.
1224
- def select(sql, name = nil, binds = [])
1225
- exec_query(sql, name, binds).to_a
1226
- end
1227
-
1228
- def select_raw(sql, name = nil)
1229
- res = execute(sql, name)
1230
- results = result_as_array(res)
1231
- fields = res.fields
1232
- res.clear
1233
- return fields, results
1234
- end
1235
-
1236
- # Returns the list of a table's column names, data types, and default values.
1237
- #
1238
- # The underlying query is roughly:
1239
- # SELECT column.name, column.type, default.value
1240
- # FROM column LEFT JOIN default
1241
- # ON column.table_id = default.table_id
1242
- # AND column.num = default.column_num
1243
- # WHERE column.table_id = get_table_id('table_name')
1244
- # AND column.num > 0
1245
- # AND NOT column.is_dropped
1246
- # ORDER BY column.num
1247
- #
1248
- # If the table name is not prefixed with a schema, the database will
1249
- # take the first match from the schema search path.
1250
- #
1251
- # Query implementation notes:
1252
- # - format_type includes the column size constraint, e.g. varchar(50)
1253
- # - ::regclass is a function that gives the id for a table name
1254
- def column_definitions(table_name) #:nodoc:
1255
- exec_query(<<-end_sql, 'SCHEMA').rows
1256
- SELECT a.attname, format_type(a.atttypid, a.atttypmod),
1257
- pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
1258
- FROM pg_attribute a LEFT JOIN pg_attrdef d
1259
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
1260
- WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
1261
- AND a.attnum > 0 AND NOT a.attisdropped
1262
- ORDER BY a.attnum
1263
- end_sql
1264
- end
1265
-
1266
- def extract_pg_identifier_from_name(name)
1267
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
1268
-
1269
- if match_data
1270
- rest = name[match_data[0].length, name.length]
1271
- rest = rest[1, rest.length] if rest.start_with? "."
1272
- [match_data[1], (rest.length > 0 ? rest : nil)]
1273
- end
1274
- end
1275
-
1276
- def extract_table_ref_from_insert_sql(sql)
1277
- sql[/into\s+([^\(]*).*values\s*\(/i]
1278
- $1.strip if $1
1279
- end
1280
-
1281
- def table_definition
1282
- TableDefinition.new(self)
1283
- end
1284
- end
1285
- end
5
+ require_relative "../tasks/redshift_tasks"
6
+
7
+ if ActiveRecord.version >= Gem::Version.new('8.0.0')
8
+ require_relative 'redshift_8_0_adapter'
9
+ elsif ActiveRecord.version >= Gem::Version.new('7.2.0')
10
+ require_relative 'redshift_7_2_adapter'
11
+ elsif ActiveRecord.version >= Gem::Version.new('7.1.0')
12
+ require_relative 'redshift_7_1_adapter'
13
+ elsif ActiveRecord.version >= Gem::Version.new('7.0.0')
14
+ require_relative 'redshift_7_0_adapter'
15
+ else
16
+ raise 'no compatible version of ActiveRecord detected'
1286
17
  end