activerecord-redshiftbulk-adapter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c5b418004bb3ccf27580a842eca07427288c692b
4
+ data.tar.gz: 37e994178ff3b8ba4644624e8b68d8a9b4560055
5
+ SHA512:
6
+ metadata.gz: 19d27252e272d22c0f54147afa007590daa51931f067a3d59d8be0742c645bcfbed5fad1a3b9f7552e7bc8049dcd12f983fb91d93e273bf3765e8e8116f090d4
7
+ data.tar.gz: f10e2d8c0bf3b10189da16b7b8acefa7a095aa51e448e1077fc724385c5373f61c9e4138f6008369770902526790fe67b04748c33f9edade546b6362503516cc
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-redshiftbulk-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_redshiftbulk_adapter/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'activerecord-redshiftbulk-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 = 'Redshiftbulk'
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