activerecord4-redshift-adapter 0.1.1 → 0.2.0

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 (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record/connection_adapters/redshift/array_parser.rb +35 -39
  3. data/lib/active_record/connection_adapters/redshift/column.rb +10 -0
  4. data/lib/active_record/connection_adapters/redshift/database_statements.rb +37 -47
  5. data/lib/active_record/connection_adapters/redshift/oid.rb +14 -359
  6. data/lib/active_record/connection_adapters/redshift/oid/date.rb +11 -0
  7. data/lib/active_record/connection_adapters/redshift/oid/date_time.rb +36 -0
  8. data/lib/active_record/connection_adapters/redshift/oid/decimal.rb +13 -0
  9. data/lib/active_record/connection_adapters/redshift/oid/float.rb +21 -0
  10. data/lib/active_record/connection_adapters/redshift/oid/infinity.rb +13 -0
  11. data/lib/active_record/connection_adapters/redshift/oid/integer.rb +11 -0
  12. data/lib/active_record/connection_adapters/redshift/oid/json.rb +35 -0
  13. data/lib/active_record/connection_adapters/redshift/oid/jsonb.rb +23 -0
  14. data/lib/active_record/connection_adapters/redshift/oid/time.rb +11 -0
  15. data/lib/active_record/connection_adapters/redshift/oid/type_map_initializer.rb +63 -0
  16. data/lib/active_record/connection_adapters/redshift/quoting.rb +45 -119
  17. data/lib/active_record/connection_adapters/redshift/referential_integrity.rb +4 -19
  18. data/lib/active_record/connection_adapters/redshift/schema_definitions.rb +73 -0
  19. data/lib/active_record/connection_adapters/redshift/schema_statements.rb +141 -76
  20. data/lib/active_record/connection_adapters/redshift/utils.rb +77 -0
  21. data/lib/active_record/connection_adapters/redshift_adapter.rb +252 -496
  22. metadata +17 -11
  23. data/lib/active_record/connection_adapters/redshift/cast.rb +0 -156
@@ -0,0 +1,77 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ # Value Object to hold a schema qualified name.
5
+ # This is usually the name of a PostgreSQL relation but it can also represent
6
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
7
+ # double quoting.
8
+ class Name # :nodoc:
9
+ SEPARATOR = "."
10
+ attr_reader :schema, :identifier
11
+
12
+ def initialize(schema, identifier)
13
+ @schema, @identifier = unquote(schema), unquote(identifier)
14
+ end
15
+
16
+ def to_s
17
+ parts.join SEPARATOR
18
+ end
19
+
20
+ def quoted
21
+ if schema
22
+ PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
23
+ else
24
+ PGconn.quote_ident(identifier)
25
+ end
26
+ end
27
+
28
+ def ==(o)
29
+ o.class == self.class && o.parts == parts
30
+ end
31
+ alias_method :eql?, :==
32
+
33
+ def hash
34
+ parts.hash
35
+ end
36
+
37
+ protected
38
+ def unquote(part)
39
+ if part && part.start_with?('"')
40
+ part[1..-2]
41
+ else
42
+ part
43
+ end
44
+ end
45
+
46
+ def parts
47
+ @parts ||= [@schema, @identifier].compact
48
+ end
49
+ end
50
+
51
+ module Utils # :nodoc:
52
+ extend self
53
+
54
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
55
+ # extracted from +string+.
56
+ # +schema+ is nil if not specified in +string+.
57
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
58
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
59
+ #
60
+ # * <tt>table_name</tt>
61
+ # * <tt>"table.name"</tt>
62
+ # * <tt>schema_name.table_name</tt>
63
+ # * <tt>schema_name."table.name"</tt>
64
+ # * <tt>"schema_name".table_name</tt>
65
+ # * <tt>"schema.name"."table name"</tt>
66
+ def extract_schema_qualified_name(string)
67
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
68
+ if table.nil?
69
+ table = schema
70
+ schema = nil
71
+ end
72
+ Redshift::Name.new(schema, table)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,16 +1,19 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
2
  require 'active_record/connection_adapters/statement_pool'
3
+
4
+ require 'active_record/connection_adapters/redshift/utils'
5
+ require 'active_record/connection_adapters/redshift/column'
3
6
  require 'active_record/connection_adapters/redshift/oid'
4
- require 'active_record/connection_adapters/redshift/cast'
5
- require 'active_record/connection_adapters/redshift/array_parser'
6
7
  require 'active_record/connection_adapters/redshift/quoting'
8
+ require 'active_record/connection_adapters/redshift/referential_integrity'
9
+ require 'active_record/connection_adapters/redshift/schema_definitions'
7
10
  require 'active_record/connection_adapters/redshift/schema_statements'
8
11
  require 'active_record/connection_adapters/redshift/database_statements'
9
- require 'active_record/connection_adapters/redshift/referential_integrity'
12
+
10
13
  require 'arel/visitors/bind_visitor'
11
14
 
12
15
  # Make sure we're using pg high enough for PGResult#values
13
- gem 'pg', '~> 0.11'
16
+ gem 'pg', '> 0.15'
14
17
  require 'pg'
15
18
 
16
19
  require 'ipaddr'
@@ -20,8 +23,8 @@ module ActiveRecord
20
23
  RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
21
24
  :client_encoding, :options, :application_name, :fallback_application_name,
22
25
  :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
23
- :tty, :sslmode, :requiressl, :sslcert, :sslkey, :sslrootcert, :sslcrl,
24
- :requirepeer, :krbsrvname, :gsslib, :service]
26
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
27
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
25
28
 
26
29
  # Establishes a connection to the database that's used by all Active Record objects
27
30
  def redshift_connection(config)
@@ -43,194 +46,6 @@ module ActiveRecord
43
46
  end
44
47
 
45
48
  module ConnectionAdapters
46
- # PostgreSQL-specific extensions to column definitions in a table.
47
- class RedshiftColumn < Column #:nodoc:
48
- attr_accessor :array
49
- # Instantiates a new PostgreSQL column definition in a table.
50
- def initialize(name, default, oid_type, sql_type = nil, null = true)
51
- @oid_type = oid_type
52
- if sql_type =~ /\[\]$/
53
- @array = true
54
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
55
- else
56
- @array = false
57
- super(name, self.class.extract_value_from_default(default), sql_type, null)
58
- end
59
- end
60
-
61
- # :stopdoc:
62
- class << self
63
- include ConnectionAdapters::RedshiftColumn::Cast
64
- include ConnectionAdapters::RedshiftColumn::ArrayParser
65
- attr_accessor :money_precision
66
- end
67
- # :startdoc:
68
-
69
- # Extracts the value from a PostgreSQL column default definition.
70
- def self.extract_value_from_default(default)
71
- # This is a performance optimization for Ruby 1.9.2 in development.
72
- # If the value is nil, we return nil straight away without checking
73
- # the regular expressions. If we check each regular expression,
74
- # Regexp#=== will call NilClass#to_str, which will trigger
75
- # method_missing (defined by whiny nil in ActiveSupport) which
76
- # makes this method very very slow.
77
- return default unless default
78
-
79
- case default
80
- when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
81
- $1
82
- # Numeric types
83
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
84
- $1
85
- # Character types
86
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
87
- $1
88
- # Binary data types
89
- when /\A'(.*)'::bytea\z/m
90
- $1
91
- # Date/time types
92
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
93
- $1
94
- when /\A'(.*)'::interval\z/
95
- $1
96
- # Boolean type
97
- when 'true'
98
- true
99
- when 'false'
100
- false
101
- # Geometric types
102
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
103
- $1
104
- # Network address types
105
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
106
- $1
107
- # Bit string types
108
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
109
- $1
110
- # XML type
111
- when /\A'(.*)'::xml\z/m
112
- $1
113
- # Arrays
114
- when /\A'(.*)'::"?\D+"?\[\]\z/
115
- $1
116
- # Hstore
117
- when /\A'(.*)'::hstore\z/
118
- $1
119
- # JSON
120
- when /\A'(.*)'::json\z/
121
- $1
122
- # Object identifier types
123
- when /\A-?\d+\z/
124
- $1
125
- else
126
- # Anything else is blank, some user type, or some function
127
- # and we can't know the value of that, so return nil.
128
- nil
129
- end
130
- end
131
-
132
- def type_cast(value)
133
- return if value.nil?
134
- return super if encoded?
135
-
136
- @oid_type.type_cast value
137
- end
138
-
139
- private
140
-
141
- def extract_limit(sql_type)
142
- case sql_type
143
- when /^bigint/i; 8
144
- when /^smallint/i; 2
145
- when /^timestamp/i; nil
146
- else super
147
- end
148
- end
149
-
150
- # Extracts the scale from PostgreSQL-specific data types.
151
- def extract_scale(sql_type)
152
- # Money type has a fixed scale of 2.
153
- sql_type =~ /^money/ ? 2 : super
154
- end
155
-
156
- # Extracts the precision from PostgreSQL-specific data types.
157
- def extract_precision(sql_type)
158
- if sql_type == 'money'
159
- self.class.money_precision
160
- elsif sql_type =~ /timestamp/i
161
- $1.to_i if sql_type =~ /\((\d+)\)/
162
- else
163
- super
164
- end
165
- end
166
-
167
- # Maps PostgreSQL-specific data types to logical Rails types.
168
- def simplified_type(field_type)
169
- case field_type
170
- # Numeric and monetary types
171
- when /^(?:real|double precision)$/
172
- :float
173
- # Monetary types
174
- when 'money'
175
- :decimal
176
- when 'hstore'
177
- :hstore
178
- when 'ltree'
179
- :ltree
180
- # Network address types
181
- when 'inet'
182
- :inet
183
- when 'cidr'
184
- :cidr
185
- when 'macaddr'
186
- :macaddr
187
- # Character types
188
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
189
- :string
190
- # Binary data types
191
- when 'bytea'
192
- :binary
193
- # Date/time types
194
- when /^timestamp with(?:out)? time zone$/
195
- :datetime
196
- when /^interval(?:|\(\d+\))$/
197
- :string
198
- # Geometric types
199
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
200
- :string
201
- # Bit strings
202
- when /^bit(?: varying)?(?:\(\d+\))?$/
203
- :string
204
- # XML type
205
- when 'xml'
206
- :xml
207
- # tsvector type
208
- when 'tsvector'
209
- :tsvector
210
- # Arrays
211
- when /^\D+\[\]$/
212
- :string
213
- # Object identifier types
214
- when 'oid'
215
- :integer
216
- # UUID type
217
- when 'uuid'
218
- :uuid
219
- # JSON type
220
- when 'json'
221
- :json
222
- # Small and big integer types
223
- when /^(?:small|big)int$/
224
- :integer
225
- when /(num|date|tstz|ts|int4|int8)range$/
226
- field_type.to_sym
227
- # Pass through all types that are not specific to PostgreSQL.
228
- else
229
- super
230
- end
231
- end
232
- end
233
-
234
49
  # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
235
50
  #
236
51
  # Options:
@@ -249,7 +64,7 @@ module ActiveRecord
249
64
  # <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
250
65
  # * <tt>:variables</tt> - An optional hash of additional parameters that
251
66
  # will be used in <tt>SET SESSION key = val</tt> calls on the connection.
252
- # * <tt>:insert_returning</tt> - does nothing for Redshift.
67
+ # * <tt>:insert_returning</tt> - Does nothing for Redshift.
253
68
  #
254
69
  # Any further options are used as connection parameters to libpq. See
255
70
  # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
@@ -258,187 +73,44 @@ module ActiveRecord
258
73
  # In addition, default connection parameters of libpq can be set per environment variables.
259
74
  # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
260
75
  class RedshiftAdapter < AbstractAdapter
261
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
262
- attr_accessor :array
263
- end
264
-
265
- module ColumnMethods
266
- def xml(*args)
267
- options = args.extract_options!
268
- column(args[0], 'xml', options)
269
- end
270
-
271
- def tsvector(*args)
272
- options = args.extract_options!
273
- column(args[0], 'tsvector', options)
274
- end
275
-
276
- def int4range(name, options = {})
277
- column(name, 'int4range', options)
278
- end
279
-
280
- def int8range(name, options = {})
281
- column(name, 'int8range', options)
282
- end
283
-
284
- def tsrange(name, options = {})
285
- column(name, 'tsrange', options)
286
- end
287
-
288
- def tstzrange(name, options = {})
289
- column(name, 'tstzrange', options)
290
- end
291
-
292
- def numrange(name, options = {})
293
- column(name, 'numrange', options)
294
- end
295
-
296
- def daterange(name, options = {})
297
- column(name, 'daterange', options)
298
- end
299
-
300
- def hstore(name, options = {})
301
- column(name, 'hstore', options)
302
- end
303
-
304
- def ltree(name, options = {})
305
- column(name, 'ltree', options)
306
- end
307
-
308
- def inet(name, options = {})
309
- column(name, 'inet', options)
310
- end
311
-
312
- def cidr(name, options = {})
313
- column(name, 'cidr', options)
314
- end
315
-
316
- def macaddr(name, options = {})
317
- column(name, 'macaddr', options)
318
- end
319
-
320
- def uuid(name, options = {})
321
- column(name, 'uuid', options)
322
- end
323
-
324
- def json(name, options = {})
325
- column(name, 'json', options)
326
- end
327
- end
328
-
329
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
330
- include ColumnMethods
331
-
332
- # Defines the primary key field.
333
- # Use of the native PostgreSQL UUID type is supported, and can be used
334
- # by defining your tables as such:
335
- #
336
- # create_table :stuffs, id: :uuid do |t|
337
- # t.string :content
338
- # t.timestamps
339
- # end
340
- #
341
- # By default, this will use the +uuid_generate_v4()+ function from the
342
- # +uuid-ossp+ extension, which MUST be enabled on your databse. To enable
343
- # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
344
- # migrations To use a UUID primary key without +uuid-ossp+ enabled, you can
345
- # set the +:default+ option to nil:
346
- #
347
- # create_table :stuffs, id: false do |t|
348
- # t.primary_key :id, :uuid, default: nil
349
- # t.uuid :foo_id
350
- # t.timestamps
351
- # end
352
- #
353
- # You may also pass a different UUID generation function from +uuid-ossp+
354
- # or another library.
355
- #
356
- # Note that setting the UUID primary key default value to +nil+
357
- # will require you to assure that you always provide a UUID value
358
- # before saving a record (as primary keys cannot be nil). This might be
359
- # done via the SecureRandom.uuid method and a +before_save+ callback,
360
- # for instance.
361
- def primary_key(name, type = :primary_key, options = {})
362
- return super unless type == :uuid
363
- options[:default] = options.fetch(:default, 'uuid_generate_v4()')
364
- options[:primary_key] = true
365
- column name, type, options
366
- end
367
-
368
- def column(name, type = nil, options = {})
369
- super
370
- column = self[name]
371
- column.array = options[:array]
372
-
373
- self
374
- end
375
-
376
- def xml(options = {})
377
- column(args[0], :text, options)
378
- end
379
-
380
- private
381
-
382
- def create_column_definition(name, type)
383
- ColumnDefinition.new name, type
384
- end
385
- end
386
-
387
- class Table < ActiveRecord::ConnectionAdapters::Table
388
- include ColumnMethods
389
- end
390
-
391
- ADAPTER_NAME = 'Redshift'
76
+ ADAPTER_NAME = 'Redshift'.freeze
392
77
 
393
78
  NATIVE_DATABASE_TYPES = {
394
- primary_key: "serial primary key",
395
- string: { name: "character varying", limit: 255 },
79
+ primary_key: "integer primary key",
80
+ string: { name: "character varying" },
396
81
  text: { name: "text" },
397
82
  integer: { name: "integer" },
398
83
  float: { name: "float" },
399
84
  decimal: { name: "decimal" },
400
85
  datetime: { name: "timestamp" },
401
- timestamp: { name: "timestamp" },
402
86
  time: { name: "time" },
403
87
  date: { name: "date" },
404
- daterange: { name: "daterange" },
405
- numrange: { name: "numrange" },
406
- tsrange: { name: "tsrange" },
407
- tstzrange: { name: "tstzrange" },
408
- int4range: { name: "int4range" },
409
- int8range: { name: "int8range" },
410
- binary: { name: "bytea" },
411
- boolean: { name: "boolean" },
412
- xml: { name: "xml" },
413
- tsvector: { name: "tsvector" },
414
- hstore: { name: "hstore" },
415
- inet: { name: "inet" },
416
- cidr: { name: "cidr" },
417
- macaddr: { name: "macaddr" },
418
- uuid: { name: "uuid" },
88
+ bigint: { name: "bigint" },
419
89
  json: { name: "json" },
420
- ltree: { name: "ltree" }
90
+ jsonb: { name: "jsonb" }
421
91
  }
422
92
 
423
- include Quoting
424
- include ReferentialIntegrity
425
- include SchemaStatements
426
- include DatabaseStatements
93
+ OID = Redshift::OID #:nodoc:
94
+
95
+ include Redshift::Quoting
96
+ include Redshift::ReferentialIntegrity
97
+ include Redshift::SchemaStatements
98
+ include Redshift::DatabaseStatements
427
99
 
428
- # Returns 'PostgreSQL' as adapter name for identification purposes.
429
- def adapter_name
430
- ADAPTER_NAME
100
+ def schema_creation # :nodoc:
101
+ Redshift::SchemaCreation.new self
431
102
  end
432
103
 
433
- # Adds `:array` option to the default set provided by the
104
+ # Adds +:array+ option to the default set provided by the
434
105
  # AbstractAdapter
435
- def prepare_column_options(column, types)
106
+ def prepare_column_options(column, types) # :nodoc:
436
107
  spec = super
437
108
  spec[:array] = 'true' if column.respond_to?(:array) && column.array
109
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
438
110
  spec
439
111
  end
440
112
 
441
- # Adds `:array` as a valid migration key
113
+ # Adds +:array+ as a valid migration key
442
114
  def migration_keys
443
115
  super + [:array]
444
116
  end
@@ -450,14 +122,22 @@ module ActiveRecord
450
122
  end
451
123
 
452
124
  def supports_index_sort_order?
453
- true
125
+ false
454
126
  end
455
127
 
456
128
  def supports_partial_index?
457
- true
129
+ false
458
130
  end
459
131
 
460
132
  def supports_transaction_isolation?
133
+ false
134
+ end
135
+
136
+ def supports_foreign_keys?
137
+ true
138
+ end
139
+
140
+ def supports_views?
461
141
  true
462
142
  end
463
143
 
@@ -518,21 +198,13 @@ module ActiveRecord
518
198
  end
519
199
  end
520
200
 
521
- class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
522
- include Arel::Visitors::BindVisitor
523
- end
524
-
525
201
  # Initializes and connects a PostgreSQL adapter.
526
202
  def initialize(connection, logger, connection_parameters, config)
527
203
  super(connection, logger)
528
204
 
529
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
530
- @visitor = Arel::Visitors::PostgreSQL.new self
531
- else
532
- @visitor = unprepared_visitor
533
- end
205
+ @visitor = Arel::Visitors::PostgreSQL.new self
206
+ @prepared_statements = false
534
207
 
535
- connection_parameters.delete :prepared_statements
536
208
  @connection_parameters, @config = connection_parameters, config
537
209
 
538
210
  # @local_tz is initialized as nil to avoid warnings when connect tries to use it
@@ -543,7 +215,8 @@ module ActiveRecord
543
215
  @statements = StatementPool.new @connection,
544
216
  self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
545
217
 
546
- initialize_type_map
218
+ @type_map = Type::HashLookupTypeMap.new
219
+ initialize_type_map(type_map)
547
220
  @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
548
221
  @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : false
549
222
  end
@@ -553,9 +226,14 @@ module ActiveRecord
553
226
  @statements.clear
554
227
  end
555
228
 
229
+ def truncate(table_name, name = nil)
230
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
231
+ end
232
+
556
233
  # Is this connection alive and ready for queries?
557
234
  def active?
558
- @connection.connect_poll != PG::PGRES_POLLING_FAILED
235
+ @connection.query 'SELECT 1'
236
+ true
559
237
  rescue PGError
560
238
  false
561
239
  end
@@ -569,7 +247,12 @@ module ActiveRecord
569
247
 
570
248
  def reset!
571
249
  clear_cache!
572
- super
250
+ reset_transaction
251
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
252
+ @connection.query 'ROLLBACK'
253
+ end
254
+ @connection.query 'DISCARD ALL'
255
+ configure_connection
573
256
  end
574
257
 
575
258
  # Disconnects from the database if already connected. Otherwise, this
@@ -593,38 +276,38 @@ module ActiveRecord
593
276
  true
594
277
  end
595
278
 
596
- def supports_insert_with_returning?
597
- false
279
+ # Enable standard-conforming strings if available.
280
+ def set_standard_conforming_strings
281
+ old, self.client_min_messages = client_min_messages, 'panic'
282
+ execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
283
+ ensure
284
+ self.client_min_messages = old
598
285
  end
599
286
 
600
287
  def supports_ddl_transactions?
601
288
  true
602
289
  end
603
290
 
604
- # Returns true, since this connection adapter supports savepoints.
605
- def supports_savepoints?
606
- false
607
- end
608
-
609
- def supports_import?
610
- true
611
- end
612
-
613
- # Returns true.
614
291
  def supports_explain?
615
292
  true
616
293
  end
617
294
 
618
- # Returns true if pg > 9.2
619
295
  def supports_extensions?
620
296
  false
621
297
  end
622
298
 
623
- # Range datatypes weren't introduced until PostgreSQL 9.2
624
299
  def supports_ranges?
625
300
  false
626
301
  end
627
302
 
303
+ def supports_materialized_views?
304
+ false
305
+ end
306
+
307
+ def supports_import?
308
+ true
309
+ end
310
+
628
311
  def enable_extension(name)
629
312
  end
630
313
 
@@ -635,9 +318,6 @@ module ActiveRecord
635
318
  false
636
319
  end
637
320
 
638
- #def extensions
639
- #end
640
-
641
321
  # Returns the configured supported identifier length supported by PostgreSQL
642
322
  def table_alias_length
643
323
  @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
@@ -649,25 +329,6 @@ module ActiveRecord
649
329
  exec_query "SET SESSION AUTHORIZATION #{user}"
650
330
  end
651
331
 
652
- module Utils
653
- extend self
654
-
655
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
656
- # +schema_name+ is nil if not specified in +name+.
657
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
658
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
659
- #
660
- # * <tt>table_name</tt>
661
- # * <tt>"table.name"</tt>
662
- # * <tt>schema_name.table_name</tt>
663
- # * <tt>schema_name."table.name"</tt>
664
- # * <tt>"schema.name"."table name"</tt>
665
- def extract_schema_and_table(name)
666
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
667
- [schema, table]
668
- end
669
- end
670
-
671
332
  def use_insert_returning?
672
333
  false
673
334
  end
@@ -676,17 +337,32 @@ module ActiveRecord
676
337
  !native_database_types[type].nil?
677
338
  end
678
339
 
340
+ def update_table_definition(table_name, base) #:nodoc:
341
+ Redshift::Table.new(table_name, base)
342
+ end
343
+
344
+ def lookup_cast_type(sql_type) # :nodoc:
345
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
346
+ super(oid)
347
+ end
348
+
349
+ def column_name_for_operation(operation, node) # :nodoc:
350
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
351
+ end
352
+
353
+ OPERATION_ALIASES = { # :nodoc:
354
+ "maximum" => "max",
355
+ "minimum" => "min",
356
+ "average" => "avg",
357
+ }
358
+
679
359
  protected
680
360
 
681
361
  # Returns the version of the connected PostgreSQL server.
682
- def postgresql_version
362
+ def redshift_version
683
363
  @connection.server_version
684
364
  end
685
365
 
686
- # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
687
- FOREIGN_KEY_VIOLATION = "23503"
688
- UNIQUE_VIOLATION = "23505"
689
-
690
366
  def translate_exception(exception, message)
691
367
  return exception unless exception.respond_to?(:result)
692
368
 
@@ -702,64 +378,170 @@ module ActiveRecord
702
378
 
703
379
  private
704
380
 
705
- def reload_type_map
706
- OID::TYPE_MAP.clear
707
- initialize_type_map
708
- end
381
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
382
+ if !type_map.key?(oid)
383
+ load_additional_types(type_map, [oid])
384
+ end
709
385
 
710
- def initialize_type_map
711
- result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
712
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
386
+ type_map.fetch(oid, fmod, sql_type) {
387
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
388
+ Type::Value.new.tap do |cast_type|
389
+ type_map.register_type(oid, cast_type)
390
+ end
391
+ }
392
+ end
393
+
394
+ def initialize_type_map(m) # :nodoc:
395
+ register_class_with_limit m, 'int2', OID::Integer
396
+ register_class_with_limit m, 'int4', OID::Integer
397
+ register_class_with_limit m, 'int8', OID::Integer
398
+ m.alias_type 'oid', 'int2'
399
+ m.register_type 'float4', OID::Float.new
400
+ m.alias_type 'float8', 'float4'
401
+ m.register_type 'text', Type::Text.new
402
+ register_class_with_limit m, 'varchar', Type::String
403
+ m.alias_type 'char', 'varchar'
404
+ m.alias_type 'name', 'varchar'
405
+ m.alias_type 'bpchar', 'varchar'
406
+ m.alias_type 'timestamptz', 'timestamp'
407
+ m.register_type 'date', OID::Date.new
408
+ m.register_type 'time', OID::Time.new
409
+
410
+ m.register_type 'json', OID::Json.new
411
+ m.register_type 'jsonb', OID::Jsonb.new
412
+
413
+ # FIXME: why are we keeping these types as strings?
414
+ m.alias_type 'interval', 'varchar'
415
+
416
+ m.register_type 'timestamp' do |_, _, sql_type|
417
+ precision = extract_precision(sql_type)
418
+ OID::DateTime.new(precision: precision)
419
+ end
713
420
 
714
- # populate the leaf nodes
715
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
716
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
421
+ m.register_type 'numeric' do |_, fmod, sql_type|
422
+ precision = extract_precision(sql_type)
423
+ scale = extract_scale(sql_type)
424
+
425
+ # The type for the numeric depends on the width of the field,
426
+ # so we'll do something special here.
427
+ #
428
+ # When dealing with decimal columns:
429
+ #
430
+ # places after decimal = fmod - 4 & 0xffff
431
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
432
+ if fmod && (fmod - 4 & 0xffff).zero?
433
+ # FIXME: Remove this class, and the second argument to
434
+ # lookups on PG
435
+ Type::DecimalWithoutScale.new(precision: precision)
436
+ else
437
+ OID::Decimal.new(precision: precision, scale: scale)
438
+ end
717
439
  end
718
440
 
719
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
441
+ load_additional_types(m)
442
+ end
720
443
 
721
- # populate composite types
722
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
723
- if OID.registered_type? row['typname']
724
- # this composite type is explicitly registered
725
- vector = OID::NAMES[row['typname']]
444
+ def extract_limit(sql_type) # :nodoc:
445
+ case sql_type
446
+ when /^bigint/i, /^int8/i
447
+ 8
448
+ when /^smallint/i
449
+ 2
450
+ else
451
+ super
452
+ end
453
+ end
454
+
455
+ # Extracts the value from a PostgreSQL column default definition.
456
+ def extract_value_from_default(oid, default) # :nodoc:
457
+ case default
458
+ # Quoted types
459
+ when /\A[\(B]?'(.*)'::/m
460
+ $1.gsub(/''/, "'")
461
+ # Boolean types
462
+ when 'true', 'false'
463
+ default
464
+ # Numeric types
465
+ when /\A\(?(-?\d+(\.\d*)?)\)?\z/
466
+ $1
467
+ # Object identifier types
468
+ when /\A-?\d+\z/
469
+ $1
726
470
  else
727
- # use the default for composite types
728
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
729
- end
471
+ # Anything else is blank, some user type, or some function
472
+ # and we can't know the value of that, so return nil.
473
+ nil
474
+ end
475
+ end
476
+
477
+ def extract_default_function(default_value, default) # :nodoc:
478
+ default if has_default_function?(default_value, default)
479
+ end
480
+
481
+ def has_default_function?(default_value, default) # :nodoc:
482
+ !default_value && (%r{\w+\(.*\)} === default)
483
+ end
484
+
485
+ def load_additional_types(type_map, oids = nil) # :nodoc:
486
+ initializer = OID::TypeMapInitializer.new(type_map)
487
+
488
+ if supports_ranges?
489
+ query = <<-SQL
490
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
491
+ FROM pg_type as t
492
+ LEFT JOIN pg_range as r ON oid = rngtypid
493
+ SQL
494
+ else
495
+ query = <<-SQL
496
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
497
+ FROM pg_type as t
498
+ SQL
499
+ end
730
500
 
731
- OID::TYPE_MAP[row['oid'].to_i] = vector
501
+ if oids
502
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
732
503
  end
733
504
 
734
- # populate array types
735
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
736
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
737
- OID::TYPE_MAP[row['oid'].to_i] = array
505
+ execute_and_clear(query, 'SCHEMA', []) do |records|
506
+ initializer.run(records)
738
507
  end
739
508
  end
740
509
 
741
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
510
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
511
+
512
+ def execute_and_clear(sql, name, binds)
513
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
514
+ exec_cache(sql, name, binds)
515
+ ret = yield result
516
+ result.clear
517
+ ret
518
+ end
742
519
 
743
- def exec_no_cache(sql, binds)
744
- @connection.async_exec(sql, [])
520
+ def exec_no_cache(sql, name, binds)
521
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
745
522
  end
746
523
 
747
- def exec_cache(sql, binds)
748
- stmt_key = prepare_statement sql
749
-
750
- # Clear the queue
751
- @connection.get_last_result
752
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
753
- type_cast(val, col)
754
- })
755
- @connection.block
756
- @connection.get_last_result
757
- rescue PGError => e
524
+ def exec_cache(sql, name, binds)
525
+ stmt_key = prepare_statement(sql)
526
+ type_casted_binds = binds.map { |col, val|
527
+ [col, type_cast(val, col)]
528
+ }
529
+
530
+ log(sql, name, type_casted_binds, stmt_key) do
531
+ @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
532
+ end
533
+ rescue ActiveRecord::StatementInvalid => e
534
+ pgerror = e.original_exception
535
+
758
536
  # Get the PG code for the failure. Annoyingly, the code for
759
537
  # prepared statements whose return value may have changed is
760
538
  # FEATURE_NOT_SUPPORTED. Check here for more details:
761
539
  # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
762
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
540
+ begin
541
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
542
+ rescue
543
+ raise e
544
+ end
763
545
  if FEATURE_NOT_SUPPORTED == code
764
546
  @statements.delete sql_key(sql)
765
547
  retry
@@ -780,28 +562,30 @@ module ActiveRecord
780
562
  sql_key = sql_key(sql)
781
563
  unless @statements.key? sql_key
782
564
  nextkey = @statements.next_key
783
- @connection.prepare nextkey, sql
565
+ begin
566
+ @connection.prepare nextkey, sql
567
+ rescue => e
568
+ raise translate_exception_class(e, sql)
569
+ end
570
+ # Clear the queue
571
+ @connection.get_last_result
784
572
  @statements[sql_key] = nextkey
785
573
  end
786
574
  @statements[sql_key]
787
575
  end
788
576
 
789
- # The internal PostgreSQL identifier of the money data type.
790
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
791
- # The internal PostgreSQL identifier of the BYTEA data type.
792
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
793
-
794
577
  # Connects to a PostgreSQL server and sets up the adapter depending on the
795
578
  # connected server's characteristics.
796
579
  def connect
797
580
  @connection = PGconn.connect(@connection_parameters)
798
581
 
799
- # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
800
- # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
801
- # should know about this but can't detect it there, so deal with it here.
802
- RedshiftColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
803
-
804
582
  configure_connection
583
+ rescue ::PG::Error => error
584
+ if error.message.include?("does not exist")
585
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
586
+ else
587
+ raise
588
+ end
805
589
  end
806
590
 
807
591
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -818,9 +602,9 @@ module ActiveRecord
818
602
  variables.map do |k, v|
819
603
  if v == ':default' || v == :default
820
604
  # Sets the value to the global or compile default
821
- execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
605
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
822
606
  elsif !v.nil?
823
- execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
607
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
824
608
  end
825
609
  end
826
610
  end
@@ -838,20 +622,6 @@ module ActiveRecord
838
622
  exec_query("SELECT currval('#{sequence_name}')", 'SQL')
839
623
  end
840
624
 
841
- # Executes a SELECT query and returns the results, performing any data type
842
- # conversions that are required to be performed here instead of in PostgreSQLColumn.
843
- def select(sql, name = nil, binds = [])
844
- exec_query(sql, name, binds)
845
- end
846
-
847
- def select_raw(sql, name = nil)
848
- res = execute(sql, name)
849
- results = result_as_array(res)
850
- fields = res.fields
851
- res.clear
852
- return fields, results
853
- end
854
-
855
625
  # Returns the list of a table's column names, data types, and default values.
856
626
  #
857
627
  # The underlying query is roughly:
@@ -870,7 +640,7 @@ module ActiveRecord
870
640
  # Query implementation notes:
871
641
  # - format_type includes the column size constraint, e.g. varchar(50)
872
642
  # - ::regclass is a function that gives the id for a table name
873
- def column_definitions(table_name) #:nodoc:
643
+ def column_definitions(table_name) # :nodoc:
874
644
  exec_query(<<-end_sql, 'SCHEMA').rows
875
645
  SELECT a.attname, format_type(a.atttypid, a.atttypmod),
876
646
  pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
@@ -882,27 +652,13 @@ module ActiveRecord
882
652
  end_sql
883
653
  end
884
654
 
885
- def extract_pg_identifier_from_name(name)
886
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
887
-
888
- if match_data
889
- rest = name[match_data[0].length, name.length]
890
- rest = rest[1, rest.length] if rest.start_with? "."
891
- [match_data[1], (rest.length > 0 ? rest : nil)]
892
- end
893
- end
894
-
895
- def extract_table_ref_from_insert_sql(sql)
896
- sql[/into\s+([^\(]*).*values\s*\(/i]
655
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
656
+ sql[/into\s+([^\(]*).*values\s*\(/im]
897
657
  $1.strip if $1
898
658
  end
899
659
 
900
- def create_table_definition(name, temporary, options, as = nil)
901
- TableDefinition.new native_database_types, name, temporary, options
902
- end
903
-
904
- def update_table_definition(table_name, base)
905
- Table.new(table_name, base)
660
+ def create_table_definition(name, temporary, options, as = nil) # :nodoc:
661
+ Redshift::TableDefinition.new native_database_types, name, temporary, options, as
906
662
  end
907
663
  end
908
664
  end