activerecord-redshift-adapter 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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