rdo-postgres 0.0.7 → 0.0.8
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.
- data/README.md +2 -3
- data/ext/rdo_postgres/arrays.c +124 -2
- data/lib/rdo/postgres/array.rb +39 -6
- data/lib/rdo/postgres/version.rb +1 -1
- data/spec/postgres/array_spec.rb +40 -0
- data/spec/postgres/bind_params_spec.rb +26 -0
- data/spec/postgres/type_cast_spec.rb +24 -0
- metadata +1 -1
data/README.md
CHANGED
|
@@ -73,9 +73,8 @@ currently mapped types are:
|
|
|
73
73
|
- TIMESTAMP -> DateTime (in the system time zone)
|
|
74
74
|
- TIMESTAMPTZ -> DateTime (in the specified time zone)
|
|
75
75
|
|
|
76
|
-
All
|
|
77
|
-
|
|
78
|
-
Arrays is coming.
|
|
76
|
+
All **n-dimensional Arrays** of the above listed **are supported**. Support
|
|
77
|
+
for custom-typed Arrays is coming.
|
|
79
78
|
|
|
80
79
|
### Bind parameters support
|
|
81
80
|
|
data/ext/rdo_postgres/arrays.c
CHANGED
|
@@ -11,10 +11,129 @@
|
|
|
11
11
|
#include <ruby.h>
|
|
12
12
|
#include <libpq-fe.h>
|
|
13
13
|
|
|
14
|
+
/** Use to represent state information during a parse */
|
|
15
|
+
typedef struct {
|
|
16
|
+
int encoding;
|
|
17
|
+
VALUE wrapper;
|
|
18
|
+
VALUE ary;
|
|
19
|
+
char * ptr;
|
|
20
|
+
char * buf;
|
|
21
|
+
} RDOPostgresArrayParseContext;
|
|
22
|
+
|
|
23
|
+
/** Forward-declaration of RDO::Postgres::Array.parse */
|
|
24
|
+
static VALUE rdo_postgres_array_parse(VALUE self, VALUE str);
|
|
25
|
+
|
|
26
|
+
/** Push a sub-array onto the stack */
|
|
27
|
+
static void rdo_postgres_array_parse_subarray(VALUE self,
|
|
28
|
+
RDOPostgresArrayParseContext * ctx) {
|
|
29
|
+
|
|
30
|
+
rb_ary_push(ctx->ary,
|
|
31
|
+
rdo_postgres_array_parse(self,
|
|
32
|
+
RDO_STRING(ctx->buf, ctx->ptr - ctx->buf, ctx->encoding)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Push a value onto the stack */
|
|
36
|
+
static void rdo_postgres_array_parse_value(VALUE self,
|
|
37
|
+
RDOPostgresArrayParseContext * ctx) {
|
|
38
|
+
|
|
39
|
+
rb_ary_push(ctx->ary,
|
|
40
|
+
rb_funcall(ctx->wrapper,
|
|
41
|
+
rb_intern("parse_value_or_null"), 1,
|
|
42
|
+
RDO_STRING(ctx->buf, ctx->ptr - ctx->buf, ctx->encoding)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse the given PostgreSQL formatted array String into an Array */
|
|
46
|
+
static VALUE rdo_postgres_array_parse(VALUE self, VALUE str) {
|
|
47
|
+
Check_Type(str, T_STRING);
|
|
48
|
+
|
|
49
|
+
RDOPostgresArrayParseContext ctx = {
|
|
50
|
+
.encoding = rb_enc_get_index(str),
|
|
51
|
+
.wrapper = rb_funcall(self, rb_intern("new"), 0),
|
|
52
|
+
.ary = rb_ary_new(),
|
|
53
|
+
.buf = malloc(sizeof(char) * RSTRING_LEN(str)),
|
|
54
|
+
.ptr = NULL
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
ctx.ptr = ctx.buf;
|
|
58
|
+
|
|
59
|
+
char * cstr = RSTRING_PTR(str);
|
|
60
|
+
char * s = cstr;
|
|
61
|
+
int braces = 0;
|
|
62
|
+
int quotes = 0;
|
|
63
|
+
|
|
64
|
+
for (; *s; ++s) {
|
|
65
|
+
switch (*s) {
|
|
66
|
+
case '{':
|
|
67
|
+
if (quotes) {
|
|
68
|
+
*(ctx.ptr++) = *s;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (braces) // nested brace
|
|
73
|
+
*(ctx.ptr++) = *s;
|
|
74
|
+
++braces;
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case '}':
|
|
78
|
+
if (quotes) {
|
|
79
|
+
*(ctx.ptr++) = *s;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
--braces;
|
|
84
|
+
if (braces) {
|
|
85
|
+
*(ctx.ptr++) = *s;
|
|
86
|
+
if (braces == 1) { // child
|
|
87
|
+
rdo_postgres_array_parse_subarray(self, &ctx);
|
|
88
|
+
ctx.ptr = ctx.buf;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (ctx.ptr != ctx.buf) { // not empty braces
|
|
92
|
+
rdo_postgres_array_parse_value(self, &ctx);
|
|
93
|
+
ctx.ptr = ctx.buf;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case '"':
|
|
99
|
+
quotes = !quotes; // jump in and out of quotes
|
|
100
|
+
*(ctx.ptr++) = *s;
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case '\\':
|
|
104
|
+
*(ctx.ptr++) = *(s++); // swallow anything after escape
|
|
105
|
+
*(ctx.ptr++) = *s;
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case ',':
|
|
109
|
+
if (quotes) {
|
|
110
|
+
*(ctx.ptr++) = *s;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (braces > 1) { // still in child
|
|
115
|
+
*(ctx.ptr++) = *s;
|
|
116
|
+
} else {
|
|
117
|
+
if (ctx.ptr != ctx.buf) { // not an outer array
|
|
118
|
+
rdo_postgres_array_parse_value(self, &ctx);
|
|
119
|
+
ctx.ptr = ctx.buf;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
*(ctx.ptr++) = *s;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
free(ctx.buf);
|
|
130
|
+
|
|
131
|
+
return rb_funcall(ctx.wrapper, rb_intern("replace"), 1, ctx.ary);
|
|
132
|
+
}
|
|
133
|
+
|
|
14
134
|
/** Parse a bytea string into a binary Ruby String */
|
|
15
135
|
static VALUE rdo_postgres_array_bytea_parse_value(VALUE self, VALUE s) {
|
|
16
|
-
s = rb_call_super(1, &s);
|
|
17
|
-
Check_Type(s, T_STRING);
|
|
136
|
+
Check_Type((s = rb_call_super(1, &s)), T_STRING);
|
|
18
137
|
return rdo_postgres_cast_bytea(RSTRING_PTR(s), RSTRING_LEN(s));
|
|
19
138
|
}
|
|
20
139
|
|
|
@@ -38,8 +157,11 @@ static VALUE rdo_postgres_array_bytea_format_value(VALUE self, VALUE v) {
|
|
|
38
157
|
|
|
39
158
|
/** Initialize Array extensions */
|
|
40
159
|
void Init_rdo_postgres_arrays(void) {
|
|
160
|
+
VALUE cArray = rb_path2class("RDO::Postgres::Array");
|
|
41
161
|
VALUE cByteaArray = rb_path2class("RDO::Postgres::Array::Bytea");
|
|
42
162
|
|
|
163
|
+
rb_define_singleton_method(cArray, "parse", rdo_postgres_array_parse, 1);
|
|
164
|
+
|
|
43
165
|
rb_define_method(cByteaArray,
|
|
44
166
|
"parse_value", rdo_postgres_array_bytea_parse_value, 1);
|
|
45
167
|
|
data/lib/rdo/postgres/array.rb
CHANGED
|
@@ -21,28 +21,57 @@ module RDO
|
|
|
21
21
|
# # => ["John Smith", "Sarah Doe"]
|
|
22
22
|
class Array < ::Array
|
|
23
23
|
class << self
|
|
24
|
+
# Shortcut for the constructor.
|
|
25
|
+
#
|
|
26
|
+
# @param [Object...] *args
|
|
27
|
+
# a list of objects to put inside the Array
|
|
28
|
+
#
|
|
29
|
+
# @return [Array]
|
|
30
|
+
# a newly initialzed Array
|
|
31
|
+
def [](*args)
|
|
32
|
+
new(args)
|
|
33
|
+
end
|
|
34
|
+
|
|
24
35
|
# Read a PostgreSQL array in its string form.
|
|
25
36
|
#
|
|
26
37
|
# @param [String] str
|
|
27
|
-
# an array string from
|
|
38
|
+
# an array string from PostgreSQL
|
|
28
39
|
#
|
|
29
40
|
# @return [Array]
|
|
30
41
|
# a Ruby Array for this string
|
|
31
42
|
def parse(str)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
# defined in ext/rdo_postgres/arrays.c
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Initialize a new Array, coercing any sub-Arrays to the same type.
|
|
48
|
+
#
|
|
49
|
+
# @param [Array] arr
|
|
50
|
+
# the Array to wrap
|
|
51
|
+
def initialize(arr = 0)
|
|
52
|
+
if ::Array === arr
|
|
53
|
+
super(arr.map{|v| ::Array === v ? self.class.new(v) : v})
|
|
54
|
+
else
|
|
55
|
+
super
|
|
35
56
|
end
|
|
36
57
|
end
|
|
37
58
|
|
|
38
59
|
# Convert the Array to the format used by PostgreSQL.
|
|
39
60
|
#
|
|
40
61
|
# @return [String]
|
|
41
|
-
# a
|
|
62
|
+
# a PostgreSQL array string
|
|
42
63
|
def to_s
|
|
43
64
|
"{#{map(&method(:format_value_or_null)).join(",")}}"
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
# Convert the Array to a standard Ruby Array.
|
|
68
|
+
#
|
|
69
|
+
# @return [::Array]
|
|
70
|
+
# a Ruby Array
|
|
71
|
+
def to_a
|
|
72
|
+
super.map{|v| Array === v ? v.to_a : v}
|
|
73
|
+
end
|
|
74
|
+
|
|
46
75
|
# Format an individual element in the Array for building into a String.
|
|
47
76
|
#
|
|
48
77
|
# The default implementation wraps quotes around the element.
|
|
@@ -73,7 +102,11 @@ module RDO
|
|
|
73
102
|
private
|
|
74
103
|
|
|
75
104
|
def format_value_or_null(v)
|
|
76
|
-
|
|
105
|
+
case v
|
|
106
|
+
when nil then "NULL"
|
|
107
|
+
when Array then v.to_s
|
|
108
|
+
else format_value(v)
|
|
109
|
+
end
|
|
77
110
|
end
|
|
78
111
|
|
|
79
112
|
def parse_value_or_null(s)
|
data/lib/rdo/postgres/version.rb
CHANGED
data/spec/postgres/array_spec.rb
CHANGED
|
@@ -53,6 +53,14 @@ describe RDO::Postgres::Array do
|
|
|
53
53
|
arr.to_s.should == '{"42","7"}'
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
context "with a multi-dimensional Array" do
|
|
58
|
+
let(:arr) { RDO::Postgres::Array[["a", "b"], ["c", "d"]] }
|
|
59
|
+
|
|
60
|
+
it "formats the inner Arrays" do
|
|
61
|
+
arr.to_s.should == '{{"a","b"},{"c","d"}}'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
describe "#to_a" do
|
|
@@ -61,6 +69,14 @@ describe RDO::Postgres::Array do
|
|
|
61
69
|
it "returns a core ruby Array" do
|
|
62
70
|
arr.to_a.class.should == ::Array
|
|
63
71
|
end
|
|
72
|
+
|
|
73
|
+
context "with a multidimensional Array" do
|
|
74
|
+
let(:arr) { RDO::Postgres::Array[[1, 2], [3, 4]] }
|
|
75
|
+
|
|
76
|
+
it "converts the inner elements to core Ruby Arrays" do
|
|
77
|
+
arr.to_a[0].class.should == ::Array
|
|
78
|
+
end
|
|
79
|
+
end
|
|
64
80
|
end
|
|
65
81
|
|
|
66
82
|
describe ".parse" do
|
|
@@ -118,5 +134,29 @@ describe RDO::Postgres::Array do
|
|
|
118
134
|
arr.to_a.should == [nil, nil, "c"]
|
|
119
135
|
end
|
|
120
136
|
end
|
|
137
|
+
|
|
138
|
+
context "with a multi-dimensonal array" do
|
|
139
|
+
let(:str) { '{{a,b},{c,d}}' }
|
|
140
|
+
|
|
141
|
+
it "returns an Array of Arrays of Strings" do
|
|
142
|
+
arr.to_a.should == [["a", "b"], ["c", "d"]]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context "containing commas" do
|
|
146
|
+
let(:str) { '{{"a,b","c,d"},{"e,f","g,h"}}' }
|
|
147
|
+
|
|
148
|
+
it "returns an Array of Arrays of Strings" do
|
|
149
|
+
arr.to_a.should == [["a,b", "c,d"], ["e,f", "g,h"]]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
context "containing escaped quotes" do
|
|
154
|
+
let(:str) { '{{"a \\"b\\"","c \\"d\\""},{"e","f"}}' }
|
|
155
|
+
|
|
156
|
+
it "returns an Array of Arrays of Strings" do
|
|
157
|
+
arr.to_a.should == [['a "b"', 'c "d"'], ["e", "f"]]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
121
161
|
end
|
|
122
162
|
end
|
|
@@ -738,6 +738,19 @@ describe RDO::Postgres::Driver, "bind parameter support" do
|
|
|
738
738
|
tuple.should == {id: 1, words: ["apple\norange"]}
|
|
739
739
|
end
|
|
740
740
|
end
|
|
741
|
+
|
|
742
|
+
context "multidimensional" do
|
|
743
|
+
let(:tuple) do
|
|
744
|
+
connection.execute(
|
|
745
|
+
"INSERT INTO test (words) VALUES (?) RETURNING *",
|
|
746
|
+
[['a "b"', "c"], ["d \\ e", "f"]]
|
|
747
|
+
).first
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
it "is inferred correctly" do
|
|
751
|
+
tuple.should == {id: 1, words: [['a "b"', "c"], ["d \\ e", "f"]]}
|
|
752
|
+
end
|
|
753
|
+
end
|
|
741
754
|
end
|
|
742
755
|
|
|
743
756
|
context "against an integer[] field" do
|
|
@@ -759,6 +772,19 @@ describe RDO::Postgres::Driver, "bind parameter support" do
|
|
|
759
772
|
tuple.should == {id: 1, days: [4, nil]}
|
|
760
773
|
end
|
|
761
774
|
end
|
|
775
|
+
|
|
776
|
+
context "multidimensional" do
|
|
777
|
+
let(:tuple) do
|
|
778
|
+
connection.execute(
|
|
779
|
+
"INSERT INTO test (days) VALUES (?) RETURNING *",
|
|
780
|
+
[[4, 12], [9, 29]]
|
|
781
|
+
).first
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
it "is inferred correctly" do
|
|
785
|
+
tuple.should == {id: 1, days: [[4, 12], [9, 29]]}
|
|
786
|
+
end
|
|
787
|
+
end
|
|
762
788
|
end
|
|
763
789
|
|
|
764
790
|
context "against an numeric[] field" do
|
|
@@ -301,6 +301,14 @@ describe RDO::Postgres::Driver, "type casting" do
|
|
|
301
301
|
value.should == [nil, "b"]
|
|
302
302
|
end
|
|
303
303
|
end
|
|
304
|
+
|
|
305
|
+
context "multidimensional" do
|
|
306
|
+
let(:sql) { %q{SELECT ARRAY[ARRAY['a "x"', 'b'], ARRAY['c', 'd']]::text[]} }
|
|
307
|
+
|
|
308
|
+
it "returns an Array of Arrays of Strings" do
|
|
309
|
+
value.should == [['a "x"', "b"], ["c", "d"]]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
304
312
|
end
|
|
305
313
|
|
|
306
314
|
describe "char[] cast" do
|
|
@@ -373,6 +381,14 @@ describe RDO::Postgres::Driver, "type casting" do
|
|
|
373
381
|
value.should == [nil, 7]
|
|
374
382
|
end
|
|
375
383
|
end
|
|
384
|
+
|
|
385
|
+
context "multidimensional" do
|
|
386
|
+
let(:sql) { "SELECT ARRAY[ARRAY[42, 7], ARRAY[1, 9]]::integer[]" }
|
|
387
|
+
|
|
388
|
+
it "returns an Array of Arrays of Fixnums" do
|
|
389
|
+
value.should == [[42, 7], [1, 9]]
|
|
390
|
+
end
|
|
391
|
+
end
|
|
376
392
|
end
|
|
377
393
|
|
|
378
394
|
describe "float[] cast" do
|
|
@@ -397,6 +413,14 @@ describe RDO::Postgres::Driver, "type casting" do
|
|
|
397
413
|
value.should == [nil, 7.2]
|
|
398
414
|
end
|
|
399
415
|
end
|
|
416
|
+
|
|
417
|
+
context "multidimensional" do
|
|
418
|
+
let(:sql) { %q{SELECT ARRAY[ARRAY[9.7, 10.1], ARRAY[0.4, 1.2]]::float[]} }
|
|
419
|
+
|
|
420
|
+
it "returns an Array of Arrays of Floats" do
|
|
421
|
+
value.should == [[9.7, 10.1], [0.4, 1.2]]
|
|
422
|
+
end
|
|
423
|
+
end
|
|
400
424
|
end
|
|
401
425
|
|
|
402
426
|
describe "numeric[] cast" do
|