pg_funcall 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ pg_funcall (0.0.1)
5
+ activerecord (>= 3.1.0)
6
+ ipaddr_extensions
7
+ pg (>= 0.17.0)
8
+ uuid
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activemodel (4.2.0)
14
+ activesupport (= 4.2.0)
15
+ builder (~> 3.1)
16
+ activerecord (4.2.0)
17
+ activemodel (= 4.2.0)
18
+ activesupport (= 4.2.0)
19
+ arel (~> 6.0)
20
+ activesupport (4.2.0)
21
+ i18n (~> 0.7)
22
+ json (~> 1.7, >= 1.7.7)
23
+ minitest (~> 5.1)
24
+ thread_safe (~> 0.3, >= 0.3.4)
25
+ tzinfo (~> 1.1)
26
+ arel (6.0.0)
27
+ builder (3.2.2)
28
+ coderay (1.1.0)
29
+ diff-lcs (1.2.5)
30
+ docile (1.1.5)
31
+ i18n (0.7.0)
32
+ ipaddr_extensions (1.0.1)
33
+ json (1.8.2)
34
+ macaddr (1.7.1)
35
+ systemu (~> 2.6.2)
36
+ method_source (0.8.2)
37
+ minitest (5.5.1)
38
+ multi_json (1.10.1)
39
+ pg (0.18.1)
40
+ pry (0.10.1)
41
+ coderay (~> 1.1.0)
42
+ method_source (~> 0.8.1)
43
+ slop (~> 3.4)
44
+ rake (10.4.2)
45
+ rspec (2.14.1)
46
+ rspec-core (~> 2.14.0)
47
+ rspec-expectations (~> 2.14.0)
48
+ rspec-mocks (~> 2.14.0)
49
+ rspec-core (2.14.8)
50
+ rspec-expectations (2.14.5)
51
+ diff-lcs (>= 1.1.3, < 2.0)
52
+ rspec-mocks (2.14.6)
53
+ simplecov (0.9.1)
54
+ docile (~> 1.1.0)
55
+ multi_json (~> 1.0)
56
+ simplecov-html (~> 0.8.0)
57
+ simplecov-html (0.8.0)
58
+ slop (3.6.0)
59
+ systemu (2.6.4)
60
+ thread_safe (0.3.4)
61
+ tzinfo (1.2.2)
62
+ thread_safe (~> 0.1)
63
+ uuid (2.3.7)
64
+ macaddr (~> 1.0)
65
+ wwtd (0.7.0)
66
+
67
+ PLATFORMS
68
+ ruby
69
+
70
+ DEPENDENCIES
71
+ activerecord (~> 4.2.0)
72
+ bundler (~> 1.7)
73
+ pg_funcall!
74
+ pry
75
+ rake (~> 10.0)
76
+ rspec (~> 2.14.0)
77
+ simplecov
78
+ wwtd
data/lib/pg_funcall.rb ADDED
@@ -0,0 +1,389 @@
1
+ # used for some query forms, and for mapping types from AR -> Ruby
2
+ require 'active_record'
3
+
4
+ # supported types
5
+ require 'uuid'
6
+ require 'ipaddr'
7
+ require 'ipaddr_extensions'
8
+ require 'pg_funcall/ipaddr_monkeys'
9
+ require 'pg_funcall/type_map'
10
+
11
+ class PgFuncall
12
+ module HelperMethods
13
+ [:call_uncast, :call_raw, :call_scalar, :call_returning_array,
14
+ :clear_cache, :call_returning_type, :call_cast, :call, :casting_query].each do |meth|
15
+ define_method(meth) do |*args, &blk|
16
+ PgFuncall.default_instance.__send__(meth, *args, &blk)
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+ def self.default_instance
23
+ @default_instance ||= PgFuncall.new(ActiveRecord::Base.connection)
24
+ end
25
+
26
+ def self.default_instance=(instance)
27
+ @default_instance = instance
28
+ end
29
+
30
+ def initialize(connection)
31
+ raise ArgumentError, "Requires ActiveRecord PG connection" unless
32
+ connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
33
+
34
+ @ar_connection = connection
35
+
36
+ clear_cache
37
+ end
38
+
39
+ def clear_cache
40
+ (@ftype_cache ||= {}).clear
41
+ @type_map = nil
42
+ true
43
+ end
44
+
45
+ #
46
+ # Value wrappers
47
+ #
48
+
49
+ module PGTyped
50
+ end
51
+
52
+ module PGWritable
53
+ extend(PGTyped)
54
+ end
55
+
56
+ module PGReadable
57
+ extend(PGTyped)
58
+ end
59
+
60
+ Typed = Struct.new(:value, :type) do
61
+ include PGTyped
62
+ def __pg_type
63
+ type
64
+ end
65
+ end
66
+
67
+ class TypedArray < Typed
68
+ end
69
+
70
+ class PGTime < Typed
71
+ def initialize(time)
72
+ super(time, 'time')
73
+ end
74
+ end
75
+
76
+ class PGTimeInterval < Typed
77
+ def initialize(interval)
78
+ super(interval, 'interval')
79
+ end
80
+ end
81
+
82
+ class PGUUID < Typed
83
+ def initialize(uuid)
84
+ super(uuid, 'uuid')
85
+ end
86
+
87
+ def self.generate
88
+ PGUUID.initialize(UUID.new.generate)
89
+ end
90
+
91
+ def to_s
92
+ self.value
93
+ end
94
+ end
95
+
96
+ Literal = Struct.new(:value)
97
+
98
+ def self.tag_pg_type(value, tagtype, pgvalue = nil)
99
+ pgvalue ||= value
100
+
101
+ # XXX: this is going to blow the method cache every time it runs
102
+ value.class_eval do
103
+ include PGTyped
104
+
105
+ define_method(:__pg_value, lambda do
106
+ pgvalue
107
+ end)
108
+
109
+ define_method(:__pg_type, lambda do
110
+ tagtype
111
+ end)
112
+ end
113
+ value
114
+ end
115
+
116
+ #
117
+ # wrap a value so that it is inserted into the query as-is
118
+ #
119
+ def self.literal(arg)
120
+ Literal.new(arg)
121
+ end
122
+
123
+ #
124
+ # Calls Database function with a given set of arguments. Returns result as a string.
125
+ #
126
+ def call_uncast(fn, *args)
127
+ call_raw(fn, *args).rows.first.first
128
+ end
129
+ alias :call_scalar :call_uncast
130
+
131
+ def call_returning_array(fn, *args)
132
+ call_raw(fn, *args).rows
133
+ end
134
+
135
+ #
136
+ # "Quote", which means to format and quote, a parameter for inclusion into
137
+ # a SQL query as a string.
138
+ #
139
+ def _quote_param(param, type=nil)
140
+ return param.value if param.is_a?(Literal)
141
+
142
+ case param
143
+ when Array
144
+ "ARRAY[" + param.map {|p| _quote_param(p)}.join(",") + "]"
145
+ when Set
146
+ _quote_param(param.to_a)
147
+ when Hash
148
+ '$$' + param.map do |k,v|
149
+ "#{k} => #{v}"
150
+ end.join(',') + '$$::hstore'
151
+ else
152
+ ActiveRecord::Base.connection.quote(param)
153
+ end
154
+ end
155
+
156
+ #
157
+ # Represent a Ruby object in a string form to be passed as a parameter
158
+ # within a descriptor hash, rather than substituted into a string-form
159
+ # query.
160
+ #
161
+ def _format_param_for_descriptor(param, type=nil)
162
+ return param.value if param.is_a?(Literal)
163
+
164
+ case param
165
+ when TypedArray
166
+ _format_param_for_descriptor(param.value, param.type + "[]")
167
+ when Typed
168
+ _format_param_for_descriptor(param.value, param.type)
169
+ when PGTyped
170
+ param.respond_to?(:__pg_value) ?
171
+ param.__pg_value :
172
+ _format_param_for_descriptor(param, type)
173
+ when TrueClass
174
+ 'true'
175
+ when FalseClass
176
+ 'false'
177
+ when String
178
+ if type == 'bytea' || param.encoding == Encoding::BINARY
179
+ '\x' + param.unpack('C*').map {|x| sprintf("%02X", x)}.join("")
180
+ else
181
+ param
182
+ end
183
+ when Array
184
+ "{" + param.map {|p| _format_param_for_descriptor(p)}.join(",") + "}"
185
+ when IPAddr
186
+ param.to_cidr_string
187
+ when Range
188
+ last_char = param.exclude_end? ? ')' : ']'
189
+ case type
190
+ when 'tsrange', 'tstzrange'
191
+ "[#{param.first.utc},#{param.last.utc}#{last_char}"
192
+ else
193
+ "[#{param.first},#{param.last}#{last_char}"
194
+ end
195
+ when Set
196
+ _format_param_for_descriptor(param.to_a)
197
+ when Hash
198
+ param.map do |k,v|
199
+ "#{k} => #{v}"
200
+ end.join(',')
201
+ else
202
+ ActiveRecord::Base.connection.quote(param)
203
+ end
204
+ end
205
+
206
+ def call_raw_inline(fn, *args)
207
+ query = "SELECT #{fn}(" +
208
+ args.map {|arg| _quote_param(arg) }.join(", ") + ") as res;"
209
+
210
+ ActiveRecord::Base.connection.exec_query(query,
211
+ "calling for DB function #{fn}")
212
+ end
213
+
214
+ def call_raw_pg(fn, *args, &blk)
215
+ query = "SELECT #{fn}(" +
216
+ args.map {|arg| _quote_param(arg) }.join(", ") + ") as res;"
217
+
218
+ _pg_conn.query(query, &blk).tap do |res|
219
+ PgFuncall._assign_pg_type_map_to_res(res, _pg_conn)
220
+ end
221
+ end
222
+
223
+ alias :call_raw :call_raw_inline
224
+
225
+ def type_for_typeid(typeid)
226
+ type_map.resolve(typeid.to_i)
227
+ end
228
+
229
+ def type_for_name(name)
230
+ type_map.resolve(name)
231
+ end
232
+
233
+ def type_map
234
+ @type_map ||= TypeMap.fetch(@ar_connection, search_path: search_path)
235
+ end
236
+
237
+ #
238
+ # Force a typecast of the return value
239
+ #
240
+ def call_returning_type(fn, ret_type, *args)
241
+ type_map.type_cast_from_database(call(fn, *args),
242
+ type_for_name(ret_type))
243
+ end
244
+
245
+ def self._assign_pg_type_map_to_res(res, conn)
246
+ return res
247
+
248
+ ## this appears to fail to roundtrip on bytea and date types
249
+ ##
250
+ #if res.respond_to?(:type_map=)
251
+ # res.type_map = PG::BasicTypeMapForResults.new(conn)
252
+ #end
253
+ #res
254
+ end
255
+
256
+ #
257
+ # Take a PGResult and cast the first column of each tuple to the
258
+ # Ruby equivalent of the PG type as described in the PGResult.
259
+ #
260
+ def _cast_pgresult(res)
261
+ PgFuncall._assign_pg_type_map_to_res(res, _pg_conn)
262
+ res.column_values(0).map do |val|
263
+ type_map.type_cast_from_database(val,
264
+ type_for_typeid(res.ftype(0)))
265
+ end
266
+ end
267
+
268
+ def call_cast(fn, *args)
269
+ fn_sig = type_map.function_types(fn)
270
+
271
+ ## TODO: finish this with the new type info class
272
+ # unwrap = fn_sig && type_map.is_scalar_type?(type_map.lookup_by_oid(fn_sig.ret_type))
273
+
274
+ call_raw_pg(fn, *args) do |res|
275
+ results = _cast_pgresult(res)
276
+ # unwrap && results.ntuples < 2 ? results.first : results
277
+ results.first
278
+ end
279
+ end
280
+
281
+ def _pg_param_descriptors(params)
282
+ params.map do |p|
283
+ pgtype = _pgtype_for_value(p)
284
+ typeinfo = type_map.resolve(pgtype)
285
+ {
286
+ # value: typeinfo.cast_to_database(p),
287
+ value: _format_param_for_descriptor(p, pgtype),
288
+ # if we can't find a type, let PG guess
289
+ type: (typeinfo && typeinfo.oid) || 0,
290
+ format: 0
291
+ }
292
+ end
293
+ end
294
+
295
+ def casting_query(query, params)
296
+ # puts "param descriptors = #{_pg_param_descriptors(params)}.inspect"
297
+ _pg_conn.exec_params(query, _pg_param_descriptors(params)) do |res|
298
+ _cast_pgresult(res)
299
+ end
300
+ end
301
+
302
+
303
+ def _pgtype_for_value(value)
304
+ case value
305
+ # type-forcing wrapper for arrays
306
+ when TypedArray
307
+ value.type + '[]'
308
+
309
+ # type-forcing wrapper
310
+ when Typed
311
+ value.type
312
+
313
+ # marker ancestor
314
+ when PGTyped
315
+ value.__pg_type
316
+
317
+ when String
318
+ if value.encoding == Encoding::BINARY
319
+ 'bytea'
320
+ else
321
+ 'text'
322
+ end
323
+ when Fixnum, Bignum
324
+ 'int4'
325
+ when Float
326
+ 'float4'
327
+ when TrueClass, FalseClass
328
+ 'bool'
329
+ when BigDecimal
330
+ 'numeric'
331
+ when Hash
332
+ 'hstore'
333
+ when UUID
334
+ 'uuid'
335
+ when Time, DateTime
336
+ 'timestamp'
337
+ when Date
338
+ 'date'
339
+ when IPAddr
340
+ if value.host?
341
+ 'inet'
342
+ else
343
+ 'cidr'
344
+ end
345
+ when Range
346
+ case value.last
347
+ when Fixnum
348
+ if value.last > (2**31)-1
349
+ 'int8range'
350
+ else
351
+ 'int4range'
352
+ end
353
+ when Bignum then 'int8range'
354
+ when DateTime, Time then 'tsrange'
355
+ when Date then 'daterange'
356
+ when Float, BigDecimal, Numeric then 'numrange'
357
+ else
358
+ raise "Unknown range type: #{value.first.type}"
359
+ end
360
+ when Array, Set
361
+ first = value.flatten.first
362
+ raise "Empty untyped array" if first.nil?
363
+ _pgtype_for_value(first) + '[]'
364
+ else
365
+ 'text'
366
+ end
367
+ end
368
+
369
+ alias :call :call_cast
370
+
371
+ def _ar_conn
372
+ @ar_connection
373
+ end
374
+
375
+ def _pg_conn
376
+ _ar_conn.raw_connection
377
+ end
378
+
379
+ #
380
+ # Return an array of schema names for the current session's search path
381
+ #
382
+ def search_path
383
+ _pg_conn.query("SHOW search_path;") do |res|
384
+ res.column_values(0).first.split(/, ?/)
385
+ end
386
+ end
387
+
388
+ extend(HelperMethods)
389
+ end