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.
@@ -0,0 +1,366 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class RedshiftAdapter < AbstractAdapter
6
+ module OID
7
+ class Type
8
+ def type; end
9
+
10
+ def type_cast_for_write(value)
11
+ value
12
+ end
13
+ end
14
+
15
+ class Identity < Type
16
+ def type_cast(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ class Bit < Type
22
+ def type_cast(value)
23
+ if String === value
24
+ ConnectionAdapters::RedshiftColumn.string_to_bit value
25
+ else
26
+ value
27
+ end
28
+ end
29
+ end
30
+
31
+ class Bytea < Type
32
+ def type_cast(value)
33
+ return if value.nil?
34
+ PGconn.unescape_bytea value
35
+ end
36
+ end
37
+
38
+ class Money < Type
39
+ def type_cast(value)
40
+ return if value.nil?
41
+
42
+ # Because money output is formatted according to the locale, there are two
43
+ # cases to consider (note the decimal separators):
44
+ # (1) $12,345,678.12
45
+ # (2) $12.345.678,12
46
+
47
+ case value
48
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
49
+ value.gsub!(/[^-\d.]/, '')
50
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
51
+ value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
52
+ end
53
+
54
+ ConnectionAdapters::Column.value_to_decimal value
55
+ end
56
+ end
57
+
58
+ class Vector < Type
59
+ attr_reader :delim, :subtype
60
+
61
+ # +delim+ corresponds to the `typdelim` column in the pg_types
62
+ # table. +subtype+ is derived from the `typelem` column in the
63
+ # pg_types table.
64
+ def initialize(delim, subtype)
65
+ @delim = delim
66
+ @subtype = subtype
67
+ end
68
+
69
+ # FIXME: this should probably split on +delim+ and use +subtype+
70
+ # to cast the values. Unfortunately, the current Rails behavior
71
+ # is to just return the string.
72
+ def type_cast(value)
73
+ value
74
+ end
75
+ end
76
+
77
+ class Point < Type
78
+ def type_cast(value)
79
+ if String === value
80
+ ConnectionAdapters::RedshiftColumn.string_to_point value
81
+ else
82
+ value
83
+ end
84
+ end
85
+ end
86
+
87
+ class Array < Type
88
+ attr_reader :subtype
89
+ def initialize(subtype)
90
+ @subtype = subtype
91
+ end
92
+
93
+ def type_cast(value)
94
+ if String === value
95
+ ConnectionAdapters::RedshiftColumn.string_to_array value, @subtype
96
+ else
97
+ value
98
+ end
99
+ end
100
+ end
101
+
102
+ class Range < Type
103
+ attr_reader :subtype
104
+ def initialize(subtype)
105
+ @subtype = subtype
106
+ end
107
+
108
+ def extract_bounds(value)
109
+ from, to = value[1..-2].split(',')
110
+ {
111
+ from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
112
+ to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
113
+ exclude_start: (value[0] == '('),
114
+ exclude_end: (value[-1] == ')')
115
+ }
116
+ end
117
+
118
+ def infinity(options = {})
119
+ ::Float::INFINITY * (options[:negative] ? -1 : 1)
120
+ end
121
+
122
+ def infinity?(value)
123
+ value.respond_to?(:infinite?) && value.infinite?
124
+ end
125
+
126
+ def to_integer(value)
127
+ infinity?(value) ? value : value.to_i
128
+ end
129
+
130
+ def type_cast(value)
131
+ return if value.nil? || value == 'empty'
132
+ return value if value.is_a?(::Range)
133
+
134
+ extracted = extract_bounds(value)
135
+
136
+ case @subtype
137
+ when :date
138
+ from = ConnectionAdapters::Column.value_to_date(extracted[:from])
139
+ from -= 1.day if extracted[:exclude_start]
140
+ to = ConnectionAdapters::Column.value_to_date(extracted[:to])
141
+ when :decimal
142
+ from = BigDecimal.new(extracted[:from].to_s)
143
+ # FIXME: add exclude start for ::Range, same for timestamp ranges
144
+ to = BigDecimal.new(extracted[:to].to_s)
145
+ when :time
146
+ from = ConnectionAdapters::Column.string_to_time(extracted[:from])
147
+ to = ConnectionAdapters::Column.string_to_time(extracted[:to])
148
+ when :integer
149
+ from = to_integer(extracted[:from]) rescue value ? 1 : 0
150
+ from -= 1 if extracted[:exclude_start]
151
+ to = to_integer(extracted[:to]) rescue value ? 1 : 0
152
+ else
153
+ return value
154
+ end
155
+
156
+ ::Range.new(from, to, extracted[:exclude_end])
157
+ end
158
+ end
159
+
160
+ class Integer < Type
161
+ def type_cast(value)
162
+ return if value.nil?
163
+
164
+ ConnectionAdapters::Column.value_to_integer value
165
+ end
166
+ end
167
+
168
+ class Boolean < Type
169
+ def type_cast(value)
170
+ return if value.nil?
171
+
172
+ ConnectionAdapters::Column.value_to_boolean value
173
+ end
174
+ end
175
+
176
+ class Timestamp < Type
177
+ def type; :timestamp; end
178
+
179
+ def type_cast(value)
180
+ return if value.nil?
181
+
182
+ # FIXME: probably we can improve this since we know it is PG
183
+ # specific
184
+ ConnectionAdapters::RedshiftColumn.string_to_time value
185
+ end
186
+ end
187
+
188
+ class Date < Type
189
+ def type; :datetime; end
190
+
191
+ def type_cast(value)
192
+ return if value.nil?
193
+
194
+ # FIXME: probably we can improve this since we know it is PG
195
+ # specific
196
+ ConnectionAdapters::Column.value_to_date value
197
+ end
198
+ end
199
+
200
+ class Time < Type
201
+ def type_cast(value)
202
+ return if value.nil?
203
+
204
+ # FIXME: probably we can improve this since we know it is PG
205
+ # specific
206
+ ConnectionAdapters::Column.string_to_dummy_time value
207
+ end
208
+ end
209
+
210
+ class Float < Type
211
+ def type_cast(value)
212
+ return if value.nil?
213
+
214
+ value.to_f
215
+ end
216
+ end
217
+
218
+ class Decimal < Type
219
+ def type_cast(value)
220
+ return if value.nil?
221
+
222
+ ConnectionAdapters::Column.value_to_decimal value
223
+ end
224
+ end
225
+
226
+ class Hstore < Type
227
+ def type_cast(value)
228
+ return if value.nil?
229
+
230
+ ConnectionAdapters::RedshiftColumn.string_to_hstore value
231
+ end
232
+ end
233
+
234
+ class Cidr < Type
235
+ def type_cast(value)
236
+ return if value.nil?
237
+
238
+ ConnectionAdapters::RedshiftColumn.string_to_cidr value
239
+ end
240
+ end
241
+
242
+ class Json < Type
243
+ def type_cast(value)
244
+ return if value.nil?
245
+
246
+ ConnectionAdapters::RedshiftColumn.string_to_json value
247
+ end
248
+ end
249
+
250
+ class TypeMap
251
+ def initialize
252
+ @mapping = {}
253
+ end
254
+
255
+ def []=(oid, type)
256
+ @mapping[oid] = type
257
+ end
258
+
259
+ def [](oid)
260
+ @mapping[oid]
261
+ end
262
+
263
+ def clear
264
+ @mapping.clear
265
+ end
266
+
267
+ def key?(oid)
268
+ @mapping.key? oid
269
+ end
270
+
271
+ def fetch(ftype, fmod)
272
+ # The type for the numeric depends on the width of the field,
273
+ # so we'll do something special here.
274
+ #
275
+ # When dealing with decimal columns:
276
+ #
277
+ # places after decimal = fmod - 4 & 0xffff
278
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
279
+ if ftype == 1700 && (fmod - 4 & 0xffff).zero?
280
+ ftype = 23
281
+ end
282
+
283
+ @mapping.fetch(ftype) { |oid| yield oid, fmod }
284
+ end
285
+ end
286
+
287
+ TYPE_MAP = TypeMap.new # :nodoc:
288
+
289
+ # When the PG adapter connects, the pg_type table is queried. The
290
+ # key of this hash maps to the `typname` column from the table.
291
+ # TYPE_MAP is then dynamically built with oids as the key and type
292
+ # objects as values.
293
+ NAMES = Hash.new { |h,k| # :nodoc:
294
+ h[k] = OID::Identity.new
295
+ }
296
+
297
+ # Register an OID type named +name+ with a typcasting object in
298
+ # +type+. +name+ should correspond to the `typname` column in
299
+ # the `pg_type` table.
300
+ def self.register_type(name, type)
301
+ NAMES[name] = type
302
+ end
303
+
304
+ # Alias the +old+ type to the +new+ type.
305
+ def self.alias_type(new, old)
306
+ NAMES[new] = NAMES[old]
307
+ end
308
+
309
+ # Is +name+ a registered type?
310
+ def self.registered_type?(name)
311
+ NAMES.key? name
312
+ end
313
+
314
+ register_type 'int2', OID::Integer.new
315
+ alias_type 'int4', 'int2'
316
+ alias_type 'int8', 'int2'
317
+ alias_type 'oid', 'int2'
318
+
319
+ register_type 'daterange', OID::Range.new(:date)
320
+ register_type 'numrange', OID::Range.new(:decimal)
321
+ register_type 'tsrange', OID::Range.new(:time)
322
+ register_type 'int4range', OID::Range.new(:integer)
323
+ alias_type 'tstzrange', 'tsrange'
324
+ alias_type 'int8range', 'int4range'
325
+
326
+ register_type 'numeric', OID::Decimal.new
327
+ register_type 'text', OID::Identity.new
328
+ alias_type 'varchar', 'text'
329
+ alias_type 'char', 'text'
330
+ alias_type 'bpchar', 'text'
331
+ alias_type 'xml', 'text'
332
+
333
+ # FIXME: why are we keeping these types as strings?
334
+ alias_type 'tsvector', 'text'
335
+ alias_type 'interval', 'text'
336
+ alias_type 'macaddr', 'text'
337
+ alias_type 'uuid', 'text'
338
+
339
+ register_type 'money', OID::Money.new
340
+ register_type 'bytea', OID::Bytea.new
341
+ register_type 'bool', OID::Boolean.new
342
+ register_type 'bit', OID::Bit.new
343
+ register_type 'varbit', OID::Bit.new
344
+
345
+ register_type 'float4', OID::Float.new
346
+ alias_type 'float8', 'float4'
347
+
348
+ register_type 'timestamp', OID::Timestamp.new
349
+ register_type 'timestamptz', OID::Timestamp.new
350
+ register_type 'date', OID::Date.new
351
+ register_type 'time', OID::Time.new
352
+
353
+ register_type 'path', OID::Identity.new
354
+ register_type 'point', OID::Point.new
355
+ register_type 'polygon', OID::Identity.new
356
+ register_type 'circle', OID::Identity.new
357
+ register_type 'hstore', OID::Hstore.new
358
+ register_type 'json', OID::Json.new
359
+ register_type 'ltree', OID::Identity.new
360
+
361
+ register_type 'cidr', OID::Cidr.new
362
+ alias_type 'inet', 'cidr'
363
+ end
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,172 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RedshiftAdapter < AbstractAdapter
4
+ module Quoting
5
+ # Escapes binary strings for bytea input to the database.
6
+ def escape_bytea(value)
7
+ PGconn.escape_bytea(value) if value
8
+ end
9
+
10
+ # Unescapes bytea output from a database to the binary string it represents.
11
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
12
+ # on escaped binary output from database drive.
13
+ def unescape_bytea(value)
14
+ PGconn.unescape_bytea(value) if value
15
+ end
16
+
17
+ # Quotes PostgreSQL-specific data types for SQL input.
18
+ def quote(value, column = nil) #:nodoc:
19
+ return super unless column
20
+
21
+ sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale)
22
+
23
+ case value
24
+ when Range
25
+ if /range$/ =~ sql_type
26
+ escaped = quote_string(RedshiftColumn.range_to_string(value))
27
+ "'#{escaped}'::#{sql_type}"
28
+ else
29
+ super
30
+ end
31
+ when Array
32
+ case sql_type
33
+ when 'point' then super(RedshiftColumn.point_to_string(value))
34
+ else
35
+ if column.array
36
+ "'#{RedshiftColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
37
+ else
38
+ super
39
+ end
40
+ end
41
+ when Hash
42
+ case sql_type
43
+ when 'hstore' then super(RedshiftColumn.hstore_to_string(value), column)
44
+ when 'json' then super(RedshiftColumn.json_to_string(value), column)
45
+ else super
46
+ end
47
+ when IPAddr
48
+ case sql_type
49
+ when 'inet', 'cidr' then super(RedshiftColumn.cidr_to_string(value), column)
50
+ else super
51
+ end
52
+ when Float
53
+ if value.infinite? && column.type == :datetime
54
+ "'#{value.to_s.downcase}'"
55
+ elsif value.infinite? || value.nan?
56
+ "'#{value.to_s}'"
57
+ else
58
+ super
59
+ end
60
+ when Numeric
61
+ if sql_type == 'money' || [:string, :text].include?(column.type)
62
+ # Not truly string input, so doesn't require (or allow) escape string syntax.
63
+ "'#{value}'"
64
+ else
65
+ super
66
+ end
67
+ when String
68
+ case sql_type
69
+ when 'bytea' then "'#{escape_bytea(value)}'"
70
+ when 'xml' then "xml '#{quote_string(value)}'"
71
+ when /^bit/
72
+ case value
73
+ when /\A[01]*\Z/ then "B'#{value}'" # Bit-string notation
74
+ when /\A[0-9A-F]*\Z/i then "X'#{value}'" # Hexadecimal notation
75
+ end
76
+ else
77
+ super
78
+ end
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ def type_cast(value, column, array_member = false)
85
+ return super(value, column) unless column
86
+
87
+ case value
88
+ when Range
89
+ return super(value, column) unless /range$/ =~ column.sql_type
90
+ RedshiftColumn.range_to_string(value)
91
+ when NilClass
92
+ if column.array && array_member
93
+ 'NULL'
94
+ elsif column.array
95
+ value
96
+ else
97
+ super(value, column)
98
+ end
99
+ when Array
100
+ case column.sql_type
101
+ when 'point' then RedshiftColumn.point_to_string(value)
102
+ else
103
+ return super(value, column) unless column.array
104
+ RedshiftColumn.array_to_string(value, column, self)
105
+ end
106
+ when String
107
+ return super(value, column) unless 'bytea' == column.sql_type
108
+ { :value => value, :format => 1 }
109
+ when Hash
110
+ case column.sql_type
111
+ when 'hstore' then RedshiftColumn.hstore_to_string(value)
112
+ when 'json' then RedshiftColumn.json_to_string(value)
113
+ else super(value, column)
114
+ end
115
+ when IPAddr
116
+ return super(value, column) unless ['inet','cidr'].include? column.sql_type
117
+ RedshiftColumn.cidr_to_string(value)
118
+ else
119
+ super(value, column)
120
+ end
121
+ end
122
+
123
+ # Quotes strings for use in SQL input.
124
+ def quote_string(s) #:nodoc:
125
+ @connection.escape(s)
126
+ end
127
+
128
+ # Checks the following cases:
129
+ #
130
+ # - table_name
131
+ # - "table.name"
132
+ # - schema_name.table_name
133
+ # - schema_name."table.name"
134
+ # - "schema.name".table_name
135
+ # - "schema.name"."table.name"
136
+ def quote_table_name(name)
137
+ schema, name_part = extract_pg_identifier_from_name(name.to_s)
138
+
139
+ unless name_part
140
+ quote_column_name(schema)
141
+ else
142
+ table_name, name_part = extract_pg_identifier_from_name(name_part)
143
+ "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
144
+ end
145
+ end
146
+
147
+ def quote_table_name_for_assignment(table, attr)
148
+ quote_column_name(attr)
149
+ end
150
+
151
+ # Quotes column names for use in SQL queries.
152
+ def quote_column_name(name) #:nodoc:
153
+ PGconn.quote_ident(name.to_s)
154
+ end
155
+
156
+ # Quote date/time values for use in SQL input. Includes microseconds
157
+ # if the value is a Time responding to usec.
158
+ def quoted_date(value) #:nodoc:
159
+ result = super
160
+ if value.acts_like?(:time) && value.respond_to?(:usec)
161
+ result = "#{result}.#{sprintf("%06d", value.usec)}"
162
+ end
163
+
164
+ if value.year < 0
165
+ result = result.sub(/^-/, "") + " BC"
166
+ end
167
+ result
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end