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 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