activerecord-redshift-bulk-adapter 0.0.1

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