activerecord4-redshift-adapter 0.1.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 +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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a7e60cefd025afac2ee4f60f8206198ef8904c79
|
4
|
+
data.tar.gz: ef72bd8f85e0657b328fe9e1c53c5f084e78ce6e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dbd01e3a518651f0028dfb7c49eccf6f650ae14d14480c551e9b6e8e6706abcc132a6043ed22e4f6745712a87b053c0df96dbc7254ff8e0622b2c515c78a29d8
|
7
|
+
data.tar.gz: 52fed7e5a4c7ffb6bd4acc222afadb2b46a5e9fc7b19d75b104bb5a072c46dee2b0405ad0b95269f99e323fcbb5769c8e41e8d94d39beb23db95ffc47ccd9c6f
|
data/LICENSE
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
-----------------------------------------------------------------------------------
|
2
|
+
The MIT License (MIT)
|
3
|
+
|
4
|
+
Copyright (c) 2004-2013 David Heinemeier Hansson (original code author)
|
5
|
+
Copyright (c) 2013 Minero Aoki
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
8
|
+
this software and associated documentation files (the "Software"), to deal in
|
9
|
+
the Software without restriction, including without limitation the rights to
|
10
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
11
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
12
|
+
subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
19
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
20
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
21
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
22
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
-----------------------------------------------------------------------------------
|
25
|
+
Copyright (c) 2010-2013, Fiksu, Inc.
|
26
|
+
All rights reserved.
|
27
|
+
|
28
|
+
Redistribution and use in source and binary forms, with or without
|
29
|
+
modification, are permitted provided that the following conditions are
|
30
|
+
met:
|
31
|
+
|
32
|
+
o Redistributions of source code must retain the above copyright
|
33
|
+
notice, this list of conditions and the following disclaimer.
|
34
|
+
|
35
|
+
o Redistributions in binary form must reproduce the above copyright
|
36
|
+
notice, this list of conditions and the following disclaimer in the
|
37
|
+
documentation and/or other materials provided with the
|
38
|
+
distribution.
|
39
|
+
|
40
|
+
o Fiksu, Inc. nor the names of its contributors may be used to
|
41
|
+
endorse or promote products derived from this software without
|
42
|
+
specific prior written permission.
|
43
|
+
|
44
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
45
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
46
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
47
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
48
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
49
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
50
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
51
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
52
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
53
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
54
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
activerecord4-redshift-adapter
|
2
|
+
==============================
|
3
|
+
|
4
|
+
Amazon Redshift adapter for ActiveRecord 4 (Rails 4).
|
5
|
+
I copied PostgreSQL driver from ActiveRecord 4.0.0 and modified for Redshift.
|
6
|
+
"Barely works" patch was borrowed from fiksu/activerecord-redshift-adapter.
|
7
|
+
|
8
|
+
I know Redshift driver already exists (https://github.com/fiksu/activerecord-redshift-adapter),
|
9
|
+
but it currently supports only ActiveRecord 3. Also, AR4 code is magnifically
|
10
|
+
different from AR3 code because of file separation, patching does not work well.
|
11
|
+
I want to use Rails 4 with Redshift NOW, so I wrote this driver.
|
12
|
+
If anybody write better Redshift driver which works with ActiveRecord 4,
|
13
|
+
I abandon this driver.
|
14
|
+
|
15
|
+
Usage
|
16
|
+
-------------------
|
17
|
+
|
18
|
+
In Gemfile
|
19
|
+
```
|
20
|
+
gem 'activerecord4-redshift-adapter', github: 'aamine/activerecord4-redshift-adapter'
|
21
|
+
```
|
22
|
+
|
23
|
+
In database.yml
|
24
|
+
```
|
25
|
+
development:
|
26
|
+
adapter: redshift
|
27
|
+
host: your_cluster_name.hashhash.ap-northeast-1.redshift.amazonaws.com
|
28
|
+
port: 5439
|
29
|
+
database: dev
|
30
|
+
username: your_user
|
31
|
+
password: your_password
|
32
|
+
encoding: utf8
|
33
|
+
pool: 3
|
34
|
+
timeout: 5000
|
35
|
+
```
|
36
|
+
|
37
|
+
License
|
38
|
+
---------
|
39
|
+
|
40
|
+
MIT license (same as ActiveRecord)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class RedshiftColumn < Column
|
4
|
+
module ArrayParser
|
5
|
+
private
|
6
|
+
# Loads pg_array_parser if available. String parsing can be
|
7
|
+
# performed quicker by a native extension, which will not create
|
8
|
+
# a large amount of Ruby objects that will need to be garbage
|
9
|
+
# collected. pg_array_parser has a C and Java extension
|
10
|
+
begin
|
11
|
+
require 'pg_array_parser'
|
12
|
+
include PgArrayParser
|
13
|
+
rescue LoadError
|
14
|
+
def parse_pg_array(string)
|
15
|
+
parse_data(string, 0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_data(string, index)
|
20
|
+
local_index = index
|
21
|
+
array = []
|
22
|
+
while(local_index < string.length)
|
23
|
+
case string[local_index]
|
24
|
+
when '{'
|
25
|
+
local_index,array = parse_array_contents(array, string, local_index + 1)
|
26
|
+
when '}'
|
27
|
+
return array
|
28
|
+
end
|
29
|
+
local_index += 1
|
30
|
+
end
|
31
|
+
|
32
|
+
array
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_array_contents(array, string, index)
|
36
|
+
is_escaping = false
|
37
|
+
is_quoted = false
|
38
|
+
was_quoted = false
|
39
|
+
current_item = ''
|
40
|
+
|
41
|
+
local_index = index
|
42
|
+
while local_index
|
43
|
+
token = string[local_index]
|
44
|
+
if is_escaping
|
45
|
+
current_item << token
|
46
|
+
is_escaping = false
|
47
|
+
else
|
48
|
+
if is_quoted
|
49
|
+
case token
|
50
|
+
when '"'
|
51
|
+
is_quoted = false
|
52
|
+
was_quoted = true
|
53
|
+
when "\\"
|
54
|
+
is_escaping = true
|
55
|
+
else
|
56
|
+
current_item << token
|
57
|
+
end
|
58
|
+
else
|
59
|
+
case token
|
60
|
+
when "\\"
|
61
|
+
is_escaping = true
|
62
|
+
when ','
|
63
|
+
add_item_to_array(array, current_item, was_quoted)
|
64
|
+
current_item = ''
|
65
|
+
was_quoted = false
|
66
|
+
when '"'
|
67
|
+
is_quoted = true
|
68
|
+
when '{'
|
69
|
+
internal_items = []
|
70
|
+
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
|
71
|
+
array.push(internal_items)
|
72
|
+
when '}'
|
73
|
+
add_item_to_array(array, current_item, was_quoted)
|
74
|
+
return local_index,array
|
75
|
+
else
|
76
|
+
current_item << token
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
local_index += 1
|
82
|
+
end
|
83
|
+
return local_index,array
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_item_to_array(array, current_item, quoted)
|
87
|
+
if current_item.length == 0
|
88
|
+
elsif !quoted && current_item == 'NULL'
|
89
|
+
array.push nil
|
90
|
+
else
|
91
|
+
array.push current_item
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class RedshiftColumn < Column
|
4
|
+
module Cast
|
5
|
+
def point_to_string(point)
|
6
|
+
"(#{point[0]},#{point[1]})"
|
7
|
+
end
|
8
|
+
|
9
|
+
def string_to_point(string)
|
10
|
+
if string[0] == '(' && string[-1] == ')'
|
11
|
+
string = string[1...-1]
|
12
|
+
end
|
13
|
+
string.split(',').map{ |v| Float(v) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def string_to_time(string)
|
17
|
+
return string unless String === string
|
18
|
+
|
19
|
+
case string
|
20
|
+
when 'infinity'; 1.0 / 0.0
|
21
|
+
when '-infinity'; -1.0 / 0.0
|
22
|
+
when / BC$/
|
23
|
+
super("-" + string.sub(/ BC$/, ""))
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def string_to_bit(value)
|
30
|
+
case value
|
31
|
+
when /^0x/i
|
32
|
+
value[2..-1].hex.to_s(2) # Hexadecimal notation
|
33
|
+
else
|
34
|
+
value # Bit-string notation
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def hstore_to_string(object)
|
39
|
+
if Hash === object
|
40
|
+
object.map { |k,v|
|
41
|
+
"#{escape_hstore(k)}=>#{escape_hstore(v)}"
|
42
|
+
}.join ','
|
43
|
+
else
|
44
|
+
object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def string_to_hstore(string)
|
49
|
+
if string.nil?
|
50
|
+
nil
|
51
|
+
elsif String === string
|
52
|
+
Hash[string.scan(HstorePair).map { |k,v|
|
53
|
+
v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
|
54
|
+
k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
|
55
|
+
[k,v]
|
56
|
+
}]
|
57
|
+
else
|
58
|
+
string
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def json_to_string(object)
|
63
|
+
if Hash === object
|
64
|
+
ActiveSupport::JSON.encode(object)
|
65
|
+
else
|
66
|
+
object
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def array_to_string(value, column, adapter, should_be_quoted = false)
|
71
|
+
casted_values = value.map do |val|
|
72
|
+
if String === val
|
73
|
+
if val == "NULL"
|
74
|
+
"\"#{val}\""
|
75
|
+
else
|
76
|
+
quote_and_escape(adapter.type_cast(val, column, true))
|
77
|
+
end
|
78
|
+
else
|
79
|
+
adapter.type_cast(val, column, true)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
"{#{casted_values.join(',')}}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def range_to_string(object)
|
86
|
+
from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
|
87
|
+
to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
|
88
|
+
"[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def string_to_json(string)
|
92
|
+
if String === string
|
93
|
+
ActiveSupport::JSON.decode(string)
|
94
|
+
else
|
95
|
+
string
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def string_to_cidr(string)
|
100
|
+
if string.nil?
|
101
|
+
nil
|
102
|
+
elsif String === string
|
103
|
+
IPAddr.new(string)
|
104
|
+
else
|
105
|
+
string
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def cidr_to_string(object)
|
110
|
+
if IPAddr === object
|
111
|
+
"#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
|
112
|
+
else
|
113
|
+
object
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def string_to_array(string, oid)
|
118
|
+
parse_pg_array(string).map{|val| oid.type_cast val}
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
HstorePair = begin
|
124
|
+
quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
|
125
|
+
unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
|
126
|
+
/(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
|
127
|
+
end
|
128
|
+
|
129
|
+
def escape_hstore(value)
|
130
|
+
if value.nil?
|
131
|
+
'NULL'
|
132
|
+
else
|
133
|
+
if value == ""
|
134
|
+
'""'
|
135
|
+
else
|
136
|
+
'"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
|
142
|
+
|
143
|
+
def quote_and_escape(value)
|
144
|
+
case value
|
145
|
+
when "NULL"
|
146
|
+
value
|
147
|
+
else
|
148
|
+
value = value.gsub(/\\/, ARRAY_ESCAPE)
|
149
|
+
value.gsub!(/"/,"\\\"")
|
150
|
+
"\"#{value}\""
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class RedshiftAdapter < AbstractAdapter
|
4
|
+
module DatabaseStatements
|
5
|
+
def explain(arel, binds = [])
|
6
|
+
sql = "EXPLAIN #{to_sql(arel, binds)}"
|
7
|
+
ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
|
8
|
+
end
|
9
|
+
|
10
|
+
class ExplainPrettyPrinter # :nodoc:
|
11
|
+
# Pretty prints the result of a EXPLAIN in a way that resembles the output of the
|
12
|
+
# PostgreSQL shell:
|
13
|
+
#
|
14
|
+
# QUERY PLAN
|
15
|
+
# ------------------------------------------------------------------------------
|
16
|
+
# Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
|
17
|
+
# Join Filter: (posts.user_id = users.id)
|
18
|
+
# -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
|
19
|
+
# Index Cond: (id = 1)
|
20
|
+
# -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
|
21
|
+
# Filter: (posts.user_id = 1)
|
22
|
+
# (6 rows)
|
23
|
+
#
|
24
|
+
def pp(result)
|
25
|
+
header = result.columns.first
|
26
|
+
lines = result.rows.map(&:first)
|
27
|
+
|
28
|
+
# We add 2 because there's one char of padding at both sides, note
|
29
|
+
# the extra hyphens in the example above.
|
30
|
+
width = [header, *lines].map(&:length).max + 2
|
31
|
+
|
32
|
+
pp = []
|
33
|
+
|
34
|
+
pp << header.center(width).rstrip
|
35
|
+
pp << '-' * width
|
36
|
+
|
37
|
+
pp += lines.map {|line| " #{line}"}
|
38
|
+
|
39
|
+
nrows = result.rows.length
|
40
|
+
rows_label = nrows == 1 ? 'row' : 'rows'
|
41
|
+
pp << "(#{nrows} #{rows_label})"
|
42
|
+
|
43
|
+
pp.join("\n") + "\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Executes a SELECT query and returns an array of rows. Each row is an
|
48
|
+
# array of field values.
|
49
|
+
def select_rows(sql, name = nil, bind = [])
|
50
|
+
select_raw(sql, name).last
|
51
|
+
end
|
52
|
+
|
53
|
+
# Executes an INSERT query and returns the new record's ID
|
54
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
55
|
+
unless pk
|
56
|
+
# Extract the table from the insert sql. Yuck.
|
57
|
+
table_ref = extract_table_ref_from_insert_sql(sql)
|
58
|
+
pk = primary_key(table_ref) if table_ref
|
59
|
+
end
|
60
|
+
|
61
|
+
if pk && use_insert_returning?
|
62
|
+
select_value("#{sql} RETURNING #{quote_column_name(pk)}")
|
63
|
+
elsif pk
|
64
|
+
super
|
65
|
+
last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk))
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def create
|
72
|
+
super.insert
|
73
|
+
end
|
74
|
+
|
75
|
+
# create a 2D array representing the result set
|
76
|
+
def result_as_array(res) #:nodoc:
|
77
|
+
# check if we have any binary column and if they need escaping
|
78
|
+
ftypes = Array.new(res.nfields) do |i|
|
79
|
+
[i, res.ftype(i)]
|
80
|
+
end
|
81
|
+
|
82
|
+
rows = res.values
|
83
|
+
return rows unless ftypes.any? { |_, x|
|
84
|
+
x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
|
85
|
+
}
|
86
|
+
|
87
|
+
typehash = ftypes.group_by { |_, type| type }
|
88
|
+
binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
|
89
|
+
monies = typehash[MONEY_COLUMN_TYPE_OID] || []
|
90
|
+
|
91
|
+
rows.each do |row|
|
92
|
+
# unescape string passed BYTEA field (OID == 17)
|
93
|
+
binaries.each do |index, _|
|
94
|
+
row[index] = unescape_bytea(row[index])
|
95
|
+
end
|
96
|
+
|
97
|
+
# If this is a money type column and there are any currency symbols,
|
98
|
+
# then strip them off. Indeed it would be prettier to do this in
|
99
|
+
# PostgreSQLColumn.string_to_decimal but would break form input
|
100
|
+
# fields that call value_before_type_cast.
|
101
|
+
monies.each do |index, _|
|
102
|
+
data = row[index]
|
103
|
+
# Because money output is formatted according to the locale, there are two
|
104
|
+
# cases to consider (note the decimal separators):
|
105
|
+
# (1) $12,345,678.12
|
106
|
+
# (2) $12.345.678,12
|
107
|
+
case data
|
108
|
+
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
|
109
|
+
data.gsub!(/[^-\d.]/, '')
|
110
|
+
when /^-?\D+[\d.]+,\d{2}$/ # (2)
|
111
|
+
data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Queries the database and returns the results in an Array-like object
|
118
|
+
def query(sql, name = nil) #:nodoc:
|
119
|
+
log(sql, name) do
|
120
|
+
result_as_array @connection.async_exec(sql)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Executes an SQL statement, returning a PGresult object on success
|
125
|
+
# or raising a PGError exception otherwise.
|
126
|
+
def execute(sql, name = nil)
|
127
|
+
log(sql, name) do
|
128
|
+
@connection.async_exec(sql)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def substitute_at(column, index)
|
133
|
+
Arel::Nodes::BindParam.new "$#{index + 1}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
137
|
+
log(sql, name, binds) do
|
138
|
+
result = binds.empty? ? exec_no_cache(sql, binds) :
|
139
|
+
exec_cache(sql, binds)
|
140
|
+
|
141
|
+
types = {}
|
142
|
+
result.fields.each_with_index do |fname, i|
|
143
|
+
ftype = result.ftype i
|
144
|
+
fmod = result.fmod i
|
145
|
+
types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
|
146
|
+
warn "unknown OID: #{fname}(#{oid}) (#{sql})"
|
147
|
+
OID::Identity.new
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
ret = ActiveRecord::Result.new(result.fields, result.values, types)
|
152
|
+
result.clear
|
153
|
+
return ret
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def exec_delete(sql, name = 'SQL', binds = [])
|
158
|
+
log(sql, name, binds) do
|
159
|
+
result = binds.empty? ? exec_no_cache(sql, binds) :
|
160
|
+
exec_cache(sql, binds)
|
161
|
+
affected = result.cmd_tuples
|
162
|
+
result.clear
|
163
|
+
affected
|
164
|
+
end
|
165
|
+
end
|
166
|
+
alias :exec_update :exec_delete
|
167
|
+
|
168
|
+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
|
169
|
+
unless pk
|
170
|
+
# Extract the table from the insert sql. Yuck.
|
171
|
+
table_ref = extract_table_ref_from_insert_sql(sql)
|
172
|
+
pk = primary_key(table_ref) if table_ref
|
173
|
+
end
|
174
|
+
|
175
|
+
if pk && use_insert_returning?
|
176
|
+
sql = "#{sql} RETURNING #{quote_column_name(pk)}"
|
177
|
+
end
|
178
|
+
|
179
|
+
[sql, binds]
|
180
|
+
end
|
181
|
+
|
182
|
+
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
|
183
|
+
val = exec_query(sql, name, binds)
|
184
|
+
if !use_insert_returning? && pk
|
185
|
+
unless sequence_name
|
186
|
+
table_ref = extract_table_ref_from_insert_sql(sql)
|
187
|
+
sequence_name = default_sequence_name(table_ref, pk)
|
188
|
+
return val unless sequence_name
|
189
|
+
end
|
190
|
+
last_insert_id_result(sequence_name)
|
191
|
+
else
|
192
|
+
val
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Executes an UPDATE query and returns the number of affected tuples.
|
197
|
+
def update_sql(sql, name = nil)
|
198
|
+
super.cmd_tuples
|
199
|
+
end
|
200
|
+
|
201
|
+
# Begins a transaction.
|
202
|
+
def begin_db_transaction
|
203
|
+
execute "BEGIN"
|
204
|
+
end
|
205
|
+
|
206
|
+
def begin_isolated_db_transaction(isolation)
|
207
|
+
begin_db_transaction
|
208
|
+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Commits a transaction.
|
212
|
+
def commit_db_transaction
|
213
|
+
execute "COMMIT"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Aborts a transaction.
|
217
|
+
def rollback_db_transaction
|
218
|
+
execute "ROLLBACK"
|
219
|
+
end
|
220
|
+
|
221
|
+
def outside_transaction?
|
222
|
+
message = "#outside_transaction? is deprecated. This method was only really used " \
|
223
|
+
"internally, but you can use #transaction_open? instead."
|
224
|
+
ActiveSupport::Deprecation.warn message
|
225
|
+
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
226
|
+
end
|
227
|
+
|
228
|
+
def create_savepoint
|
229
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
230
|
+
end
|
231
|
+
|
232
|
+
def rollback_to_savepoint
|
233
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
234
|
+
end
|
235
|
+
|
236
|
+
def release_savepoint
|
237
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|