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,23 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'ipaddr_extensions'
|
3
|
+
|
4
|
+
class IPAddr
|
5
|
+
def prefixlen
|
6
|
+
mask = @mask_addr
|
7
|
+
len = 0
|
8
|
+
len += mask & 1 and mask >>= 1 until mask == 0
|
9
|
+
len
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_cidr_string
|
13
|
+
"#{to_s}/#{prefixlen}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json(options = {})
|
17
|
+
if (ipv6? && prefixlen == 64) || (ipv4? && prefixlen == 32)
|
18
|
+
to_s
|
19
|
+
else
|
20
|
+
to_cidr_string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
class PgFuncall::TypeInfo
|
2
|
+
attr_accessor :ar_type
|
3
|
+
attr_accessor :array_type
|
4
|
+
attr_accessor :element_type
|
5
|
+
|
6
|
+
def initialize(row, ar_type = nil)
|
7
|
+
@row = {}
|
8
|
+
# copy and convert int-looking things to int along the way
|
9
|
+
row.each do |key, val|
|
10
|
+
@row[key] =
|
11
|
+
(val && val.respond_to?(:match) && val.match(/^-?\d+$/)) ? val.to_i : val
|
12
|
+
end
|
13
|
+
@row.freeze
|
14
|
+
@ar_type = ar_type
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: replace this to not use ar_type
|
18
|
+
def cast_from_database(value)
|
19
|
+
@ar_type.respond_to?(:type_cast_from_database) ?
|
20
|
+
@ar_type.type_cast_from_database(value) :
|
21
|
+
@ar_type.type_cast(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Represent a Ruby object in a string form to be passed as a parameter
|
26
|
+
# within a descriptor hash, rather than substituted into a string-form
|
27
|
+
# query.
|
28
|
+
#
|
29
|
+
def _format_param_for_descriptor(param, type=nil)
|
30
|
+
return param.value if param.is_a?(PgFuncall::Literal)
|
31
|
+
|
32
|
+
case param
|
33
|
+
when PgFuncall::TypedArray
|
34
|
+
_format_param_for_descriptor(param.value, param.type + "[]")
|
35
|
+
when PgFuncall::Typed
|
36
|
+
_format_param_for_descriptor(param.value, param.type)
|
37
|
+
when PgFuncall::PGTyped
|
38
|
+
param.respond_to?(:__pg_value) ?
|
39
|
+
param.__pg_value :
|
40
|
+
_format_param_for_descriptor(param, type)
|
41
|
+
when TrueClass
|
42
|
+
'true'
|
43
|
+
when FalseClass
|
44
|
+
'false'
|
45
|
+
when String
|
46
|
+
if type == 'bytea' || param.encoding == Encoding::BINARY
|
47
|
+
'\x' + param.unpack('C*').map {|x| sprintf("%02X", x)}.join("")
|
48
|
+
else
|
49
|
+
param
|
50
|
+
end
|
51
|
+
when Array
|
52
|
+
"{" + param.map {|p| _format_param_for_descriptor(p)}.join(",") + "}"
|
53
|
+
when IPAddr
|
54
|
+
param.to_cidr_string
|
55
|
+
when Range
|
56
|
+
last_char = param.exclude_end? ? ')' : ']'
|
57
|
+
case type
|
58
|
+
when 'tsrange', 'tstzrange'
|
59
|
+
"[#{param.first.utc},#{param.last.utc}#{last_char}"
|
60
|
+
else
|
61
|
+
"[#{param.first},#{param.last}#{last_char}"
|
62
|
+
end
|
63
|
+
when Set
|
64
|
+
_format_param_for_descriptor(param.to_a)
|
65
|
+
when Hash
|
66
|
+
param.map do |k,v|
|
67
|
+
"#{k} => #{v}"
|
68
|
+
end.join(',')
|
69
|
+
else
|
70
|
+
ActiveRecord::Base.connection.quote(param)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: replace this to not use ar_type
|
75
|
+
def cast_to_database(value)
|
76
|
+
@ar_type.respond_to?(:type_cast_for_database) ?
|
77
|
+
@ar_type.type_cast_for_database(value).to_s :
|
78
|
+
_format_param_for_descriptor(value, name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def name
|
82
|
+
@row['typname']
|
83
|
+
end
|
84
|
+
|
85
|
+
def namespace
|
86
|
+
@row['nspname']
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Don't fully qualify base types -- this is pretty, but is it wise?
|
91
|
+
#
|
92
|
+
def fqname
|
93
|
+
namespace == 'pg_catalog' ?
|
94
|
+
name :
|
95
|
+
namespace + '.' + name
|
96
|
+
end
|
97
|
+
|
98
|
+
def oid
|
99
|
+
@row['oid']
|
100
|
+
end
|
101
|
+
|
102
|
+
def category
|
103
|
+
@row['typcategory']
|
104
|
+
end
|
105
|
+
|
106
|
+
def temporal?
|
107
|
+
datetime? || timespan?
|
108
|
+
end
|
109
|
+
|
110
|
+
CATEGORY_MAP =
|
111
|
+
{'A' => 'array',
|
112
|
+
'B' => 'boolean',
|
113
|
+
'C' => 'composite',
|
114
|
+
'D' => 'datetime',
|
115
|
+
'E' => 'enum',
|
116
|
+
'G' => 'geometric',
|
117
|
+
'I' => 'network_address',
|
118
|
+
'N' => 'numeric',
|
119
|
+
'P' => 'pseudotype',
|
120
|
+
'S' => 'string',
|
121
|
+
'T' => 'timespan',
|
122
|
+
'U' => 'user_defined',
|
123
|
+
'V' => 'bit_string',
|
124
|
+
'X' => 'unknown'
|
125
|
+
}
|
126
|
+
|
127
|
+
CATEGORY_MAP.each do |typ, name|
|
128
|
+
define_method("#{name}?") do
|
129
|
+
category == typ
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def category_name
|
134
|
+
CATEGORY_MAP[category]
|
135
|
+
end
|
136
|
+
|
137
|
+
def element_type_oid
|
138
|
+
raise "Can only call on array" unless array?
|
139
|
+
@row['typelem']
|
140
|
+
end
|
141
|
+
|
142
|
+
def array_type_oid
|
143
|
+
@row['typarray']
|
144
|
+
end
|
145
|
+
|
146
|
+
def [](element)
|
147
|
+
@row[element.to_s]
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'pg_funcall/type_info'
|
3
|
+
|
4
|
+
class PgFuncall
|
5
|
+
class FunctionSig
|
6
|
+
FTYPE_CACHE = {}
|
7
|
+
|
8
|
+
attr_reader :name, :ret_type, :arg_sigs
|
9
|
+
|
10
|
+
def initialize(name, ret_type, arg_sigs)
|
11
|
+
@name = name.freeze
|
12
|
+
@ret_type = ret_type
|
13
|
+
@arg_sigs = arg_sigs.sort.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
other.name == @name
|
18
|
+
other.ret_type == @ret_type
|
19
|
+
other.arg_sigs == @arg_sigs
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class PgType
|
24
|
+
def initialize(pginfo, ar_type)
|
25
|
+
@pginfo = pginfo
|
26
|
+
@ar_type = ar_type
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
array ? "#{name}[]" : name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# See http://www.postgresql.org/docs/9.4/static/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE
|
36
|
+
#
|
37
|
+
class TypeMap
|
38
|
+
def self.fetch(connection, options = {})
|
39
|
+
case ActiveRecord.version.segments[0..1]
|
40
|
+
when [4,0] then AR40TypeMap.new(connection, options)
|
41
|
+
when [4,1] then AR41TypeMap.new(connection, options)
|
42
|
+
when [4,2] then AR42TypeMap.new(connection, options)
|
43
|
+
else
|
44
|
+
raise ArgumentError, "Unsupported ActiveRecord version #{ActiveRecord.version}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def initialize(connection, options = {})
|
50
|
+
@ftype_cache = {}
|
51
|
+
@ar_connection = connection
|
52
|
+
@options = options
|
53
|
+
@typeinfo = []
|
54
|
+
@typeinfo_by_name = {}
|
55
|
+
@typeinfo_by_oid = {}
|
56
|
+
|
57
|
+
load_types
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_types
|
61
|
+
res = pg_connection.query <<-SQL
|
62
|
+
SELECT pgt.oid, ns.nspname, *
|
63
|
+
FROM pg_type as pgt
|
64
|
+
JOIN pg_namespace as ns on pgt.typnamespace = ns.oid;
|
65
|
+
SQL
|
66
|
+
|
67
|
+
PgFuncall._assign_pg_type_map_to_res(res, pg_connection)
|
68
|
+
|
69
|
+
fields = res.fields
|
70
|
+
@typeinfo = res.values.map do |values|
|
71
|
+
row = Hash[fields.zip(values)]
|
72
|
+
TypeInfo.new(row, lookup_ar_by_oid(row['oid'].to_i))
|
73
|
+
end
|
74
|
+
|
75
|
+
@typeinfo_by_name.clear
|
76
|
+
@typeinfo_by_oid.clear
|
77
|
+
|
78
|
+
@typeinfo.each do |ti|
|
79
|
+
@typeinfo_by_name[ti.name] = ti
|
80
|
+
@typeinfo_by_oid[ti.oid] = ti
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def ar_connection
|
85
|
+
@ar_connection
|
86
|
+
end
|
87
|
+
|
88
|
+
def pg_connection
|
89
|
+
@ar_connection.raw_connection
|
90
|
+
end
|
91
|
+
|
92
|
+
def type_cast_from_database(value, type)
|
93
|
+
type.cast_from_database(value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def _canonicalize_type_name(name)
|
97
|
+
if name.end_with?('[]')
|
98
|
+
name = '_' + name.gsub(/(\[\])+$/, '')
|
99
|
+
end
|
100
|
+
name
|
101
|
+
end
|
102
|
+
|
103
|
+
def resolve(oid_or_name)
|
104
|
+
if oid_or_name.is_a?(Integer) || (oid_or_name.is_a?(String) && oid_or_name.match(/^[0-9]+$/))
|
105
|
+
@typeinfo_by_oid[oid_or_name.to_i]
|
106
|
+
elsif oid_or_name.is_a?(String) || oid_or_name.is_a?(Symbol)
|
107
|
+
@typeinfo_by_name[_canonicalize_type_name(oid_or_name.to_s)]
|
108
|
+
else
|
109
|
+
raise ArgumentError, "You must supply a numeric OID or a string Type name"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Given a type name, with optional appended [] or prefixed _ for array types,
|
115
|
+
# return the OID for it.
|
116
|
+
#
|
117
|
+
# If array = true, find array type for given base type.
|
118
|
+
#
|
119
|
+
def oid_for_type(type, array = false)
|
120
|
+
type = _canonicalize_type_name(type)
|
121
|
+
type = '_' + type if array && !type.start_with?('_')
|
122
|
+
@typeinfo_by_name[type]
|
123
|
+
end
|
124
|
+
|
125
|
+
FMETAQUERY = <<-"SQL"
|
126
|
+
SELECT prorettype, proargtypes
|
127
|
+
FROM pg_proc as pgp
|
128
|
+
JOIN pg_namespace as ns on pgp.pronamespace = ns.oid
|
129
|
+
WHERE proname = '%s' AND ns.nspname = '%s';
|
130
|
+
SQL
|
131
|
+
|
132
|
+
#
|
133
|
+
# Query PostgreSQL metadata about function to find its
|
134
|
+
# return type and argument types
|
135
|
+
#
|
136
|
+
def function_types(fn, search_path = @options[:search_path])
|
137
|
+
return @ftype_cache[fn] if @ftype_cache[fn]
|
138
|
+
|
139
|
+
parts = fn.split('.')
|
140
|
+
info = if parts.length == 1
|
141
|
+
raise ArgumentError, "Must supply search_path for non-namespaced function" unless
|
142
|
+
search_path && search_path.is_a?(Enumerable) && !search_path.empty?
|
143
|
+
search_path.map do |ns|
|
144
|
+
res = pg_connection.query(FMETAQUERY % [parts[0], ns])
|
145
|
+
PgFuncall._assign_pg_type_map_to_res(res, pg_connection)
|
146
|
+
res.ntuples == 1 ? res : nil
|
147
|
+
end.compact.first
|
148
|
+
else
|
149
|
+
PgFuncall._assign_pg_type_map_to_res(pg_connection.query(FMETAQUERY % [parts[1], parts[0]]),
|
150
|
+
pg_connection)
|
151
|
+
end
|
152
|
+
|
153
|
+
return nil unless info && info.ntuples >= 1
|
154
|
+
|
155
|
+
@ftype_cache[fn] =
|
156
|
+
FunctionSig.new(fn,
|
157
|
+
info.getvalue(0,0).to_i,
|
158
|
+
(0..info.ntuples-1).map { |row|
|
159
|
+
info.getvalue(row, 1).split(/ +/).map(&:to_i)
|
160
|
+
})
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class AR40TypeMap < TypeMap
|
165
|
+
def lookup_ar_by_oid(oid)
|
166
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::TYPE_MAP[oid]
|
167
|
+
end
|
168
|
+
|
169
|
+
def lookup_ar_by_name(name)
|
170
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::NAMES[name]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class AR41TypeMap < TypeMap
|
175
|
+
def lookup_ar_by_name(name)
|
176
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::NAMES[name]
|
177
|
+
end
|
178
|
+
|
179
|
+
def lookup_ar_by_oid(oid)
|
180
|
+
@ar_connection.instance_variable_get("@type_map")[oid]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class AR42TypeMap < TypeMap
|
185
|
+
def lookup_ar_by_name(name)
|
186
|
+
@ar_connection.instance_variable_get("@type_map").lookup(name)
|
187
|
+
end
|
188
|
+
|
189
|
+
def lookup_ar_by_oid(oid)
|
190
|
+
@ar_connection.instance_variable_get("@type_map").lookup(oid)
|
191
|
+
end
|
192
|
+
|
193
|
+
def type_cast_from_database(value, type)
|
194
|
+
type.ar_type.type_cast_from_database(value)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
data/pg_funcall.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pg_funcall/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pg_funcall"
|
8
|
+
spec.version = PgFuncall::VERSION
|
9
|
+
spec.authors = ["Robert Sanders"]
|
10
|
+
spec.email = ["robert@curioussquid.com"]
|
11
|
+
spec.summary = %q{Utility class for calling functions defined in a PostgreSQL database.}
|
12
|
+
# spec.description = %q{.}
|
13
|
+
spec.homepage = "http://github.com/rsanders/pg_funcall"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "pg", ">= 0.17.0"
|
22
|
+
spec.add_dependency "activerecord", ">= 4.0.0"
|
23
|
+
|
24
|
+
# support for various PG types
|
25
|
+
spec.add_dependency "uuid"
|
26
|
+
spec.add_dependency "ipaddr_extensions"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 2.14.0"
|
31
|
+
spec.add_development_dependency "simplecov"
|
32
|
+
spec.add_development_dependency "wwtd"
|
33
|
+
|
34
|
+
end
|
data/script/shell
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'yaml'
|
6
|
+
require 'active_record'
|
7
|
+
configs = YAML.load(File.read("config/database.yml.example"))
|
8
|
+
ActiveRecord::Base.establish_connection(configs['development'])
|
9
|
+
|
10
|
+
require 'pg_funcall'
|
11
|
+
|
12
|
+
def conn
|
13
|
+
ActiveRecord::Base.connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def pgconn
|
17
|
+
conn.raw_connection
|
18
|
+
end
|
19
|
+
|
20
|
+
def oidclass
|
21
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
require 'pry'
|
26
|
+
Pry.start
|
27
|
+
rescue LoadError
|
28
|
+
require 'irb'
|
29
|
+
IRB.start
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,371 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Test the utility class for calling database functions
|
5
|
+
#
|
6
|
+
|
7
|
+
describe PgFuncall do
|
8
|
+
before(:all) do
|
9
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
10
|
+
CREATE EXTENSION IF NOT EXISTS hstore;
|
11
|
+
|
12
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_polyfunc(arg1 anyelement)
|
13
|
+
RETURNS anyelement
|
14
|
+
LANGUAGE plpgsql
|
15
|
+
AS $function$
|
16
|
+
BEGIN
|
17
|
+
RETURN arg1;
|
18
|
+
END;
|
19
|
+
$function$;
|
20
|
+
|
21
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_textfunc(arg1 text, arg2 text)
|
22
|
+
RETURNS text
|
23
|
+
LANGUAGE plpgsql
|
24
|
+
AS $function$
|
25
|
+
BEGIN
|
26
|
+
RETURN arg1 || ',' || arg2;
|
27
|
+
END;
|
28
|
+
$function$;
|
29
|
+
|
30
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_intfunc(arg1 integer, arg2 integer)
|
31
|
+
RETURNS integer
|
32
|
+
LANGUAGE plpgsql
|
33
|
+
AS $function$
|
34
|
+
BEGIN
|
35
|
+
RETURN arg1 + arg2;
|
36
|
+
END;
|
37
|
+
$function$;
|
38
|
+
|
39
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_arrayfunc(arg integer[])
|
40
|
+
RETURNS integer[]
|
41
|
+
LANGUAGE plpgsql
|
42
|
+
AS $function$
|
43
|
+
DECLARE
|
44
|
+
result integer[];
|
45
|
+
val integer;
|
46
|
+
BEGIN
|
47
|
+
result := '{}';
|
48
|
+
FOREACH val IN ARRAY arg LOOP
|
49
|
+
result := array_append(result, val * 2);
|
50
|
+
END LOOP;
|
51
|
+
RETURN result;
|
52
|
+
END;
|
53
|
+
$function$;
|
54
|
+
|
55
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_hstorefunc(arg hstore, cay text)
|
56
|
+
RETURNS text
|
57
|
+
LANGUAGE plpgsql
|
58
|
+
AS $function$
|
59
|
+
BEGIN
|
60
|
+
RETURN arg -> cay;
|
61
|
+
END;
|
62
|
+
$function$;
|
63
|
+
|
64
|
+
CREATE OR REPLACE FUNCTION public.dbfspec_hstorefunc(arg1 hstore, arg2 hstore)
|
65
|
+
RETURNS hstore
|
66
|
+
LANGUAGE plpgsql
|
67
|
+
AS $function$
|
68
|
+
BEGIN
|
69
|
+
RETURN arg1 || arg2;
|
70
|
+
END;
|
71
|
+
$function$;
|
72
|
+
SQL
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
after(:all) do
|
77
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
78
|
+
DROP FUNCTION IF EXISTS public.dbfspec_polyfunc(anyelement);
|
79
|
+
DROP FUNCTION IF EXISTS public.dbfspec_textfunc(text, text);
|
80
|
+
DROP FUNCTION IF EXISTS public.dbfspec_intfunc(integer, integer);
|
81
|
+
DROP FUNCTION IF EXISTS public.dbfspec_arrayfunc(integer[]);
|
82
|
+
DROP FUNCTION IF EXISTS public.dbfspec_hstorefunc(hstore, text);
|
83
|
+
|
84
|
+
SQL
|
85
|
+
end
|
86
|
+
|
87
|
+
let :search_path do
|
88
|
+
ActiveRecord::Base.connection.schema_search_path.split(/, ?/)
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'introspection' do
|
92
|
+
subject { PgFuncall.default_instance }
|
93
|
+
|
94
|
+
context '#search_path' do
|
95
|
+
it 'should return the expected search path' do
|
96
|
+
subject.search_path.should == search_path
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'quoting for inlining into string' do
|
102
|
+
subject { PgFuncall.default_instance }
|
103
|
+
it 'does not quote integer' do
|
104
|
+
subject._quote_param(50).should == "50"
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'single-quotes a string' do
|
108
|
+
subject._quote_param("foo").should == "'foo'"
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'handles single quotes embedded in string' do
|
112
|
+
subject._quote_param("ain't misbehavin'").should ==
|
113
|
+
"'ain''t misbehavin'''"
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'quotes string array properly' do
|
117
|
+
subject._quote_param(%w[a b cdef]).should ==
|
118
|
+
"ARRAY['a','b','cdef']"
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'quotes integer array properly' do
|
122
|
+
subject._quote_param([99, 100]).should ==
|
123
|
+
"ARRAY[99,100]"
|
124
|
+
end
|
125
|
+
|
126
|
+
# XXX: this can be iffy unless there's a clear typecast or
|
127
|
+
# unambiguous function parameter type; arrays must be typed
|
128
|
+
# so it's best to specify the type of empty arrays
|
129
|
+
it 'quotes empty array' do
|
130
|
+
subject._quote_param([]).should ==
|
131
|
+
"ARRAY[]"
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'quotes Ruby hash as hstore' do
|
135
|
+
subject._quote_param({a: 1, b: :foo}).should ==
|
136
|
+
"$$a => 1,b => foo$$::hstore"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'quoting for inclusing in Pg param descriptor' do
|
141
|
+
subject { PgFuncall.default_instance }
|
142
|
+
|
143
|
+
it 'does not quote integer' do
|
144
|
+
subject._format_param_for_descriptor(50).should == "50"
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'single-quotes a string' do
|
148
|
+
subject._format_param_for_descriptor("foo").should == "foo"
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'handles single quotes embedded in string' do
|
152
|
+
subject._format_param_for_descriptor("ain't misbehavin'").should ==
|
153
|
+
"ain't misbehavin'"
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'quotes string array properly' do
|
157
|
+
subject._format_param_for_descriptor(%w[a b cdef]).should ==
|
158
|
+
"{a,b,cdef}"
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'quotes integer array properly' do
|
162
|
+
subject._format_param_for_descriptor([99, 100]).should ==
|
163
|
+
"{99,100}"
|
164
|
+
end
|
165
|
+
|
166
|
+
# XXX: this can be iffy unless there's a clear typecast or
|
167
|
+
# unambiguous function parameter type; arrays must be typed
|
168
|
+
# so it's best to specify the type of empty arrays
|
169
|
+
it 'quotes empty array' do
|
170
|
+
subject._format_param_for_descriptor([]).should ==
|
171
|
+
"{}"
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'quotes Ruby hash as hstore' do
|
175
|
+
subject._format_param_for_descriptor({a: 1, b: :foo}).should ==
|
176
|
+
"a => 1,b => foo"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'simple call with string return' do
|
181
|
+
it 'should return a string as a string' do
|
182
|
+
PgFuncall.call_uncast('public.dbfspec_textfunc', "hello", "goodbye").should == 'hello,goodbye'
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'should return a polymorphic-cast string as a string' do
|
186
|
+
PgFuncall.call_uncast('public.dbfspec_polyfunc',
|
187
|
+
PgFuncall.literal("'hello'::text")).should == 'hello'
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'should return a number as a string' do
|
191
|
+
PgFuncall.call_uncast('public.dbfspec_intfunc', 55, 100).should == "155"
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should return an array as a string' do
|
195
|
+
PgFuncall.call_uncast('public.dbfspec_arrayfunc',
|
196
|
+
PgFuncall.literal('ARRAY[55, 100]::integer[]')).should == '{110,200}'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'call with typecast return' do
|
201
|
+
it 'should return a polymorphic-cast string as a string' do
|
202
|
+
PgFuncall.call('public.dbfspec_polyfunc',
|
203
|
+
PgFuncall.literal("'hello'::text")).should == 'hello'
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should return a string as a string' do
|
207
|
+
PgFuncall.call('public.dbfspec_textfunc', "hello", "goodbye").should == 'hello,goodbye'
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'should return a number as a string' do
|
211
|
+
PgFuncall.call('public.dbfspec_intfunc', 55, 100).should == 155
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'should return a literal array as an array' do
|
215
|
+
PgFuncall.call('public.dbfspec_arrayfunc',
|
216
|
+
PgFuncall.literal('ARRAY[55, 100]::integer[]')).
|
217
|
+
should == [110, 200]
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should take a Ruby array as a PG array' do
|
221
|
+
PgFuncall.call('public.dbfspec_arrayfunc',
|
222
|
+
[30, 92]).should == [60, 184]
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should take a Ruby hash as a PG hstore' do
|
226
|
+
PgFuncall.call('public.dbfspec_hstorefunc',
|
227
|
+
{'a' => 'foo', 'b' => 'baz'}, 'b').should == 'baz'
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'should return a PG hstore as a Ruby hash ' do
|
231
|
+
PgFuncall.call('public.dbfspec_hstorefunc',
|
232
|
+
{'a' => 'foo', 'b' => 'baz'},
|
233
|
+
{'c' => 'cat'}).should == {'a' => 'foo', 'b' => 'baz', 'c' => 'cat'}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
context 'type roundtripping via SELECT' do
|
238
|
+
def roundtrip(value)
|
239
|
+
PgFuncall.casting_query("SELECT $1;", [value]).first
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'numeric types' do
|
243
|
+
it 'should return an integer for an integer' do
|
244
|
+
roundtrip(3215).should eql(3215)
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'should return a float for a float' do
|
248
|
+
roundtrip(77.45).should eql(77.45)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'should handle bigdecimal' do
|
252
|
+
roundtrip(BigDecimal.new("500.23")).should == BigDecimal.new("500.23")
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context 'textual types' do
|
257
|
+
it 'should handle character' do
|
258
|
+
roundtrip(?Z).should == "Z"
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'should handle UTF-8 string' do
|
262
|
+
roundtrip("peter piper picked").should == "peter piper picked"
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'should handle binary string' do
|
266
|
+
binstring = [*(1..100)].pack('L*')
|
267
|
+
# puts "encoding is #{binstring.encoding}"
|
268
|
+
roundtrip(binstring).should == binstring
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
context 'hashes' do
|
273
|
+
it 'should handle empty hashes' do
|
274
|
+
roundtrip({}).should == {}
|
275
|
+
end
|
276
|
+
it 'should handle text->text hashes' do
|
277
|
+
roundtrip({'a' => 'foo', 'b' => 'baz'}).should == {'a' => 'foo', 'b' => 'baz'}
|
278
|
+
end
|
279
|
+
it 'should handle hashes with other type values' do
|
280
|
+
roundtrip({a: 'foo', b: 750}).should == {'a' => 'foo', 'b' => '750'}
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
context 'arrays' do
|
285
|
+
it 'should throw exception on untagged empty array' do
|
286
|
+
expect { roundtrip([]) }.to raise_error
|
287
|
+
end
|
288
|
+
it 'should handle wrapped, typed empty arrays' do
|
289
|
+
roundtrip(PgFuncall::TypedArray.new([], 'int4')).should == []
|
290
|
+
end
|
291
|
+
it 'should handle tagged arrays' do
|
292
|
+
roundtrip(PgFuncall::TypedArray.new([1,2,2**45], 'int8')).should == [1, 2, 2**45]
|
293
|
+
end
|
294
|
+
it 'should handle int arrays' do
|
295
|
+
roundtrip([1,2,77]).should == [1, 2, 77]
|
296
|
+
end
|
297
|
+
it 'should handle string arrays' do
|
298
|
+
roundtrip(%w[a b longerstring c]).should == ['a', 'b', 'longerstring', 'c']
|
299
|
+
end
|
300
|
+
it 'should handle arrays of int arrays' do
|
301
|
+
pending('it returns [nil, nil] for reasons unknown on AR 4.0.x - PLS FIX GOOBY') if
|
302
|
+
ActiveRecord.version.segments[0..1] == [4,0]
|
303
|
+
roundtrip(PgFuncall::TypedArray.new([[1,2,77], [99, 0, 4]], 'int4[]')).
|
304
|
+
should == [[1,2,77], [99, 0, 4]]
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'should handle arrays of txt arrays' do
|
308
|
+
roundtrip([['a', 'b'], ['x', 'y']]).should == [['a', 'b'], ['x', 'y']]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
context 'temporal types' do
|
313
|
+
let(:now) { Time.now }
|
314
|
+
it 'should handle Date' do
|
315
|
+
roundtrip(now).should == now
|
316
|
+
end
|
317
|
+
it 'should handle DateTime' do
|
318
|
+
roundtrip(now.to_datetime).should == now.to_datetime
|
319
|
+
end
|
320
|
+
it 'should handle Time' do
|
321
|
+
roundtrip(PgFuncall::PGTime.new('11:45')).should == Time.parse('2000-01-01 11:45:00 UTC')
|
322
|
+
end
|
323
|
+
it 'should handle interval' do
|
324
|
+
roundtrip(PgFuncall::PGTimeInterval.new('1 hour')).should == '01:00:00'
|
325
|
+
end
|
326
|
+
it 'should handle with time zone'
|
327
|
+
end
|
328
|
+
|
329
|
+
context 'network types' do
|
330
|
+
it 'should handle IPv4 host without netmask' do
|
331
|
+
roundtrip(IPAddr.new("1.2.3.4")).should == IPAddr.new("1.2.3.4")
|
332
|
+
end
|
333
|
+
it 'should handle IPv4 host expressed as /32' do
|
334
|
+
roundtrip(IPAddr.new("1.2.3.4/32")).should == IPAddr.new("1.2.3.4/32")
|
335
|
+
end
|
336
|
+
it 'should handle IPv4 network' do
|
337
|
+
roundtrip(IPAddr.new("1.2.3.4/20")).should == IPAddr.new("1.2.3.4/20")
|
338
|
+
end
|
339
|
+
it 'should handle IPv6 host' do
|
340
|
+
roundtrip(IPAddr.new("2607:f8b0:4002:c01::65")).should == IPAddr.new("2607:f8b0:4002:c01::65")
|
341
|
+
end
|
342
|
+
it 'should handle IPv6 network' do
|
343
|
+
roundtrip(IPAddr.new("2001:db8:abcd:8000::/50")).should == IPAddr.new("2001:db8:abcd:8000::/50")
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
context 'misc types' do
|
348
|
+
it 'should handle UUIDs' do
|
349
|
+
uuid = UUID.new.generate
|
350
|
+
roundtrip(PgFuncall::PGUUID.new(uuid)).should == uuid
|
351
|
+
end
|
352
|
+
it 'should handle OIDs'
|
353
|
+
it 'should handle explicitly tagged types'
|
354
|
+
it 'should handle untyped literals'
|
355
|
+
it 'should handle literals including a cast'
|
356
|
+
end
|
357
|
+
|
358
|
+
context 'range types' do
|
359
|
+
it 'should handle int4 ranges'
|
360
|
+
it 'should handle int8 ranges'
|
361
|
+
it 'should handle end-exclusive ranges'
|
362
|
+
it 'should handle bignum ranges'
|
363
|
+
it 'should handle decimal ranges'
|
364
|
+
it 'should handle date ranges'
|
365
|
+
it 'should handle time ranges'
|
366
|
+
it 'should handle timestamp ranges'
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|