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,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
+
@@ -0,0 +1,3 @@
1
+ class PgFuncall
2
+ VERSION = "0.1.0"
3
+ end
@@ -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