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 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: