sequel 3.36.1 → 3.37.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.
Files changed (108) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +13 -0
  3. data/bin/sequel +12 -16
  4. data/doc/advanced_associations.rdoc +36 -67
  5. data/doc/association_basics.rdoc +11 -16
  6. data/doc/release_notes/3.37.0.txt +338 -0
  7. data/doc/schema_modification.rdoc +4 -0
  8. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  9. data/lib/sequel/adapters/jdbc/postgresql.rb +26 -8
  10. data/lib/sequel/adapters/mysql2.rb +4 -3
  11. data/lib/sequel/adapters/odbc/mssql.rb +2 -2
  12. data/lib/sequel/adapters/postgres.rb +4 -60
  13. data/lib/sequel/adapters/shared/mssql.rb +2 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +0 -5
  15. data/lib/sequel/adapters/shared/postgres.rb +68 -2
  16. data/lib/sequel/adapters/shared/sqlite.rb +17 -1
  17. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +12 -1
  18. data/lib/sequel/adapters/utils/pg_types.rb +76 -0
  19. data/lib/sequel/core.rb +13 -0
  20. data/lib/sequel/database/misc.rb +41 -1
  21. data/lib/sequel/database/schema_generator.rb +23 -10
  22. data/lib/sequel/database/schema_methods.rb +26 -4
  23. data/lib/sequel/dataset/graph.rb +2 -1
  24. data/lib/sequel/dataset/query.rb +62 -2
  25. data/lib/sequel/extensions/_pretty_table.rb +7 -3
  26. data/lib/sequel/extensions/arbitrary_servers.rb +5 -4
  27. data/lib/sequel/extensions/blank.rb +4 -0
  28. data/lib/sequel/extensions/columns_introspection.rb +13 -2
  29. data/lib/sequel/extensions/core_extensions.rb +6 -0
  30. data/lib/sequel/extensions/eval_inspect.rb +158 -0
  31. data/lib/sequel/extensions/inflector.rb +4 -0
  32. data/lib/sequel/extensions/looser_typecasting.rb +5 -4
  33. data/lib/sequel/extensions/migration.rb +4 -1
  34. data/lib/sequel/extensions/named_timezones.rb +4 -0
  35. data/lib/sequel/extensions/null_dataset.rb +4 -0
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pg_array.rb +219 -168
  38. data/lib/sequel/extensions/pg_array_ops.rb +7 -2
  39. data/lib/sequel/extensions/pg_auto_parameterize.rb +10 -4
  40. data/lib/sequel/extensions/pg_hstore.rb +3 -1
  41. data/lib/sequel/extensions/pg_hstore_ops.rb +7 -2
  42. data/lib/sequel/extensions/pg_inet.rb +28 -3
  43. data/lib/sequel/extensions/pg_interval.rb +192 -0
  44. data/lib/sequel/extensions/pg_json.rb +21 -9
  45. data/lib/sequel/extensions/pg_range.rb +487 -0
  46. data/lib/sequel/extensions/pg_range_ops.rb +122 -0
  47. data/lib/sequel/extensions/pg_statement_cache.rb +3 -2
  48. data/lib/sequel/extensions/pretty_table.rb +12 -1
  49. data/lib/sequel/extensions/query.rb +4 -0
  50. data/lib/sequel/extensions/query_literals.rb +6 -6
  51. data/lib/sequel/extensions/schema_dumper.rb +39 -38
  52. data/lib/sequel/extensions/select_remove.rb +4 -0
  53. data/lib/sequel/extensions/server_block.rb +3 -2
  54. data/lib/sequel/extensions/split_array_nil.rb +65 -0
  55. data/lib/sequel/extensions/sql_expr.rb +4 -0
  56. data/lib/sequel/extensions/string_date_time.rb +4 -0
  57. data/lib/sequel/extensions/thread_local_timezones.rb +9 -3
  58. data/lib/sequel/extensions/to_dot.rb +4 -0
  59. data/lib/sequel/model/associations.rb +150 -91
  60. data/lib/sequel/plugins/identity_map.rb +2 -2
  61. data/lib/sequel/plugins/list.rb +1 -0
  62. data/lib/sequel/plugins/many_through_many.rb +33 -32
  63. data/lib/sequel/plugins/nested_attributes.rb +11 -3
  64. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  65. data/lib/sequel/plugins/schema.rb +1 -1
  66. data/lib/sequel/sql.rb +14 -14
  67. data/lib/sequel/version.rb +2 -2
  68. data/spec/adapters/mysql_spec.rb +25 -0
  69. data/spec/adapters/postgres_spec.rb +572 -28
  70. data/spec/adapters/sqlite_spec.rb +16 -1
  71. data/spec/core/database_spec.rb +61 -2
  72. data/spec/core/dataset_spec.rb +92 -0
  73. data/spec/core/expression_filters_spec.rb +12 -0
  74. data/spec/extensions/arbitrary_servers_spec.rb +1 -1
  75. data/spec/extensions/boolean_readers_spec.rb +25 -25
  76. data/spec/extensions/eval_inspect_spec.rb +58 -0
  77. data/spec/extensions/json_serializer_spec.rb +0 -6
  78. data/spec/extensions/list_spec.rb +1 -1
  79. data/spec/extensions/looser_typecasting_spec.rb +7 -7
  80. data/spec/extensions/many_through_many_spec.rb +81 -0
  81. data/spec/extensions/nested_attributes_spec.rb +21 -4
  82. data/spec/extensions/pg_array_ops_spec.rb +1 -11
  83. data/spec/extensions/pg_array_spec.rb +181 -90
  84. data/spec/extensions/pg_auto_parameterize_spec.rb +3 -3
  85. data/spec/extensions/pg_hstore_spec.rb +1 -3
  86. data/spec/extensions/pg_inet_spec.rb +6 -1
  87. data/spec/extensions/pg_interval_spec.rb +73 -0
  88. data/spec/extensions/pg_json_spec.rb +5 -9
  89. data/spec/extensions/pg_range_ops_spec.rb +49 -0
  90. data/spec/extensions/pg_range_spec.rb +372 -0
  91. data/spec/extensions/pg_statement_cache_spec.rb +1 -2
  92. data/spec/extensions/query_literals_spec.rb +1 -2
  93. data/spec/extensions/schema_dumper_spec.rb +48 -89
  94. data/spec/extensions/serialization_spec.rb +1 -5
  95. data/spec/extensions/server_block_spec.rb +2 -2
  96. data/spec/extensions/spec_helper.rb +12 -2
  97. data/spec/extensions/split_array_nil_spec.rb +24 -0
  98. data/spec/integration/associations_test.rb +4 -4
  99. data/spec/integration/database_test.rb +2 -2
  100. data/spec/integration/dataset_test.rb +4 -4
  101. data/spec/integration/eager_loader_test.rb +6 -6
  102. data/spec/integration/plugin_test.rb +2 -2
  103. data/spec/integration/spec_helper.rb +2 -2
  104. data/spec/model/association_reflection_spec.rb +5 -0
  105. data/spec/model/associations_spec.rb +156 -49
  106. data/spec/model/eager_loading_spec.rb +137 -2
  107. data/spec/model/model_spec.rb +10 -10
  108. metadata +15 -2
@@ -5,7 +5,8 @@ describe "pg_inet extension" do
5
5
  before do
6
6
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
7
7
  @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
8
- @db.extend(Sequel::Postgres::InetDatabaseMethods)
8
+ @db.extension(:pg_array)
9
+ @db.extension(:pg_inet)
9
10
  end
10
11
 
11
12
  it "should literalize IPAddr v4 instances to strings correctly" do
@@ -29,6 +30,10 @@ describe "pg_inet extension" do
29
30
  @db.bound_variable_arg(IPAddr.new('127.0.0.1'), nil).should == '127.0.0.1/32'
30
31
  end
31
32
 
33
+ it "should support using IPAddr instances in array types in bound variables" do
34
+ @db.bound_variable_arg([IPAddr.new('127.0.0.1')].pg_array, nil).should == '{"127.0.0.1/32"}'
35
+ end
36
+
32
37
  it "should parse inet/cidr type from the schema correctly" do
33
38
  @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'inet'}, {:name=>'c', :db_type=>'cidr'}]
34
39
  @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :ipaddr, :ipaddr]
@@ -0,0 +1,73 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ begin
4
+ require 'active_support/duration'
5
+ rescue LoadError => e
6
+ skip_warn "pg_interval plugin: can't load active_support/duration (#{e.class}: #{e})"
7
+ else
8
+ describe "pg_interval extension" do
9
+ before do
10
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
11
+ @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
12
+ @db.extension(:pg_array, :pg_interval)
13
+ end
14
+
15
+ it "should literalize ActiveSupport::Duration instances to strings correctly" do
16
+ @db.literal(ActiveSupport::Duration.new(0, [])).should == "'0'::interval"
17
+ @db.literal(ActiveSupport::Duration.new(0, [[:seconds, 0]])).should == "'0'::interval"
18
+ @db.literal(ActiveSupport::Duration.new(0, [[:seconds, 10], [:minutes, 20], [:days, 3], [:months, 4], [:years, 6]])).should == "'6 years 4 months 3 days 20 minutes 10 seconds '::interval"
19
+ @db.literal(ActiveSupport::Duration.new(0, [[:seconds, -10.000001], [:minutes, -20], [:days, -3], [:months, -4], [:years, -6]])).should == "'-6 years -4 months -3 days -20 minutes -10.000001 seconds '::interval"
20
+ end
21
+
22
+ it "should literalize ActiveSupport::Duration instances with repeated parts correctly" do
23
+ @db.literal(ActiveSupport::Duration.new(0, [[:seconds, 2], [:seconds, 1]])).should == "'3 seconds '::interval"
24
+ @db.literal(ActiveSupport::Duration.new(0, [[:seconds, 2], [:seconds, 1], [:days, 1], [:days, 4]])).should == "'5 days 3 seconds '::interval"
25
+ end
26
+
27
+ it "should not affect literalization of custom objects" do
28
+ o = Object.new
29
+ def o.sql_literal(ds) 'v' end
30
+ @db.literal(o).should == 'v'
31
+ end
32
+
33
+ it "should support using ActiveSupport::Duration instances as bound variables" do
34
+ @db.bound_variable_arg(1, nil).should == 1
35
+ @db.bound_variable_arg(ActiveSupport::Duration.new(0, [[:seconds, 0]]), nil).should == '0'
36
+ @db.bound_variable_arg(ActiveSupport::Duration.new(0, [[:seconds, -10.000001], [:minutes, -20], [:days, -3], [:months, -4], [:years, -6]]), nil).should == '-6 years -4 months -3 days -20 minutes -10.000001 seconds '
37
+ end
38
+
39
+ it "should support using ActiveSupport::Duration instances in array types in bound variables" do
40
+ @db.bound_variable_arg([ActiveSupport::Duration.new(0, [[:seconds, 0]])].pg_array, nil).should == '{"0"}'
41
+ @db.bound_variable_arg([ActiveSupport::Duration.new(0, [[:seconds, -10.000001], [:minutes, -20], [:days, -3], [:months, -4], [:years, -6]])].pg_array, nil).should == '{"-6 years -4 months -3 days -20 minutes -10.000001 seconds "}'
42
+ end
43
+
44
+ it "should parse interval type from the schema correctly" do
45
+ @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'interval'}]
46
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :interval]
47
+ end
48
+
49
+ it "should support typecasting for the interval type" do
50
+ ip = IPAddr.new('127.0.0.1')
51
+ d = ActiveSupport::Duration.new(31557600 + 2*86400*30 + 3*86400*7 + 4*86400 + 5*3600 + 6*60 + 7, [[:years, 1], [:months, 2], [:days, 25], [:seconds, 18367]])
52
+ @db.typecast_value(:interval, d).object_id.should == d.object_id
53
+
54
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 05:06:07").is_a?(ActiveSupport::Duration).should be_true
55
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 05:06:07").should == d
56
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 05:06:07").parts.sort_by{|k,v| k.to_s}.should == d.parts.sort_by{|k,v| k.to_s}
57
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 05:06:07.0").parts.sort_by{|k,v| k.to_s}.should == d.parts.sort_by{|k,v| k.to_s}
58
+
59
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 5 hours 6 mins 7 secs").is_a?(ActiveSupport::Duration).should be_true
60
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 5 hours 6 mins 7 secs").should == d
61
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 5 hours 6 mins 7 secs").parts.sort_by{|k,v| k.to_s}.should == d.parts.sort_by{|k,v| k.to_s}
62
+ @db.typecast_value(:interval, "1 year 2 mons 25 days 5 hours 6 mins 7.0 secs").parts.sort_by{|k,v| k.to_s}.should == d.parts.sort_by{|k,v| k.to_s}
63
+
64
+ d2 = ActiveSupport::Duration.new(1, [[:seconds, 1]])
65
+ @db.typecast_value(:interval, 1).is_a?(ActiveSupport::Duration).should be_true
66
+ @db.typecast_value(:interval, 1).should == d2
67
+ @db.typecast_value(:interval, 1).parts.sort_by{|k,v| k.to_s}.should == d2.parts.sort_by{|k,v| k.to_s}
68
+
69
+ proc{@db.typecast_value(:interval, 'foo')}.should raise_error(Sequel::InvalidValue)
70
+ proc{@db.typecast_value(:interval, Object.new)}.should raise_error(Sequel::InvalidValue)
71
+ end
72
+ end
73
+ end
@@ -1,10 +1,5 @@
1
1
  require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
2
 
3
- begin
4
- Sequel.extension :pg_json
5
- rescue LoadError => e
6
- skip_warn "can't load pg_json extension (#{e.class}: #{e})"
7
- else
8
3
  describe "pg_json extension" do
9
4
  before(:all) do
10
5
  m = Sequel::Postgres
@@ -26,6 +21,7 @@ describe "pg_json extension" do
26
21
  before do
27
22
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
28
23
  @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
24
+ @db.extension(:pg_array, :pg_json)
29
25
  end
30
26
 
31
27
  it "should parse json strings correctly" do
@@ -70,20 +66,21 @@ describe "pg_json extension" do
70
66
  end
71
67
 
72
68
  it "should support using JSONHash and JSONArray as bound variables" do
73
- @db.extend @m
74
69
  @db.bound_variable_arg(1, nil).should == 1
75
70
  @db.bound_variable_arg([1].pg_json, nil).should == '[1]'
76
71
  @db.bound_variable_arg({'a'=>'b'}.pg_json, nil).should == '{"a":"b"}'
77
72
  end
78
73
 
74
+ it "should support using json[] types in bound variables" do
75
+ @db.bound_variable_arg([[{"a"=>1}].pg_json, {"b"=>[1, 2]}.pg_json].pg_array, nil).should == '{"[{\\"a\\":1}]","{\\"b\\":[1,2]}"}'
76
+ end
77
+
79
78
  it "should parse json type from the schema correctly" do
80
- @db.extend @m
81
79
  @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'json'}]
82
80
  @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :json]
83
81
  end
84
82
 
85
83
  it "should support typecasting for the json type" do
86
- @db.extend @m
87
84
  h = {1=>2}.pg_json
88
85
  a = [1].pg_json
89
86
  @db.typecast_value(:json, h).should equal(h)
@@ -98,4 +95,3 @@ describe "pg_json extension" do
98
95
  proc{@db.typecast_value(:json, 1)}.should raise_error(Sequel::InvalidValue)
99
96
  end
100
97
  end
101
- end
@@ -0,0 +1,49 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "Sequel::Postgres::RangeOp" do
4
+ before do
5
+ @ds = Sequel.connect('mock://postgres', :quote_identifiers=>false).dataset
6
+ @h = :h.pg_range
7
+ end
8
+
9
+ it "#pg_range should return self" do
10
+ @h.pg_range.should equal(@h)
11
+ end
12
+
13
+ it "#pg_range should return a RangeOp for symbols, literal strings, and expressions" do
14
+ @ds.literal(:h.pg_range.lower).should == "lower(h)"
15
+ @ds.literal(Sequel.function(:b, :h).pg_range.lower).should == "lower(b(h))"
16
+ @ds.literal(Sequel.lit('h').pg_range.lower).should == "lower(h)"
17
+ end
18
+
19
+ it "PGRange#op should return a RangeOp" do
20
+ @ds.literal((1..2).pg_range(:numrange).op.lower).should == "lower('[1,2]'::numrange)"
21
+ end
22
+
23
+ it "should define methods for all of the the PostgreSQL range operators" do
24
+ @ds.literal(@h.contains(@h)).should == "(h @> h)"
25
+ @ds.literal(@h.contained_by(@h)).should == "(h <@ h)"
26
+ @ds.literal(@h.overlaps(@h)).should == "(h && h)"
27
+ @ds.literal(@h.left_of(@h)).should == "(h << h)"
28
+ @ds.literal(@h.right_of(@h)).should == "(h >> h)"
29
+ @ds.literal(@h.starts_before(@h)).should == "(h &< h)"
30
+ @ds.literal(@h.ends_after(@h)).should == "(h &> h)"
31
+ @ds.literal(@h.adjacent_to(@h)).should == "(h -|- h)"
32
+ end
33
+
34
+ it "should define methods for all of the the PostgreSQL range functions" do
35
+ @ds.literal(@h.lower).should == "lower(h)"
36
+ @ds.literal(@h.upper).should == "upper(h)"
37
+ @ds.literal(@h.isempty).should == "isempty(h)"
38
+ @ds.literal(@h.lower_inc).should == "lower_inc(h)"
39
+ @ds.literal(@h.upper_inc).should == "upper_inc(h)"
40
+ @ds.literal(@h.lower_inf).should == "lower_inf(h)"
41
+ @ds.literal(@h.upper_inf).should == "upper_inf(h)"
42
+ end
43
+
44
+ it "+ - * operators should be defined and return a RangeOp" do
45
+ @ds.literal((@h + @h).lower).should == "lower((h + h))"
46
+ @ds.literal((@h * @h).lower).should == "lower((h * h))"
47
+ @ds.literal((@h - @h).lower).should == "lower((h - h))"
48
+ end
49
+ end
@@ -0,0 +1,372 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "pg_range extension" do
4
+ before do
5
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
6
+ @R = Sequel::Postgres::PGRange
7
+ @db.extend(Module.new{def get_conversion_procs(conn) {} end; def bound_variable_arg(arg, conn) arg end})
8
+ @db.extend_datasets(Module.new{def supports_timestamp_timezones?; false; end; def supports_timestamp_usecs?; false; end})
9
+ @db.extension(:pg_array)
10
+ @db.extension(:pg_range)
11
+ end
12
+
13
+ it "should literalize Range instances to strings correctly" do
14
+ @db.literal(Date.new(2011, 1, 2)...Date.new(2011, 3, 2)).should == "'[2011-01-02,2011-03-02)'"
15
+ @db.literal(Time.local(2011, 1, 2, 10, 20, 30)...Time.local(2011, 2, 3, 10, 20, 30)).should == "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'"
16
+ @db.literal(DateTime.new(2011, 1, 2, 10, 20, 30)...DateTime.new(2011, 2, 3, 10, 20, 30)).should == "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'"
17
+ @db.literal(DateTime.new(2011, 1, 2, 10, 20, 30)...DateTime.new(2011, 2, 3, 10, 20, 30)).should == "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'"
18
+ @db.literal(1..2).should == "'[1,2]'"
19
+ @db.literal(1.0..2.0).should == "'[1.0,2.0]'"
20
+ @db.literal(BigDecimal.new('1.0')..BigDecimal.new('2.0')).should == "'[1.0,2.0]'"
21
+ @db.literal(Sequel.lit('a')..Sequel.lit('z')).should == "'[a,z]'"
22
+ @db.literal(''..'()[]",\\2').should == "'[\"\",\\(\\)\\[\\]\\\"\\,\\\\2]'"
23
+ end
24
+
25
+ it "should literalize PGRange instances to strings correctly" do
26
+ @db.literal(@R.new(1, 2)).should == "'[1,2]'"
27
+ @db.literal(@R.new(true, false)).should == "'[true,false]'"
28
+ @db.literal(@R.new(1, 2, :exclude_begin=>true)).should == "'(1,2]'"
29
+ @db.literal(@R.new(1, 2, :exclude_end=>true)).should == "'[1,2)'"
30
+ @db.literal(@R.new(nil, 2)).should == "'[,2]'"
31
+ @db.literal(@R.new(1, nil)).should == "'[1,]'"
32
+ @db.literal(@R.new(1, 2, :db_type=>'int8range')).should == "'[1,2]'::int8range"
33
+ @db.literal(@R.new(nil, nil, :empty=>true)).should == "'empty'"
34
+ @db.literal(@R.new("", 2)).should == "'[\"\",2]'"
35
+ end
36
+
37
+ it "should not affect literalization of custom objects" do
38
+ o = Object.new
39
+ def o.sql_literal(ds) 'v' end
40
+ @db.literal(o).should == 'v'
41
+ end
42
+
43
+ it "should support using Range instances as bound variables" do
44
+ @db.bound_variable_arg(1..2, nil).should == "[1,2]"
45
+ end
46
+
47
+ it "should support using PGRange instances as bound variables" do
48
+ @db.bound_variable_arg(@R.new(1, 2), nil).should == "[1,2]"
49
+ end
50
+
51
+ it "should support using arrays of Range instances as bound variables" do
52
+ @db.bound_variable_arg([1..2,2...3], nil).should == '{"[1,2]","[2,3)"}'
53
+ end
54
+
55
+ it "should support using PGRange instances as bound variables" do
56
+ @db.bound_variable_arg([@R.new(1, 2),@R.new(2, 3)], nil).should == '{"[1,2]","[2,3]"}'
57
+ end
58
+
59
+ it "should parse range types from the schema correctly" do
60
+ @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i4', :db_type=>'int4range'}, {:name=>'i8', :db_type=>'int8range'}, {:name=>'n', :db_type=>'numrange'}, {:name=>'d', :db_type=>'daterange'}, {:name=>'ts', :db_type=>'tsrange'}, {:name=>'tz', :db_type=>'tstzrange'}]
61
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :int4range, :int8range, :numrange, :daterange, :tsrange, :tstzrange]
62
+ end
63
+
64
+ it "should parse arrays of range types from the schema correctly" do
65
+ @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i4', :db_type=>'int4range[]'}, {:name=>'i8', :db_type=>'int8range[]'}, {:name=>'n', :db_type=>'numrange[]'}, {:name=>'d', :db_type=>'daterange[]'}, {:name=>'ts', :db_type=>'tsrange[]'}, {:name=>'tz', :db_type=>'tstzrange[]'}]
66
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :int4range_array, :int8range_array, :numrange_array, :daterange_array, :tsrange_array, :tstzrange_array]
67
+ end
68
+
69
+ describe "database typecasting" do
70
+ before do
71
+ @o = @R.new(1, 2, :db_type=>'int4range')
72
+ @o2 = @R.new(1, 2, :db_type=>'int8range')
73
+ @eo = @R.new(nil, nil, :empty=>true, :db_type=>'int4range')
74
+ @eo2 = @R.new(nil, nil, :empty=>true, :db_type=>'int8range')
75
+ end
76
+
77
+ it "should handle multiple range types" do
78
+ %w'int4 int8 num date ts tstz'.each do |i|
79
+ @db.typecast_value(:"#{i}range", @R.new(1, 2, :db_type=>"#{i}range")).should == @R.new(1, 2, :db_type=>"#{i}range")
80
+ end
81
+ end
82
+
83
+ it "should handle multiple array range types" do
84
+ %w'int4 int8 num date ts tstz'.each do |i|
85
+ @db.typecast_value(:"#{i}range_array", [@R.new(1, 2, :db_type=>"#{i}range")]).should be_a_kind_of(Sequel::Postgres::PGArray)
86
+ @db.typecast_value(:"#{i}range_array", [@R.new(1, 2, :db_type=>"#{i}range")]).should == [@R.new(1, 2, :db_type=>"#{i}range")]
87
+ end
88
+ end
89
+
90
+ it "should return PGRange value as is if they have the same subtype" do
91
+ @db.typecast_value(:int4range, @o).should equal(@o)
92
+ end
93
+
94
+ it "should return new PGRange value as is if they have a different subtype" do
95
+ @db.typecast_value(:int8range, @o).should_not equal(@o)
96
+ @db.typecast_value(:int8range, @o).should == @o2
97
+ end
98
+
99
+ it "should return new PGRange value as is if they have a different subtype and value is empty" do
100
+ @db.typecast_value(:int8range, @eo).should == @eo2
101
+ end
102
+
103
+ it "should return new PGRange value if given a Range" do
104
+ @db.typecast_value(:int4range, 1..2).should == @o
105
+ @db.typecast_value(:int4range, 1..2).should_not == @o2
106
+ @db.typecast_value(:int8range, 1..2).should == @o2
107
+ end
108
+
109
+ it "should parse a string argument as the PostgreSQL output format" do
110
+ @db.typecast_value(:int4range, '[1,2]').should == @o
111
+ end
112
+
113
+ it "should raise errors for unparsable formats" do
114
+ proc{@db.typecast_value(:int8range, 'foo')}.should raise_error(Sequel::InvalidValue)
115
+ end
116
+
117
+ it "should raise errors for unhandled values" do
118
+ proc{@db.typecast_value(:int4range, 1)}.should raise_error(Sequel::InvalidValue)
119
+ end
120
+ end
121
+
122
+ it "should support registering custom range types" do
123
+ @R.register('foorange')
124
+ @db.typecast_value(:foorange, 1..2).should be_a_kind_of(@R)
125
+ @db.fetch = [{:name=>'id', :db_type=>'foorange'}]
126
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:foorange]
127
+ end
128
+
129
+ it "should support using a block as a custom conversion proc given as block" do
130
+ @R.register('foo2range'){|s| (s*2).to_i}
131
+ @db.typecast_value(:foo2range, '[1,2]').should == (11..22)
132
+ end
133
+
134
+ it "should support using a block as a custom conversion proc given as :converter option" do
135
+ @R.register('foo3range', :converter=>proc{|s| (s*2).to_i})
136
+ @db.typecast_value(:foo3range, '[1,2]').should == (11..22)
137
+ end
138
+
139
+ it "should support using an existing scaler conversion proc via the :subtype_oid option" do
140
+ @R.register('foo4range', :subtype_oid=>16)
141
+ @db.typecast_value(:foo4range, '[t,f]').should == @R.new(true, false, :db_type=>'foo4range')
142
+ end
143
+
144
+ it "should raise an error if using :subtype_oid option with unexisting scalar conversion proc" do
145
+ proc{@R.register('fooirange', :subtype_oid=>0)}.should raise_error(Sequel::Error)
146
+ end
147
+
148
+ it "should raise an error if using :converter option and a block argument" do
149
+ proc{@R.register('fooirange', :converter=>proc{}){}}.should raise_error(Sequel::Error)
150
+ end
151
+
152
+ it "should raise an error if using :subtype_oid option and a block argument" do
153
+ proc{@R.register('fooirange', :subtype_oid=>16){}}.should raise_error(Sequel::Error)
154
+ end
155
+
156
+ it "should support registering custom types with :oid option" do
157
+ @R.register('foo5range', :oid=>331)
158
+ Sequel::Postgres::PG_TYPES[331].call('[1,3)').should be_a_kind_of(@R)
159
+ end
160
+
161
+ describe "parser" do
162
+ before do
163
+ @p = Sequel::Postgres::PG_TYPES[3904]
164
+ @sp = @R::Parser.new(nil)
165
+ end
166
+
167
+ it "should have db_type method to return the database type string" do
168
+ @p.db_type.should == 'int4range'
169
+ end
170
+
171
+ it "should have converter method which returns a callable used for conversion" do
172
+ @p.converter.call('1').should == 1
173
+ end
174
+
175
+ it "should have call parse input string argument into PGRange instance" do
176
+ @p.call('[1,2]').should == @R.new(1, 2, :db_type=>'int4range')
177
+ end
178
+
179
+ it "should handle empty ranges" do
180
+ @p.call('empty').should == @R.new(nil, nil, :empty=>true, :db_type=>'int4range')
181
+ end
182
+
183
+ it "should handle exclusive beginnings and endings" do
184
+ @p.call('(1,3]').should == @R.new(1, 3, :exclude_begin=>true, :db_type=>'int4range')
185
+ @p.call('[1,3)').should == @R.new(1, 3, :exclude_end=>true, :db_type=>'int4range')
186
+ @p.call('(1,3)').should == @R.new(1, 3, :exclude_begin=>true, :exclude_end=>true, :db_type=>'int4range')
187
+ end
188
+
189
+ it "should handle unbounded beginnings and endings" do
190
+ @p.call('[,2]').should == @R.new(nil, 2, :db_type=>'int4range')
191
+ @p.call('[1,]').should == @R.new(1, nil, :db_type=>'int4range')
192
+ @p.call('[,]').should == @R.new(nil, nil, :db_type=>'int4range')
193
+ end
194
+
195
+ it "should unescape quoted beginnings and endings" do
196
+ @sp.call('["\\\\ \\"","\\" \\\\"]').should == @R.new("\\ \"", "\" \\")
197
+ end
198
+
199
+ it "should treat empty quoted string not as unbounded" do
200
+ @sp.call('["","z"]').should == @R.new("", "z")
201
+ @sp.call('["a",""]').should == @R.new("a", "")
202
+ @sp.call('["",""]').should == @R.new("", "")
203
+ end
204
+ end
205
+
206
+ it "should set appropriate timestamp range conversion procs when getting conversion procs" do
207
+ procs = @db.send(:get_conversion_procs, nil)
208
+ procs[3908].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').should == (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14)))
209
+ procs[3910].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').should == (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14)))
210
+ end
211
+
212
+ it "should set appropriate timestamp range array conversion procs when getting conversion procs" do
213
+ procs = @db.send(:get_conversion_procs, nil)
214
+ procs[3909].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').should == [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)]
215
+ procs[3911].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').should == [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)]
216
+ end
217
+
218
+ describe "a PGRange instance" do
219
+ before do
220
+ @r1 = @R.new(1, 2)
221
+ @r2 = @R.new(3, nil, :exclude_begin=>true, :db_type=>'int4range')
222
+ @r3 = @R.new(nil, 4, :exclude_end=>true, :db_type=>'int8range')
223
+ end
224
+
225
+ it "should have #begin return the beginning of the range" do
226
+ @r1.begin.should == 1
227
+ @r2.begin.should == 3
228
+ @r3.begin.should == nil
229
+ end
230
+
231
+ it "should have #end return the end of the range" do
232
+ @r1.end.should == 2
233
+ @r2.end.should == nil
234
+ @r3.end.should == 4
235
+ end
236
+
237
+ it "should have #db_type return the range's database type" do
238
+ @r1.db_type.should == nil
239
+ @r2.db_type.should == 'int4range'
240
+ @r3.db_type.should == 'int8range'
241
+ end
242
+
243
+ it "should be able to be created by Range#pg_range" do
244
+ (1..2).pg_range.should == @r1
245
+ end
246
+
247
+ it "should have #initialize raise if requesting an empty range with beginning or ending" do
248
+ proc{@R.new(1, nil, :empty=>true)}.should raise_error(Sequel::Error)
249
+ proc{@R.new(nil, 2, :empty=>true)}.should raise_error(Sequel::Error)
250
+ proc{@R.new(nil, nil, :empty=>true, :exclude_begin=>true)}.should raise_error(Sequel::Error)
251
+ proc{@R.new(nil, nil, :empty=>true, :exclude_end=>true)}.should raise_error(Sequel::Error)
252
+ end
253
+
254
+ it "should quack like a range" do
255
+ if RUBY_VERSION >= '1.9'
256
+ @r1.cover?(1.5).should be_true
257
+ @r1.cover?(2.5).should be_false
258
+ @r1.first(1).should == [1]
259
+ @r1.last(1).should == [2]
260
+ end
261
+ @r1.to_a.should == [1, 2]
262
+ @r1.first.should == 1
263
+ @r1.last.should == 2
264
+ a = []
265
+ @r1.step{|x| a << x}
266
+ a.should == [1, 2]
267
+ end
268
+
269
+ it "should only consider PGRanges equal if they have the same db_type" do
270
+ @R.new(1, 2, :db_type=>'int4range').should == @R.new(1, 2, :db_type=>'int4range')
271
+ @R.new(1, 2, :db_type=>'int8range').should_not == @R.new(1, 2, :db_type=>'int4range')
272
+ end
273
+
274
+ it "should only consider empty PGRanges equal with other empty PGRanges" do
275
+ @R.new(nil, nil, :empty=>true).should == @R.new(nil, nil, :empty=>true)
276
+ @R.new(nil, nil, :empty=>true).should_not == @R.new(nil, nil)
277
+ @R.new(nil, nil).should_not == @R.new(nil, nil, :empty=>true)
278
+ end
279
+
280
+ it "should only consider empty PGRanges equal if they have the same bounds" do
281
+ @R.new(1, 2).should == @R.new(1, 2)
282
+ @R.new(1, 2).should_not == @R.new(1, 3)
283
+ end
284
+
285
+ it "should only consider empty PGRanges equal if they have the same bound exclusions" do
286
+ @R.new(1, 2, :exclude_begin=>true).should == @R.new(1, 2, :exclude_begin=>true)
287
+ @R.new(1, 2, :exclude_end=>true).should == @R.new(1, 2, :exclude_end=>true)
288
+ @R.new(1, 2, :exclude_begin=>true).should_not == @R.new(1, 2, :exclude_end=>true)
289
+ @R.new(1, 2, :exclude_end=>true).should_not == @R.new(1, 2, :exclude_begin=>true)
290
+ end
291
+
292
+ it "should consider PGRanges equal with a Range they represent" do
293
+ @R.new(1, 2).should == (1..2)
294
+ @R.new(1, 2, :exclude_end=>true).should == (1...2)
295
+ @R.new(1, 3).should_not == (1..2)
296
+ @R.new(1, 2, :exclude_end=>true).should_not == (1..2)
297
+ end
298
+
299
+ it "should not consider a PGRange equal with a Range if it can't be expressed as a range" do
300
+ @R.new(nil, nil).should_not == (1..2)
301
+ end
302
+
303
+ it "should not consider a PGRange equal to other objects" do
304
+ @R.new(nil, nil).should_not == 1
305
+ end
306
+
307
+ it "should have #=== be true if given an equal PGRange" do
308
+ @R.new(1, 2).should === @R.new(1, 2)
309
+ @R.new(1, 2).should_not === @R.new(1, 3)
310
+
311
+ end
312
+
313
+ it "should have #=== be true if it would be true for the Range represented by the PGRange" do
314
+ @R.new(1, 2).should === 1.5
315
+ @R.new(1, 2).should_not === 2.5
316
+ end
317
+
318
+ it "should have #=== be false if the PGRange cannot be represented by a Range" do
319
+ @R.new(nil, nil).should_not === 1.5
320
+ end
321
+
322
+ it "should have #empty? indicate whether the range is empty" do
323
+ @R.empty.should be_empty
324
+ @R.new(1, 2).should_not be_empty
325
+ end
326
+
327
+ it "should have #exclude_begin? and #exclude_end indicate whether the beginning or ending of the range is excluded" do
328
+ @r1.exclude_begin?.should be_false
329
+ @r1.exclude_end?.should be_false
330
+ @r2.exclude_begin?.should be_true
331
+ @r2.exclude_end?.should be_false
332
+ @r3.exclude_begin?.should be_false
333
+ @r3.exclude_end?.should be_true
334
+ end
335
+
336
+ it "should have #to_range raise an exception if the PGRange cannot be represented by a Range" do
337
+ proc{@R.new(nil, 1).to_range}.should raise_error(Sequel::Error)
338
+ proc{@R.new(1, nil).to_range}.should raise_error(Sequel::Error)
339
+ proc{@R.new(0, 1, :exclude_begin=>true).to_range}.should raise_error(Sequel::Error)
340
+ proc{@R.empty.to_range}.should raise_error(Sequel::Error)
341
+ end
342
+
343
+ it "should have #to_range return the represented range" do
344
+ @r1.to_range.should == (1..2)
345
+ end
346
+
347
+ it "should have #to_range cache the returned value" do
348
+ @r1.to_range.should equal(@r1.to_range)
349
+ end
350
+
351
+ it "should have #unbounded_begin? and #unbounded_end indicate whether the beginning or ending of the range is unbounded" do
352
+ @r1.unbounded_begin?.should be_false
353
+ @r1.unbounded_end?.should be_false
354
+ @r2.unbounded_begin?.should be_false
355
+ @r2.unbounded_end?.should be_true
356
+ @r3.unbounded_begin?.should be_true
357
+ @r3.unbounded_end?.should be_false
358
+ end
359
+
360
+ it "should have #valid_ruby_range? return true if the PGRange can be represented as a Range" do
361
+ @r1.valid_ruby_range?.should be_true
362
+ @R.new(1, 2, :exclude_end=>true).valid_ruby_range?.should be_true
363
+ end
364
+
365
+ it "should have #valid_ruby_range? return false if the PGRange cannot be represented as a Range" do
366
+ @R.new(nil, 1).valid_ruby_range?.should be_false
367
+ @R.new(1, nil).valid_ruby_range?.should be_false
368
+ @R.new(0, 1, :exclude_begin=>true).valid_ruby_range?.should be_false
369
+ @R.empty.valid_ruby_range?.should be_false
370
+ end
371
+ end
372
+ end