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.
- checksums.yaml +15 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +78 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/config/database.yml +37 -0
- data/config/database.yml.example +17 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails40.gemfile.lock +79 -0
- data/gemfiles/rails41.gemfile +7 -0
- data/gemfiles/rails41.gemfile.lock +78 -0
- data/gemfiles/rails42.gemfile +7 -0
- data/gemfiles/rails42.gemfile.lock +78 -0
- data/lib/pg_funcall.rb +389 -0
- data/lib/pg_funcall/ipaddr_monkeys.rb +23 -0
- data/lib/pg_funcall/type_info.rb +149 -0
- data/lib/pg_funcall/type_map.rb +198 -0
- data/lib/pg_funcall/version.rb +3 -0
- data/pg_funcall.gemspec +34 -0
- data/script/shell +31 -0
- data/spec/lib/pg_funcall_spec.rb +371 -0
- data/spec/lib/type_info_spec.rb +147 -0
- data/spec/lib/type_map_spec.rb +110 -0
- data/spec/spec_helper.rb +20 -0
- metadata +201 -0
@@ -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
|