pg_funcall 0.1.0

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