rdo-postgres 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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 1-dimensional Arrays of the above listed are also available. Support for
77
- multi-dimensional Arrays is planned immediately. Support for custom-typed
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
 
@@ -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
 
@@ -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 postgresql
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
- new.tap do |a|
33
- a.replace(str[1...-1].split(",").map(&a.method(:parse_value_or_null)))
34
- end
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 postgresql array string
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
- v.nil? ? "NULL" : format_value(v)
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)
@@ -7,6 +7,6 @@
7
7
 
8
8
  module RDO
9
9
  module Postgres
10
- VERSION = "0.0.7"
10
+ VERSION = "0.0.8"
11
11
  end
12
12
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdo-postgres
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: