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