activerecord4-redshift-adapter 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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