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