activerecord6-redshift-adapter 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
5
+ attr_reader :oid, :fmod, :array
6
+
7
+ def initialize(type_metadata, oid: nil, fmod: nil)
8
+ super(type_metadata)
9
+ @type_metadata = type_metadata
10
+ @oid = oid
11
+ @fmod = fmod
12
+ @array = /\[\]$/ === type_metadata.sql_type
13
+ end
14
+
15
+ def sql_type
16
+ super.gsub(/\[\]$/, "".freeze)
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(Redshift::TypeMetadata) &&
21
+ attributes_for_hash == other.attributes_for_hash
22
+ end
23
+ alias eql? ==
24
+
25
+ def hash
26
+ attributes_for_hash.hash
27
+ end
28
+
29
+ protected
30
+
31
+ def attributes_for_hash
32
+ [self.class, @type_metadata, oid, fmod]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,77 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ # Value Object to hold a schema qualified name.
5
+ # This is usually the name of a PostgreSQL relation but it can also represent
6
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
7
+ # double quoting.
8
+ class Name # :nodoc:
9
+ SEPARATOR = "."
10
+ attr_reader :schema, :identifier
11
+
12
+ def initialize(schema, identifier)
13
+ @schema, @identifier = unquote(schema), unquote(identifier)
14
+ end
15
+
16
+ def to_s
17
+ parts.join SEPARATOR
18
+ end
19
+
20
+ def quoted
21
+ if schema
22
+ PG::Connection.quote_ident(schema) << SEPARATOR << PG::Connection.quote_ident(identifier)
23
+ else
24
+ PG::Connection.quote_ident(identifier)
25
+ end
26
+ end
27
+
28
+ def ==(o)
29
+ o.class == self.class && o.parts == parts
30
+ end
31
+ alias_method :eql?, :==
32
+
33
+ def hash
34
+ parts.hash
35
+ end
36
+
37
+ protected
38
+ def unquote(part)
39
+ if part && part.start_with?('"')
40
+ part[1..-2]
41
+ else
42
+ part
43
+ end
44
+ end
45
+
46
+ def parts
47
+ @parts ||= [@schema, @identifier].compact
48
+ end
49
+ end
50
+
51
+ module Utils # :nodoc:
52
+ extend self
53
+
54
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
55
+ # extracted from +string+.
56
+ # +schema+ is nil if not specified in +string+.
57
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
58
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
59
+ #
60
+ # * <tt>table_name</tt>
61
+ # * <tt>"table.name"</tt>
62
+ # * <tt>schema_name.table_name</tt>
63
+ # * <tt>schema_name."table.name"</tt>
64
+ # * <tt>"schema_name".table_name</tt>
65
+ # * <tt>"schema.name"."table name"</tt>
66
+ def extract_schema_qualified_name(string)
67
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
68
+ if table.nil?
69
+ table = schema
70
+ schema = nil
71
+ end
72
+ Redshift::Name.new(schema, table)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,635 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'active_record/connection_adapters/statement_pool'
3
+
4
+ require 'active_record/connection_adapters/redshift/utils'
5
+ require 'active_record/connection_adapters/redshift/column'
6
+ require 'active_record/connection_adapters/redshift/oid'
7
+ require 'active_record/connection_adapters/redshift/quoting'
8
+ require 'active_record/connection_adapters/redshift/referential_integrity'
9
+ require 'active_record/connection_adapters/redshift/schema_definitions'
10
+ require 'active_record/connection_adapters/redshift/schema_dumper'
11
+ require 'active_record/connection_adapters/redshift/schema_statements'
12
+ require 'active_record/connection_adapters/redshift/type_metadata'
13
+ require 'active_record/connection_adapters/redshift/database_statements'
14
+
15
+ require 'pg'
16
+
17
+ require 'ipaddr'
18
+
19
+ module ActiveRecord
20
+ module ConnectionHandling # :nodoc:
21
+ RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
22
+ :client_encoding, :options, :application_name, :fallback_application_name,
23
+ :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
24
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
25
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
26
+
27
+ # Establishes a connection to the database that's used by all Active Record objects
28
+ def redshift_connection(config)
29
+ conn_params = config.symbolize_keys
30
+
31
+ conn_params.delete_if { |_, v| v.nil? }
32
+
33
+ # Map ActiveRecords param names to PGs.
34
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
35
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
36
+
37
+ # Forward only valid config params to PG::Connection.connect.
38
+ conn_params.keep_if { |k, _| RS_VALID_CONN_PARAMS.include?(k) }
39
+
40
+ # The postgres drivers don't allow the creation of an unconnected PG::Connection object,
41
+ # so just pass a nil connection object for the time being.
42
+ ConnectionAdapters::RedshiftAdapter.new(nil, logger, conn_params, config)
43
+ end
44
+ end
45
+
46
+ module ConnectionAdapters
47
+ # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
48
+ #
49
+ # Options:
50
+ #
51
+ # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets,
52
+ # the default is to connect to localhost.
53
+ # * <tt>:port</tt> - Defaults to 5432.
54
+ # * <tt>:username</tt> - Defaults to be the same as the operating system name of the user running the application.
55
+ # * <tt>:password</tt> - Password to be used if the server demands password authentication.
56
+ # * <tt>:database</tt> - Defaults to be the same as the user name.
57
+ # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
58
+ # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
59
+ # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
60
+ # <encoding></tt> call on the connection.
61
+ # * <tt>:min_messages</tt> - An optional client min messages that is used in a
62
+ # <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
63
+ # * <tt>:variables</tt> - An optional hash of additional parameters that
64
+ # will be used in <tt>SET SESSION key = val</tt> calls on the connection.
65
+ # * <tt>:insert_returning</tt> - Does nothing for Redshift.
66
+ #
67
+ # Any further options are used as connection parameters to libpq. See
68
+ # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
69
+ # list of parameters.
70
+ #
71
+ # In addition, default connection parameters of libpq can be set per environment variables.
72
+ # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
73
+ class RedshiftAdapter < AbstractAdapter
74
+ ADAPTER_NAME = 'Redshift'.freeze
75
+
76
+ NATIVE_DATABASE_TYPES = {
77
+ primary_key: "integer identity primary key",
78
+ string: { name: "varchar" },
79
+ text: { name: "varchar" },
80
+ integer: { name: "integer" },
81
+ float: { name: "float" },
82
+ decimal: { name: "decimal" },
83
+ datetime: { name: "timestamp" },
84
+ time: { name: "time" },
85
+ date: { name: "date" },
86
+ bigint: { name: "bigint" },
87
+ boolean: { name: "boolean" },
88
+ }
89
+
90
+ OID = Redshift::OID #:nodoc:
91
+
92
+ include Redshift::Quoting
93
+ include Redshift::ReferentialIntegrity
94
+ include Redshift::SchemaStatements
95
+ include Redshift::DatabaseStatements
96
+
97
+ def schema_creation # :nodoc:
98
+ Redshift::SchemaCreation.new self
99
+ end
100
+
101
+ def supports_index_sort_order?
102
+ false
103
+ end
104
+
105
+ def supports_partial_index?
106
+ false
107
+ end
108
+
109
+ def supports_transaction_isolation?
110
+ false
111
+ end
112
+
113
+ def supports_foreign_keys?
114
+ true
115
+ end
116
+
117
+ def supports_views?
118
+ true
119
+ end
120
+
121
+ def index_algorithms
122
+ { concurrently: 'CONCURRENTLY' }
123
+ end
124
+
125
+ class StatementPool < ConnectionAdapters::StatementPool
126
+ def initialize(connection, max)
127
+ super(max)
128
+ @connection = connection
129
+ @counter = 0
130
+ @cache = Hash.new { |h,pid| h[pid] = {} }
131
+ end
132
+
133
+ def each(&block); cache.each(&block); end
134
+ def key?(key); cache.key?(key); end
135
+ def [](key); cache[key]; end
136
+ def length; cache.length; end
137
+
138
+ def next_key
139
+ "a#{@counter + 1}"
140
+ end
141
+
142
+ def []=(sql, key)
143
+ while @max <= cache.size
144
+ dealloc(cache.shift.last)
145
+ end
146
+ @counter += 1
147
+ cache[sql] = key
148
+ end
149
+
150
+ def clear
151
+ cache.each_value do |stmt_key|
152
+ dealloc stmt_key
153
+ end
154
+ cache.clear
155
+ end
156
+
157
+ def delete(sql_key)
158
+ dealloc cache[sql_key]
159
+ cache.delete sql_key
160
+ end
161
+
162
+ private
163
+
164
+ def cache
165
+ @cache[Process.pid]
166
+ end
167
+
168
+ def dealloc(key)
169
+ @connection.query "DEALLOCATE #{key}" if connection_active?
170
+ end
171
+
172
+ def connection_active?
173
+ @connection.status == PG::Connection::CONNECTION_OK
174
+ rescue PG::Error
175
+ false
176
+ end
177
+ end
178
+
179
+ # Initializes and connects a PostgreSQL adapter.
180
+ def initialize(connection, logger, connection_parameters, config)
181
+ super(connection, logger, config)
182
+
183
+ @visitor = Arel::Visitors::PostgreSQL.new self
184
+ @visitor.extend(ConnectionAdapters::DetermineIfPreparableVisitor)
185
+ @prepared_statements = false
186
+
187
+ @connection_parameters = connection_parameters
188
+
189
+ # @local_tz is initialized as nil to avoid warnings when connect tries to use it
190
+ @local_tz = nil
191
+ @table_alias_length = nil
192
+
193
+ connect
194
+ @statements = StatementPool.new @connection,
195
+ self.class.type_cast_config_to_integer(config[:statement_limit])
196
+
197
+ @type_map = Type::HashLookupTypeMap.new
198
+ initialize_type_map(type_map)
199
+ @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
200
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : false
201
+ end
202
+
203
+ # Clears the prepared statements cache.
204
+ def clear_cache!
205
+ @statements.clear
206
+ end
207
+
208
+ def truncate(table_name, name = nil)
209
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
210
+ end
211
+
212
+ # Is this connection alive and ready for queries?
213
+ def active?
214
+ @connection.query 'SELECT 1'
215
+ true
216
+ rescue PG::Error
217
+ false
218
+ end
219
+
220
+ # Close then reopen the connection.
221
+ def reconnect!
222
+ super
223
+ @connection.reset
224
+ configure_connection
225
+ end
226
+
227
+ def reset!
228
+ clear_cache!
229
+ reset_transaction
230
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
231
+ @connection.query 'ROLLBACK'
232
+ end
233
+ @connection.query 'DISCARD ALL'
234
+ configure_connection
235
+ end
236
+
237
+ # Disconnects from the database if already connected. Otherwise, this
238
+ # method does nothing.
239
+ def disconnect!
240
+ super
241
+ @connection.close rescue nil
242
+ end
243
+
244
+ def native_database_types #:nodoc:
245
+ NATIVE_DATABASE_TYPES
246
+ end
247
+
248
+ # Returns true, since this connection adapter supports migrations.
249
+ def supports_migrations?
250
+ true
251
+ end
252
+
253
+ # Does PostgreSQL support finding primary key on non-Active Record tables?
254
+ def supports_primary_key? #:nodoc:
255
+ true
256
+ end
257
+
258
+ # Enable standard-conforming strings if available.
259
+ def set_standard_conforming_strings
260
+ old, self.client_min_messages = client_min_messages, 'panic'
261
+ execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
262
+ ensure
263
+ self.client_min_messages = old
264
+ end
265
+
266
+ def supports_ddl_transactions?
267
+ true
268
+ end
269
+
270
+ def supports_explain?
271
+ true
272
+ end
273
+
274
+ def supports_extensions?
275
+ false
276
+ end
277
+
278
+ def supports_ranges?
279
+ false
280
+ end
281
+
282
+ def supports_materialized_views?
283
+ false
284
+ end
285
+
286
+ def supports_import?
287
+ true
288
+ end
289
+
290
+ def enable_extension(name)
291
+ end
292
+
293
+ def disable_extension(name)
294
+ end
295
+
296
+ def extension_enabled?(name)
297
+ false
298
+ end
299
+
300
+ # Returns the configured supported identifier length supported by PostgreSQL
301
+ def table_alias_length
302
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
303
+ end
304
+
305
+ # Set the authorized user for this session
306
+ def session_auth=(user)
307
+ clear_cache!
308
+ exec_query "SET SESSION AUTHORIZATION #{user}"
309
+ end
310
+
311
+ def use_insert_returning?
312
+ false
313
+ end
314
+
315
+ def valid_type?(type)
316
+ !native_database_types[type].nil?
317
+ end
318
+
319
+ def update_table_definition(table_name, base) #:nodoc:
320
+ Redshift::Table.new(table_name, base)
321
+ end
322
+
323
+ def lookup_cast_type(sql_type) # :nodoc:
324
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
325
+ super(oid)
326
+ end
327
+
328
+ def column_name_for_operation(operation, node) # :nodoc:
329
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
330
+ end
331
+
332
+ OPERATION_ALIASES = { # :nodoc:
333
+ "maximum" => "max",
334
+ "minimum" => "min",
335
+ "average" => "avg",
336
+ }
337
+
338
+ protected
339
+
340
+ # Returns the version of the connected PostgreSQL server.
341
+ def redshift_version
342
+ @connection.server_version
343
+ end
344
+
345
+ def translate_exception(exception, message)
346
+ return exception unless exception.respond_to?(:result)
347
+
348
+ case exception.message
349
+ when /duplicate key value violates unique constraint/
350
+ RecordNotUnique.new(message, exception)
351
+ when /violates foreign key constraint/
352
+ InvalidForeignKey.new(message, exception)
353
+ else
354
+ super
355
+ end
356
+ end
357
+
358
+ private
359
+
360
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
361
+ if !type_map.key?(oid)
362
+ load_additional_types(type_map, [oid])
363
+ end
364
+
365
+ type_map.fetch(oid, fmod, sql_type) {
366
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
367
+ Type::Value.new.tap do |cast_type|
368
+ type_map.register_type(oid, cast_type)
369
+ end
370
+ }
371
+ end
372
+
373
+ def initialize_type_map(m) # :nodoc:
374
+ register_class_with_limit m, 'int2', Type::Integer
375
+ register_class_with_limit m, 'int4', Type::Integer
376
+ register_class_with_limit m, 'int8', Type::Integer
377
+ m.alias_type 'oid', 'int2'
378
+ m.register_type 'float4', Type::Float.new
379
+ m.alias_type 'float8', 'float4'
380
+ m.register_type 'text', Type::Text.new
381
+ register_class_with_limit m, 'varchar', Type::String
382
+ m.alias_type 'char', 'varchar'
383
+ m.alias_type 'name', 'varchar'
384
+ m.alias_type 'bpchar', 'varchar'
385
+ m.register_type 'bool', Type::Boolean.new
386
+ m.alias_type 'timestamptz', 'timestamp'
387
+ m.register_type 'date', Type::Date.new
388
+ m.register_type 'time', Type::Time.new
389
+
390
+ m.register_type 'timestamp' do |_, _, sql_type|
391
+ precision = extract_precision(sql_type)
392
+ OID::DateTime.new(precision: precision)
393
+ end
394
+
395
+ m.register_type 'numeric' do |_, fmod, sql_type|
396
+ precision = extract_precision(sql_type)
397
+ scale = extract_scale(sql_type)
398
+
399
+ # The type for the numeric depends on the width of the field,
400
+ # so we'll do something special here.
401
+ #
402
+ # When dealing with decimal columns:
403
+ #
404
+ # places after decimal = fmod - 4 & 0xffff
405
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
406
+ if fmod && (fmod - 4 & 0xffff).zero?
407
+ # FIXME: Remove this class, and the second argument to
408
+ # lookups on PG
409
+ Type::DecimalWithoutScale.new(precision: precision)
410
+ else
411
+ OID::Decimal.new(precision: precision, scale: scale)
412
+ end
413
+ end
414
+
415
+ load_additional_types(m)
416
+ end
417
+
418
+ def extract_limit(sql_type) # :nodoc:
419
+ case sql_type
420
+ when /^bigint/i, /^int8/i
421
+ 8
422
+ when /^smallint/i
423
+ 2
424
+ else
425
+ super
426
+ end
427
+ end
428
+
429
+ # Extracts the value from a PostgreSQL column default definition.
430
+ def extract_value_from_default(default) # :nodoc:
431
+ case default
432
+ # Quoted types
433
+ when /\A[\(B]?'(.*)'::/m
434
+ $1.gsub(/''/, "'")
435
+ # Boolean types
436
+ when 'true', 'false'
437
+ default
438
+ # Numeric types
439
+ when /\A\(?(-?\d+(\.\d*)?)\)?\z/
440
+ $1
441
+ # Object identifier types
442
+ when /\A-?\d+\z/
443
+ $1
444
+ else
445
+ # Anything else is blank, some user type, or some function
446
+ # and we can't know the value of that, so return nil.
447
+ nil
448
+ end
449
+ end
450
+
451
+ def extract_default_function(default_value, default) # :nodoc:
452
+ default if has_default_function?(default_value, default)
453
+ end
454
+
455
+ def has_default_function?(default_value, default) # :nodoc:
456
+ !default_value && (%r{\w+\(.*\)} === default)
457
+ end
458
+
459
+ def load_additional_types(type_map, oids = nil) # :nodoc:
460
+ initializer = OID::TypeMapInitializer.new(type_map)
461
+
462
+ if supports_ranges?
463
+ query = <<-SQL
464
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
465
+ FROM pg_type as t
466
+ LEFT JOIN pg_range as r ON oid = rngtypid
467
+ SQL
468
+ else
469
+ query = <<-SQL
470
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
471
+ FROM pg_type as t
472
+ SQL
473
+ end
474
+
475
+ if oids
476
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
477
+ end
478
+
479
+ execute_and_clear(query, 'SCHEMA', []) do |records|
480
+ initializer.run(records)
481
+ end
482
+ end
483
+
484
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
485
+
486
+ def execute_and_clear(sql, name, binds, prepare: false)
487
+ if without_prepared_statement?(binds)
488
+ result = exec_no_cache(sql, name, [])
489
+ elsif !prepare
490
+ result = exec_no_cache(sql, name, binds)
491
+ else
492
+ result = exec_cache(sql, name, binds)
493
+ end
494
+ ret = yield result
495
+ result.clear
496
+ ret
497
+ end
498
+
499
+ def exec_no_cache(sql, name, binds)
500
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
501
+ end
502
+
503
+ def exec_cache(sql, name, binds)
504
+ stmt_key = prepare_statement(sql)
505
+ type_casted_binds = binds.map { |col, val|
506
+ [col, type_cast(val, col)]
507
+ }
508
+
509
+ log(sql, name, type_casted_binds, stmt_key) do
510
+ @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
511
+ end
512
+ rescue ActiveRecord::StatementInvalid => e
513
+ pgerror = e.original_exception
514
+
515
+ # Get the PG code for the failure. Annoyingly, the code for
516
+ # prepared statements whose return value may have changed is
517
+ # FEATURE_NOT_SUPPORTED. Check here for more details:
518
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
519
+ begin
520
+ code = pgerror.result.result_error_field(PG::Result::PG_DIAG_SQLSTATE)
521
+ rescue
522
+ raise e
523
+ end
524
+ if FEATURE_NOT_SUPPORTED == code
525
+ @statements.delete sql_key(sql)
526
+ retry
527
+ else
528
+ raise e
529
+ end
530
+ end
531
+
532
+ # Returns the statement identifier for the client side cache
533
+ # of statements
534
+ def sql_key(sql)
535
+ "#{schema_search_path}-#{sql}"
536
+ end
537
+
538
+ # Prepare the statement if it hasn't been prepared, return
539
+ # the statement key.
540
+ def prepare_statement(sql)
541
+ sql_key = sql_key(sql)
542
+ unless @statements.key? sql_key
543
+ nextkey = @statements.next_key
544
+ begin
545
+ @connection.prepare nextkey, sql
546
+ rescue => e
547
+ raise translate_exception_class(e, sql)
548
+ end
549
+ # Clear the queue
550
+ @connection.get_last_result
551
+ @statements[sql_key] = nextkey
552
+ end
553
+ @statements[sql_key]
554
+ end
555
+
556
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
557
+ # connected server's characteristics.
558
+ def connect
559
+ @connection = PG::Connection.connect(@connection_parameters)
560
+
561
+ configure_connection
562
+ rescue ::PG::Error => error
563
+ if error.message.include?("does not exist")
564
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
565
+ else
566
+ raise
567
+ end
568
+ end
569
+
570
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
571
+ # This is called by #connect and should not be called manually.
572
+ def configure_connection
573
+ if @config[:encoding]
574
+ @connection.set_client_encoding(@config[:encoding])
575
+ end
576
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
577
+
578
+ # SET statements from :variables config hash
579
+ # http://www.postgresql.org/docs/8.3/static/sql-set.html
580
+ variables = @config[:variables] || {}
581
+ variables.map do |k, v|
582
+ if v == ':default' || v == :default
583
+ # Sets the value to the global or compile default
584
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
585
+ elsif !v.nil?
586
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
587
+ end
588
+ end
589
+ end
590
+
591
+ def last_insert_id_result(sequence_name) #:nodoc:
592
+ exec_query("SELECT currval('#{sequence_name}')", 'SQL')
593
+ end
594
+
595
+ # Returns the list of a table's column names, data types, and default values.
596
+ #
597
+ # The underlying query is roughly:
598
+ # SELECT column.name, column.type, default.value
599
+ # FROM column LEFT JOIN default
600
+ # ON column.table_id = default.table_id
601
+ # AND column.num = default.column_num
602
+ # WHERE column.table_id = get_table_id('table_name')
603
+ # AND column.num > 0
604
+ # AND NOT column.is_dropped
605
+ # ORDER BY column.num
606
+ #
607
+ # If the table name is not prefixed with a schema, the database will
608
+ # take the first match from the schema search path.
609
+ #
610
+ # Query implementation notes:
611
+ # - format_type includes the column size constraint, e.g. varchar(50)
612
+ # - ::regclass is a function that gives the id for a table name
613
+ def column_definitions(table_name) # :nodoc:
614
+ query(<<-end_sql, 'SCHEMA')
615
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
616
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
617
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
618
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
619
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
620
+ AND a.attnum > 0 AND NOT a.attisdropped
621
+ ORDER BY a.attnum
622
+ end_sql
623
+ end
624
+
625
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
626
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
627
+ $1.strip if $1
628
+ end
629
+
630
+ def create_table_definition(*args) # :nodoc:
631
+ Redshift::TableDefinition.new(*args)
632
+ end
633
+ end
634
+ end
635
+ end