activerecord4-redshift-adapter 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +54 -0
- data/README.md +40 -0
- data/lib/active_record/connection_adapters/redshift/array_parser.rb +97 -0
- data/lib/active_record/connection_adapters/redshift/cast.rb +156 -0
- data/lib/active_record/connection_adapters/redshift/database_statements.rb +242 -0
- data/lib/active_record/connection_adapters/redshift/oid.rb +366 -0
- data/lib/active_record/connection_adapters/redshift/quoting.rb +172 -0
- data/lib/active_record/connection_adapters/redshift/referential_integrity.rb +30 -0
- data/lib/active_record/connection_adapters/redshift/schema_statements.rb +435 -0
- data/lib/active_record/connection_adapters/redshift_adapter.rb +909 -0
- metadata +87 -0
@@ -0,0 +1,909 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/statement_pool'
|
3
|
+
require 'active_record/connection_adapters/redshift/oid'
|
4
|
+
require 'active_record/connection_adapters/redshift/cast'
|
5
|
+
require 'active_record/connection_adapters/redshift/array_parser'
|
6
|
+
require 'active_record/connection_adapters/redshift/quoting'
|
7
|
+
require 'active_record/connection_adapters/redshift/schema_statements'
|
8
|
+
require 'active_record/connection_adapters/redshift/database_statements'
|
9
|
+
require 'active_record/connection_adapters/redshift/referential_integrity'
|
10
|
+
require 'arel/visitors/bind_visitor'
|
11
|
+
|
12
|
+
# Make sure we're using pg high enough for PGResult#values
|
13
|
+
gem 'pg', '~> 0.11'
|
14
|
+
require 'pg'
|
15
|
+
|
16
|
+
require 'ipaddr'
|
17
|
+
|
18
|
+
module ActiveRecord
|
19
|
+
module ConnectionHandling # :nodoc:
|
20
|
+
RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
|
21
|
+
:client_encoding, :options, :application_name, :fallback_application_name,
|
22
|
+
:keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
|
23
|
+
:tty, :sslmode, :requiressl, :sslcert, :sslkey, :sslrootcert, :sslcrl,
|
24
|
+
:requirepeer, :krbsrvname, :gsslib, :service]
|
25
|
+
|
26
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
27
|
+
def redshift_connection(config)
|
28
|
+
conn_params = config.symbolize_keys
|
29
|
+
|
30
|
+
conn_params.delete_if { |_, v| v.nil? }
|
31
|
+
|
32
|
+
# Map ActiveRecords param names to PGs.
|
33
|
+
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
34
|
+
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
35
|
+
|
36
|
+
# Forward only valid config params to PGconn.connect.
|
37
|
+
conn_params.keep_if { |k, _| RS_VALID_CONN_PARAMS.include?(k) }
|
38
|
+
|
39
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
40
|
+
# so just pass a nil connection object for the time being.
|
41
|
+
ConnectionAdapters::RedshiftAdapter.new(nil, logger, conn_params, config)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ConnectionAdapters
|
46
|
+
# PostgreSQL-specific extensions to column definitions in a table.
|
47
|
+
class RedshiftColumn < Column #:nodoc:
|
48
|
+
attr_accessor :array
|
49
|
+
# Instantiates a new PostgreSQL column definition in a table.
|
50
|
+
def initialize(name, default, oid_type, sql_type = nil, null = true)
|
51
|
+
@oid_type = oid_type
|
52
|
+
if sql_type =~ /\[\]$/
|
53
|
+
@array = true
|
54
|
+
super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
|
55
|
+
else
|
56
|
+
@array = false
|
57
|
+
super(name, self.class.extract_value_from_default(default), sql_type, null)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# :stopdoc:
|
62
|
+
class << self
|
63
|
+
include ConnectionAdapters::RedshiftColumn::Cast
|
64
|
+
include ConnectionAdapters::RedshiftColumn::ArrayParser
|
65
|
+
attr_accessor :money_precision
|
66
|
+
end
|
67
|
+
# :startdoc:
|
68
|
+
|
69
|
+
# Extracts the value from a PostgreSQL column default definition.
|
70
|
+
def self.extract_value_from_default(default)
|
71
|
+
# This is a performance optimization for Ruby 1.9.2 in development.
|
72
|
+
# If the value is nil, we return nil straight away without checking
|
73
|
+
# the regular expressions. If we check each regular expression,
|
74
|
+
# Regexp#=== will call NilClass#to_str, which will trigger
|
75
|
+
# method_missing (defined by whiny nil in ActiveSupport) which
|
76
|
+
# makes this method very very slow.
|
77
|
+
return default unless default
|
78
|
+
|
79
|
+
case default
|
80
|
+
when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
|
81
|
+
$1
|
82
|
+
# Numeric types
|
83
|
+
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
|
84
|
+
$1
|
85
|
+
# Character types
|
86
|
+
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
|
87
|
+
$1
|
88
|
+
# Binary data types
|
89
|
+
when /\A'(.*)'::bytea\z/m
|
90
|
+
$1
|
91
|
+
# Date/time types
|
92
|
+
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
|
93
|
+
$1
|
94
|
+
when /\A'(.*)'::interval\z/
|
95
|
+
$1
|
96
|
+
# Boolean type
|
97
|
+
when 'true'
|
98
|
+
true
|
99
|
+
when 'false'
|
100
|
+
false
|
101
|
+
# Geometric types
|
102
|
+
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
|
103
|
+
$1
|
104
|
+
# Network address types
|
105
|
+
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
|
106
|
+
$1
|
107
|
+
# Bit string types
|
108
|
+
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
|
109
|
+
$1
|
110
|
+
# XML type
|
111
|
+
when /\A'(.*)'::xml\z/m
|
112
|
+
$1
|
113
|
+
# Arrays
|
114
|
+
when /\A'(.*)'::"?\D+"?\[\]\z/
|
115
|
+
$1
|
116
|
+
# Hstore
|
117
|
+
when /\A'(.*)'::hstore\z/
|
118
|
+
$1
|
119
|
+
# JSON
|
120
|
+
when /\A'(.*)'::json\z/
|
121
|
+
$1
|
122
|
+
# Object identifier types
|
123
|
+
when /\A-?\d+\z/
|
124
|
+
$1
|
125
|
+
else
|
126
|
+
# Anything else is blank, some user type, or some function
|
127
|
+
# and we can't know the value of that, so return nil.
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def type_cast(value)
|
133
|
+
return if value.nil?
|
134
|
+
return super if encoded?
|
135
|
+
|
136
|
+
@oid_type.type_cast value
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def extract_limit(sql_type)
|
142
|
+
case sql_type
|
143
|
+
when /^bigint/i; 8
|
144
|
+
when /^smallint/i; 2
|
145
|
+
when /^timestamp/i; nil
|
146
|
+
else super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Extracts the scale from PostgreSQL-specific data types.
|
151
|
+
def extract_scale(sql_type)
|
152
|
+
# Money type has a fixed scale of 2.
|
153
|
+
sql_type =~ /^money/ ? 2 : super
|
154
|
+
end
|
155
|
+
|
156
|
+
# Extracts the precision from PostgreSQL-specific data types.
|
157
|
+
def extract_precision(sql_type)
|
158
|
+
if sql_type == 'money'
|
159
|
+
self.class.money_precision
|
160
|
+
elsif sql_type =~ /timestamp/i
|
161
|
+
$1.to_i if sql_type =~ /\((\d+)\)/
|
162
|
+
else
|
163
|
+
super
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Maps PostgreSQL-specific data types to logical Rails types.
|
168
|
+
def simplified_type(field_type)
|
169
|
+
case field_type
|
170
|
+
# Numeric and monetary types
|
171
|
+
when /^(?:real|double precision)$/
|
172
|
+
:float
|
173
|
+
# Monetary types
|
174
|
+
when 'money'
|
175
|
+
:decimal
|
176
|
+
when 'hstore'
|
177
|
+
:hstore
|
178
|
+
when 'ltree'
|
179
|
+
:ltree
|
180
|
+
# Network address types
|
181
|
+
when 'inet'
|
182
|
+
:inet
|
183
|
+
when 'cidr'
|
184
|
+
:cidr
|
185
|
+
when 'macaddr'
|
186
|
+
:macaddr
|
187
|
+
# Character types
|
188
|
+
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
|
189
|
+
:string
|
190
|
+
# Binary data types
|
191
|
+
when 'bytea'
|
192
|
+
:binary
|
193
|
+
# Date/time types
|
194
|
+
when /^timestamp with(?:out)? time zone$/
|
195
|
+
:datetime
|
196
|
+
when /^interval(?:|\(\d+\))$/
|
197
|
+
:string
|
198
|
+
# Geometric types
|
199
|
+
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
|
200
|
+
:string
|
201
|
+
# Bit strings
|
202
|
+
when /^bit(?: varying)?(?:\(\d+\))?$/
|
203
|
+
:string
|
204
|
+
# XML type
|
205
|
+
when 'xml'
|
206
|
+
:xml
|
207
|
+
# tsvector type
|
208
|
+
when 'tsvector'
|
209
|
+
:tsvector
|
210
|
+
# Arrays
|
211
|
+
when /^\D+\[\]$/
|
212
|
+
:string
|
213
|
+
# Object identifier types
|
214
|
+
when 'oid'
|
215
|
+
:integer
|
216
|
+
# UUID type
|
217
|
+
when 'uuid'
|
218
|
+
:uuid
|
219
|
+
# JSON type
|
220
|
+
when 'json'
|
221
|
+
:json
|
222
|
+
# Small and big integer types
|
223
|
+
when /^(?:small|big)int$/
|
224
|
+
:integer
|
225
|
+
when /(num|date|tstz|ts|int4|int8)range$/
|
226
|
+
field_type.to_sym
|
227
|
+
# Pass through all types that are not specific to PostgreSQL.
|
228
|
+
else
|
229
|
+
super
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
|
235
|
+
#
|
236
|
+
# Options:
|
237
|
+
#
|
238
|
+
# * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets,
|
239
|
+
# the default is to connect to localhost.
|
240
|
+
# * <tt>:port</tt> - Defaults to 5432.
|
241
|
+
# * <tt>:username</tt> - Defaults to be the same as the operating system name of the user running the application.
|
242
|
+
# * <tt>:password</tt> - Password to be used if the server demands password authentication.
|
243
|
+
# * <tt>:database</tt> - Defaults to be the same as the user name.
|
244
|
+
# * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
|
245
|
+
# as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
|
246
|
+
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
|
247
|
+
# <encoding></tt> call on the connection.
|
248
|
+
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
|
249
|
+
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
|
250
|
+
# * <tt>:variables</tt> - An optional hash of additional parameters that
|
251
|
+
# will be used in <tt>SET SESSION key = val</tt> calls on the connection.
|
252
|
+
# * <tt>:insert_returning</tt> - does nothing for Redshift.
|
253
|
+
#
|
254
|
+
# Any further options are used as connection parameters to libpq. See
|
255
|
+
# http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the
|
256
|
+
# list of parameters.
|
257
|
+
#
|
258
|
+
# In addition, default connection parameters of libpq can be set per environment variables.
|
259
|
+
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
|
260
|
+
class RedshiftAdapter < AbstractAdapter
|
261
|
+
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
|
262
|
+
attr_accessor :array
|
263
|
+
end
|
264
|
+
|
265
|
+
module ColumnMethods
|
266
|
+
def xml(*args)
|
267
|
+
options = args.extract_options!
|
268
|
+
column(args[0], 'xml', options)
|
269
|
+
end
|
270
|
+
|
271
|
+
def tsvector(*args)
|
272
|
+
options = args.extract_options!
|
273
|
+
column(args[0], 'tsvector', options)
|
274
|
+
end
|
275
|
+
|
276
|
+
def int4range(name, options = {})
|
277
|
+
column(name, 'int4range', options)
|
278
|
+
end
|
279
|
+
|
280
|
+
def int8range(name, options = {})
|
281
|
+
column(name, 'int8range', options)
|
282
|
+
end
|
283
|
+
|
284
|
+
def tsrange(name, options = {})
|
285
|
+
column(name, 'tsrange', options)
|
286
|
+
end
|
287
|
+
|
288
|
+
def tstzrange(name, options = {})
|
289
|
+
column(name, 'tstzrange', options)
|
290
|
+
end
|
291
|
+
|
292
|
+
def numrange(name, options = {})
|
293
|
+
column(name, 'numrange', options)
|
294
|
+
end
|
295
|
+
|
296
|
+
def daterange(name, options = {})
|
297
|
+
column(name, 'daterange', options)
|
298
|
+
end
|
299
|
+
|
300
|
+
def hstore(name, options = {})
|
301
|
+
column(name, 'hstore', options)
|
302
|
+
end
|
303
|
+
|
304
|
+
def ltree(name, options = {})
|
305
|
+
column(name, 'ltree', options)
|
306
|
+
end
|
307
|
+
|
308
|
+
def inet(name, options = {})
|
309
|
+
column(name, 'inet', options)
|
310
|
+
end
|
311
|
+
|
312
|
+
def cidr(name, options = {})
|
313
|
+
column(name, 'cidr', options)
|
314
|
+
end
|
315
|
+
|
316
|
+
def macaddr(name, options = {})
|
317
|
+
column(name, 'macaddr', options)
|
318
|
+
end
|
319
|
+
|
320
|
+
def uuid(name, options = {})
|
321
|
+
column(name, 'uuid', options)
|
322
|
+
end
|
323
|
+
|
324
|
+
def json(name, options = {})
|
325
|
+
column(name, 'json', options)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
330
|
+
include ColumnMethods
|
331
|
+
|
332
|
+
# Defines the primary key field.
|
333
|
+
# Use of the native PostgreSQL UUID type is supported, and can be used
|
334
|
+
# by defining your tables as such:
|
335
|
+
#
|
336
|
+
# create_table :stuffs, id: :uuid do |t|
|
337
|
+
# t.string :content
|
338
|
+
# t.timestamps
|
339
|
+
# end
|
340
|
+
#
|
341
|
+
# By default, this will use the +uuid_generate_v4()+ function from the
|
342
|
+
# +uuid-ossp+ extension, which MUST be enabled on your databse. To enable
|
343
|
+
# the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
|
344
|
+
# migrations To use a UUID primary key without +uuid-ossp+ enabled, you can
|
345
|
+
# set the +:default+ option to nil:
|
346
|
+
#
|
347
|
+
# create_table :stuffs, id: false do |t|
|
348
|
+
# t.primary_key :id, :uuid, default: nil
|
349
|
+
# t.uuid :foo_id
|
350
|
+
# t.timestamps
|
351
|
+
# end
|
352
|
+
#
|
353
|
+
# You may also pass a different UUID generation function from +uuid-ossp+
|
354
|
+
# or another library.
|
355
|
+
#
|
356
|
+
# Note that setting the UUID primary key default value to +nil+
|
357
|
+
# will require you to assure that you always provide a UUID value
|
358
|
+
# before saving a record (as primary keys cannot be nil). This might be
|
359
|
+
# done via the SecureRandom.uuid method and a +before_save+ callback,
|
360
|
+
# for instance.
|
361
|
+
def primary_key(name, type = :primary_key, options = {})
|
362
|
+
return super unless type == :uuid
|
363
|
+
options[:default] = options.fetch(:default, 'uuid_generate_v4()')
|
364
|
+
options[:primary_key] = true
|
365
|
+
column name, type, options
|
366
|
+
end
|
367
|
+
|
368
|
+
def column(name, type = nil, options = {})
|
369
|
+
super
|
370
|
+
column = self[name]
|
371
|
+
column.array = options[:array]
|
372
|
+
|
373
|
+
self
|
374
|
+
end
|
375
|
+
|
376
|
+
def xml(options = {})
|
377
|
+
column(args[0], :text, options)
|
378
|
+
end
|
379
|
+
|
380
|
+
private
|
381
|
+
|
382
|
+
def create_column_definition(name, type)
|
383
|
+
ColumnDefinition.new name, type
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
class Table < ActiveRecord::ConnectionAdapters::Table
|
388
|
+
include ColumnMethods
|
389
|
+
end
|
390
|
+
|
391
|
+
ADAPTER_NAME = 'Redshift'
|
392
|
+
|
393
|
+
NATIVE_DATABASE_TYPES = {
|
394
|
+
primary_key: "serial primary key",
|
395
|
+
string: { name: "character varying", limit: 255 },
|
396
|
+
text: { name: "text" },
|
397
|
+
integer: { name: "integer" },
|
398
|
+
float: { name: "float" },
|
399
|
+
decimal: { name: "decimal" },
|
400
|
+
datetime: { name: "timestamp" },
|
401
|
+
timestamp: { name: "timestamp" },
|
402
|
+
time: { name: "time" },
|
403
|
+
date: { name: "date" },
|
404
|
+
daterange: { name: "daterange" },
|
405
|
+
numrange: { name: "numrange" },
|
406
|
+
tsrange: { name: "tsrange" },
|
407
|
+
tstzrange: { name: "tstzrange" },
|
408
|
+
int4range: { name: "int4range" },
|
409
|
+
int8range: { name: "int8range" },
|
410
|
+
binary: { name: "bytea" },
|
411
|
+
boolean: { name: "boolean" },
|
412
|
+
xml: { name: "xml" },
|
413
|
+
tsvector: { name: "tsvector" },
|
414
|
+
hstore: { name: "hstore" },
|
415
|
+
inet: { name: "inet" },
|
416
|
+
cidr: { name: "cidr" },
|
417
|
+
macaddr: { name: "macaddr" },
|
418
|
+
uuid: { name: "uuid" },
|
419
|
+
json: { name: "json" },
|
420
|
+
ltree: { name: "ltree" }
|
421
|
+
}
|
422
|
+
|
423
|
+
include Quoting
|
424
|
+
include ReferentialIntegrity
|
425
|
+
include SchemaStatements
|
426
|
+
include DatabaseStatements
|
427
|
+
|
428
|
+
# Returns 'PostgreSQL' as adapter name for identification purposes.
|
429
|
+
def adapter_name
|
430
|
+
ADAPTER_NAME
|
431
|
+
end
|
432
|
+
|
433
|
+
# Adds `:array` option to the default set provided by the
|
434
|
+
# AbstractAdapter
|
435
|
+
def prepare_column_options(column, types)
|
436
|
+
spec = super
|
437
|
+
spec[:array] = 'true' if column.respond_to?(:array) && column.array
|
438
|
+
spec
|
439
|
+
end
|
440
|
+
|
441
|
+
# Adds `:array` as a valid migration key
|
442
|
+
def migration_keys
|
443
|
+
super + [:array]
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns +true+, since this connection adapter supports prepared statement
|
447
|
+
# caching.
|
448
|
+
def supports_statement_cache?
|
449
|
+
true
|
450
|
+
end
|
451
|
+
|
452
|
+
def supports_index_sort_order?
|
453
|
+
true
|
454
|
+
end
|
455
|
+
|
456
|
+
def supports_partial_index?
|
457
|
+
true
|
458
|
+
end
|
459
|
+
|
460
|
+
def supports_transaction_isolation?
|
461
|
+
true
|
462
|
+
end
|
463
|
+
|
464
|
+
def index_algorithms
|
465
|
+
{ concurrently: 'CONCURRENTLY' }
|
466
|
+
end
|
467
|
+
|
468
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
469
|
+
def initialize(connection, max)
|
470
|
+
super
|
471
|
+
@counter = 0
|
472
|
+
@cache = Hash.new { |h,pid| h[pid] = {} }
|
473
|
+
end
|
474
|
+
|
475
|
+
def each(&block); cache.each(&block); end
|
476
|
+
def key?(key); cache.key?(key); end
|
477
|
+
def [](key); cache[key]; end
|
478
|
+
def length; cache.length; end
|
479
|
+
|
480
|
+
def next_key
|
481
|
+
"a#{@counter + 1}"
|
482
|
+
end
|
483
|
+
|
484
|
+
def []=(sql, key)
|
485
|
+
while @max <= cache.size
|
486
|
+
dealloc(cache.shift.last)
|
487
|
+
end
|
488
|
+
@counter += 1
|
489
|
+
cache[sql] = key
|
490
|
+
end
|
491
|
+
|
492
|
+
def clear
|
493
|
+
cache.each_value do |stmt_key|
|
494
|
+
dealloc stmt_key
|
495
|
+
end
|
496
|
+
cache.clear
|
497
|
+
end
|
498
|
+
|
499
|
+
def delete(sql_key)
|
500
|
+
dealloc cache[sql_key]
|
501
|
+
cache.delete sql_key
|
502
|
+
end
|
503
|
+
|
504
|
+
private
|
505
|
+
|
506
|
+
def cache
|
507
|
+
@cache[Process.pid]
|
508
|
+
end
|
509
|
+
|
510
|
+
def dealloc(key)
|
511
|
+
@connection.query "DEALLOCATE #{key}" if connection_active?
|
512
|
+
end
|
513
|
+
|
514
|
+
def connection_active?
|
515
|
+
@connection.status == PGconn::CONNECTION_OK
|
516
|
+
rescue PGError
|
517
|
+
false
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
|
522
|
+
include Arel::Visitors::BindVisitor
|
523
|
+
end
|
524
|
+
|
525
|
+
# Initializes and connects a PostgreSQL adapter.
|
526
|
+
def initialize(connection, logger, connection_parameters, config)
|
527
|
+
super(connection, logger)
|
528
|
+
|
529
|
+
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
|
530
|
+
@visitor = Arel::Visitors::PostgreSQL.new self
|
531
|
+
else
|
532
|
+
@visitor = unprepared_visitor
|
533
|
+
end
|
534
|
+
|
535
|
+
connection_parameters.delete :prepared_statements
|
536
|
+
@connection_parameters, @config = connection_parameters, config
|
537
|
+
|
538
|
+
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
|
539
|
+
@local_tz = nil
|
540
|
+
@table_alias_length = nil
|
541
|
+
|
542
|
+
connect
|
543
|
+
@statements = StatementPool.new @connection,
|
544
|
+
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
|
545
|
+
|
546
|
+
initialize_type_map
|
547
|
+
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
|
548
|
+
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : false
|
549
|
+
end
|
550
|
+
|
551
|
+
# Clears the prepared statements cache.
|
552
|
+
def clear_cache!
|
553
|
+
@statements.clear
|
554
|
+
end
|
555
|
+
|
556
|
+
# Is this connection alive and ready for queries?
|
557
|
+
def active?
|
558
|
+
@connection.connect_poll != PG::PGRES_POLLING_FAILED
|
559
|
+
rescue PGError
|
560
|
+
false
|
561
|
+
end
|
562
|
+
|
563
|
+
# Close then reopen the connection.
|
564
|
+
def reconnect!
|
565
|
+
super
|
566
|
+
@connection.reset
|
567
|
+
configure_connection
|
568
|
+
end
|
569
|
+
|
570
|
+
def reset!
|
571
|
+
clear_cache!
|
572
|
+
super
|
573
|
+
end
|
574
|
+
|
575
|
+
# Disconnects from the database if already connected. Otherwise, this
|
576
|
+
# method does nothing.
|
577
|
+
def disconnect!
|
578
|
+
super
|
579
|
+
@connection.close rescue nil
|
580
|
+
end
|
581
|
+
|
582
|
+
def native_database_types #:nodoc:
|
583
|
+
NATIVE_DATABASE_TYPES
|
584
|
+
end
|
585
|
+
|
586
|
+
# Returns true, since this connection adapter supports migrations.
|
587
|
+
def supports_migrations?
|
588
|
+
true
|
589
|
+
end
|
590
|
+
|
591
|
+
# Does PostgreSQL support finding primary key on non-Active Record tables?
|
592
|
+
def supports_primary_key? #:nodoc:
|
593
|
+
true
|
594
|
+
end
|
595
|
+
|
596
|
+
def supports_insert_with_returning?
|
597
|
+
false
|
598
|
+
end
|
599
|
+
|
600
|
+
def supports_ddl_transactions?
|
601
|
+
true
|
602
|
+
end
|
603
|
+
|
604
|
+
# Returns true, since this connection adapter supports savepoints.
|
605
|
+
def supports_savepoints?
|
606
|
+
false
|
607
|
+
end
|
608
|
+
|
609
|
+
def supports_import?
|
610
|
+
true
|
611
|
+
end
|
612
|
+
|
613
|
+
# Returns true.
|
614
|
+
def supports_explain?
|
615
|
+
true
|
616
|
+
end
|
617
|
+
|
618
|
+
# Returns true if pg > 9.2
|
619
|
+
def supports_extensions?
|
620
|
+
false
|
621
|
+
end
|
622
|
+
|
623
|
+
# Range datatypes weren't introduced until PostgreSQL 9.2
|
624
|
+
def supports_ranges?
|
625
|
+
false
|
626
|
+
end
|
627
|
+
|
628
|
+
def enable_extension(name)
|
629
|
+
end
|
630
|
+
|
631
|
+
def disable_extension(name)
|
632
|
+
end
|
633
|
+
|
634
|
+
def extension_enabled?(name)
|
635
|
+
false
|
636
|
+
end
|
637
|
+
|
638
|
+
#def extensions
|
639
|
+
#end
|
640
|
+
|
641
|
+
# Returns the configured supported identifier length supported by PostgreSQL
|
642
|
+
def table_alias_length
|
643
|
+
@table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
|
644
|
+
end
|
645
|
+
|
646
|
+
# Set the authorized user for this session
|
647
|
+
def session_auth=(user)
|
648
|
+
clear_cache!
|
649
|
+
exec_query "SET SESSION AUTHORIZATION #{user}"
|
650
|
+
end
|
651
|
+
|
652
|
+
module Utils
|
653
|
+
extend self
|
654
|
+
|
655
|
+
# Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
|
656
|
+
# +schema_name+ is nil if not specified in +name+.
|
657
|
+
# +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
|
658
|
+
# +name+ supports the range of schema/table references understood by PostgreSQL, for example:
|
659
|
+
#
|
660
|
+
# * <tt>table_name</tt>
|
661
|
+
# * <tt>"table.name"</tt>
|
662
|
+
# * <tt>schema_name.table_name</tt>
|
663
|
+
# * <tt>schema_name."table.name"</tt>
|
664
|
+
# * <tt>"schema.name"."table name"</tt>
|
665
|
+
def extract_schema_and_table(name)
|
666
|
+
table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
|
667
|
+
[schema, table]
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def use_insert_returning?
|
672
|
+
false
|
673
|
+
end
|
674
|
+
|
675
|
+
def valid_type?(type)
|
676
|
+
!native_database_types[type].nil?
|
677
|
+
end
|
678
|
+
|
679
|
+
protected
|
680
|
+
|
681
|
+
# Returns the version of the connected PostgreSQL server.
|
682
|
+
def postgresql_version
|
683
|
+
@connection.server_version
|
684
|
+
end
|
685
|
+
|
686
|
+
# See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html
|
687
|
+
FOREIGN_KEY_VIOLATION = "23503"
|
688
|
+
UNIQUE_VIOLATION = "23505"
|
689
|
+
|
690
|
+
def translate_exception(exception, message)
|
691
|
+
return exception unless exception.respond_to?(:result)
|
692
|
+
|
693
|
+
case exception.message
|
694
|
+
when /duplicate key value violates unique constraint/
|
695
|
+
RecordNotUnique.new(message, exception)
|
696
|
+
when /violates foreign key constraint/
|
697
|
+
InvalidForeignKey.new(message, exception)
|
698
|
+
else
|
699
|
+
super
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
private
|
704
|
+
|
705
|
+
def reload_type_map
|
706
|
+
OID::TYPE_MAP.clear
|
707
|
+
initialize_type_map
|
708
|
+
end
|
709
|
+
|
710
|
+
def initialize_type_map
|
711
|
+
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
|
712
|
+
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
|
713
|
+
|
714
|
+
# populate the leaf nodes
|
715
|
+
leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
|
716
|
+
OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
|
717
|
+
end
|
718
|
+
|
719
|
+
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
|
720
|
+
|
721
|
+
# populate composite types
|
722
|
+
nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
|
723
|
+
if OID.registered_type? row['typname']
|
724
|
+
# this composite type is explicitly registered
|
725
|
+
vector = OID::NAMES[row['typname']]
|
726
|
+
else
|
727
|
+
# use the default for composite types
|
728
|
+
vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
|
729
|
+
end
|
730
|
+
|
731
|
+
OID::TYPE_MAP[row['oid'].to_i] = vector
|
732
|
+
end
|
733
|
+
|
734
|
+
# populate array types
|
735
|
+
arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
|
736
|
+
array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
|
737
|
+
OID::TYPE_MAP[row['oid'].to_i] = array
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
|
742
|
+
|
743
|
+
def exec_no_cache(sql, binds)
|
744
|
+
@connection.async_exec(sql, [])
|
745
|
+
end
|
746
|
+
|
747
|
+
def exec_cache(sql, binds)
|
748
|
+
stmt_key = prepare_statement sql
|
749
|
+
|
750
|
+
# Clear the queue
|
751
|
+
@connection.get_last_result
|
752
|
+
@connection.send_query_prepared(stmt_key, binds.map { |col, val|
|
753
|
+
type_cast(val, col)
|
754
|
+
})
|
755
|
+
@connection.block
|
756
|
+
@connection.get_last_result
|
757
|
+
rescue PGError => e
|
758
|
+
# Get the PG code for the failure. Annoyingly, the code for
|
759
|
+
# prepared statements whose return value may have changed is
|
760
|
+
# FEATURE_NOT_SUPPORTED. Check here for more details:
|
761
|
+
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
|
762
|
+
code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
|
763
|
+
if FEATURE_NOT_SUPPORTED == code
|
764
|
+
@statements.delete sql_key(sql)
|
765
|
+
retry
|
766
|
+
else
|
767
|
+
raise e
|
768
|
+
end
|
769
|
+
end
|
770
|
+
|
771
|
+
# Returns the statement identifier for the client side cache
|
772
|
+
# of statements
|
773
|
+
def sql_key(sql)
|
774
|
+
"#{schema_search_path}-#{sql}"
|
775
|
+
end
|
776
|
+
|
777
|
+
# Prepare the statement if it hasn't been prepared, return
|
778
|
+
# the statement key.
|
779
|
+
def prepare_statement(sql)
|
780
|
+
sql_key = sql_key(sql)
|
781
|
+
unless @statements.key? sql_key
|
782
|
+
nextkey = @statements.next_key
|
783
|
+
@connection.prepare nextkey, sql
|
784
|
+
@statements[sql_key] = nextkey
|
785
|
+
end
|
786
|
+
@statements[sql_key]
|
787
|
+
end
|
788
|
+
|
789
|
+
# The internal PostgreSQL identifier of the money data type.
|
790
|
+
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
|
791
|
+
# The internal PostgreSQL identifier of the BYTEA data type.
|
792
|
+
BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
|
793
|
+
|
794
|
+
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
795
|
+
# connected server's characteristics.
|
796
|
+
def connect
|
797
|
+
@connection = PGconn.connect(@connection_parameters)
|
798
|
+
|
799
|
+
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
800
|
+
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
801
|
+
# should know about this but can't detect it there, so deal with it here.
|
802
|
+
RedshiftColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
|
803
|
+
|
804
|
+
configure_connection
|
805
|
+
end
|
806
|
+
|
807
|
+
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
|
808
|
+
# This is called by #connect and should not be called manually.
|
809
|
+
def configure_connection
|
810
|
+
if @config[:encoding]
|
811
|
+
@connection.set_client_encoding(@config[:encoding])
|
812
|
+
end
|
813
|
+
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
814
|
+
|
815
|
+
# SET statements from :variables config hash
|
816
|
+
# http://www.postgresql.org/docs/8.3/static/sql-set.html
|
817
|
+
variables = @config[:variables] || {}
|
818
|
+
variables.map do |k, v|
|
819
|
+
if v == ':default' || v == :default
|
820
|
+
# Sets the value to the global or compile default
|
821
|
+
execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
|
822
|
+
elsif !v.nil?
|
823
|
+
execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
|
824
|
+
end
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
# Returns the current ID of a table's sequence.
|
829
|
+
def last_insert_id(sequence_name) #:nodoc:
|
830
|
+
Integer(last_insert_id_value(sequence_name))
|
831
|
+
end
|
832
|
+
|
833
|
+
def last_insert_id_value(sequence_name)
|
834
|
+
last_insert_id_result(sequence_name).rows.first.first
|
835
|
+
end
|
836
|
+
|
837
|
+
def last_insert_id_result(sequence_name) #:nodoc:
|
838
|
+
exec_query("SELECT currval('#{sequence_name}')", 'SQL')
|
839
|
+
end
|
840
|
+
|
841
|
+
# Executes a SELECT query and returns the results, performing any data type
|
842
|
+
# conversions that are required to be performed here instead of in PostgreSQLColumn.
|
843
|
+
def select(sql, name = nil, binds = [])
|
844
|
+
exec_query(sql, name, binds)
|
845
|
+
end
|
846
|
+
|
847
|
+
def select_raw(sql, name = nil)
|
848
|
+
res = execute(sql, name)
|
849
|
+
results = result_as_array(res)
|
850
|
+
fields = res.fields
|
851
|
+
res.clear
|
852
|
+
return fields, results
|
853
|
+
end
|
854
|
+
|
855
|
+
# Returns the list of a table's column names, data types, and default values.
|
856
|
+
#
|
857
|
+
# The underlying query is roughly:
|
858
|
+
# SELECT column.name, column.type, default.value
|
859
|
+
# FROM column LEFT JOIN default
|
860
|
+
# ON column.table_id = default.table_id
|
861
|
+
# AND column.num = default.column_num
|
862
|
+
# WHERE column.table_id = get_table_id('table_name')
|
863
|
+
# AND column.num > 0
|
864
|
+
# AND NOT column.is_dropped
|
865
|
+
# ORDER BY column.num
|
866
|
+
#
|
867
|
+
# If the table name is not prefixed with a schema, the database will
|
868
|
+
# take the first match from the schema search path.
|
869
|
+
#
|
870
|
+
# Query implementation notes:
|
871
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
872
|
+
# - ::regclass is a function that gives the id for a table name
|
873
|
+
def column_definitions(table_name) #:nodoc:
|
874
|
+
exec_query(<<-end_sql, 'SCHEMA').rows
|
875
|
+
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
|
876
|
+
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
|
877
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
878
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
879
|
+
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
|
880
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
881
|
+
ORDER BY a.attnum
|
882
|
+
end_sql
|
883
|
+
end
|
884
|
+
|
885
|
+
def extract_pg_identifier_from_name(name)
|
886
|
+
match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
|
887
|
+
|
888
|
+
if match_data
|
889
|
+
rest = name[match_data[0].length, name.length]
|
890
|
+
rest = rest[1, rest.length] if rest.start_with? "."
|
891
|
+
[match_data[1], (rest.length > 0 ? rest : nil)]
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
def extract_table_ref_from_insert_sql(sql)
|
896
|
+
sql[/into\s+([^\(]*).*values\s*\(/i]
|
897
|
+
$1.strip if $1
|
898
|
+
end
|
899
|
+
|
900
|
+
def create_table_definition(name, temporary, options, as = nil)
|
901
|
+
TableDefinition.new native_database_types, name, temporary, options
|
902
|
+
end
|
903
|
+
|
904
|
+
def update_table_definition(table_name, base)
|
905
|
+
Table.new(table_name, base)
|
906
|
+
end
|
907
|
+
end
|
908
|
+
end
|
909
|
+
end
|