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