require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper") describe "pg_range extension" do before(:all) do Sequel.extension :pg_array, :pg_range @pg_types = Sequel::Postgres::PG_TYPES.dup end after(:all) do Sequel::Postgres::PG_TYPES.replace(@pg_types) end before do @db = Sequel.connect('mock://postgres', :quote_identifiers=>false) @R = Sequel::Postgres::PGRange @db.extend_datasets(Module.new{def supports_timestamp_timezones?; false; end; def supports_timestamp_usecs?; false; end}) @db.extension(:pg_array, :pg_range) end it "should literalize Range instances to strings correctly" do @db.literal(Date.new(2011, 1, 2)...Date.new(2011, 3, 2)).must_equal "'[2011-01-02,2011-03-02)'" @db.literal(Time.local(2011, 1, 2, 10, 20, 30)...Time.local(2011, 2, 3, 10, 20, 30)).must_equal "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'" @db.literal(DateTime.new(2011, 1, 2, 10, 20, 30)...DateTime.new(2011, 2, 3, 10, 20, 30)).must_equal "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'" @db.literal(DateTime.new(2011, 1, 2, 10, 20, 30)...DateTime.new(2011, 2, 3, 10, 20, 30)).must_equal "'[2011-01-02 10:20:30,2011-02-03 10:20:30)'" @db.literal(1..2).must_equal "'[1,2]'" @db.literal(1.0..2.0).must_equal "'[1.0,2.0]'" @db.literal(BigDecimal.new('1.0')..BigDecimal.new('2.0')).must_equal "'[1.0,2.0]'" @db.literal(Sequel.lit('a')..Sequel.lit('z')).must_equal "'[a,z]'" @db.literal(''..'()[]",\\2').must_equal "'[\"\",\\(\\)\\[\\]\\\"\\,\\\\2]'" end it "should literalize PGRange instances to strings correctly" do @db.literal(@R.new(1, 2)).must_equal "'[1,2]'" @db.literal(@R.new(true, false)).must_equal "'[true,false]'" @db.literal(@R.new(1, 2, :exclude_begin=>true)).must_equal "'(1,2]'" @db.literal(@R.new(1, 2, :exclude_end=>true)).must_equal "'[1,2)'" @db.literal(@R.new(nil, 2)).must_equal "'[,2]'" @db.literal(@R.new(1, nil)).must_equal "'[1,]'" @db.literal(@R.new(1, 2, :db_type=>'int8range')).must_equal "'[1,2]'::int8range" @db.literal(@R.new(nil, nil, :empty=>true)).must_equal "'empty'" @db.literal(@R.new("", 2)).must_equal "'[\"\",2]'" end it "should not affect literalization of custom objects" do o = Object.new def o.sql_literal(ds) 'v' end @db.literal(o).must_equal 'v' end it "should support using Range instances as bound variables" do @db.bound_variable_arg(1..2, nil).must_equal "[1,2]" end it "should support using PGRange instances as bound variables" do @db.bound_variable_arg(@R.new(1, 2), nil).must_equal "[1,2]" end it "should support using arrays of Range instances as bound variables" do @db.bound_variable_arg([1..2,2...3], nil).must_equal '{"[1,2]","[2,3)"}' end it "should support using PGRange instances as bound variables" do @db.bound_variable_arg([@R.new(1, 2),@R.new(2, 3)], nil).must_equal '{"[1,2]","[2,3]"}' end it "should parse range types from the schema correctly" do @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'}] @db.schema(:items).map{|e| e[1][:type]}.must_equal [:integer, :int4range, :int8range, :numrange, :daterange, :tsrange, :tstzrange] end it "should parse arrays of range types from the schema correctly" do @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[]'}] @db.schema(:items).map{|e| e[1][:type]}.must_equal [:integer, :int4range_array, :int8range_array, :numrange_array, :daterange_array, :tsrange_array, :tstzrange_array] end describe "database typecasting" do before do @o = @R.new(1, 2, :db_type=>'int4range') @o2 = @R.new(1, 2, :db_type=>'int8range') @eo = @R.new(nil, nil, :empty=>true, :db_type=>'int4range') @eo2 = @R.new(nil, nil, :empty=>true, :db_type=>'int8range') end it "should handle multiple range types" do %w'int4 int8 num date ts tstz'.each do |i| @db.typecast_value(:"#{i}range", @R.new(1, 2, :db_type=>"#{i}range")).must_equal @R.new(1, 2, :db_type=>"#{i}range") end end it "should handle multiple array range types" do %w'int4 int8 num date ts tstz'.each do |i| @db.typecast_value(:"#{i}range_array", [@R.new(1, 2, :db_type=>"#{i}range")]).class.must_equal(Sequel::Postgres::PGArray) @db.typecast_value(:"#{i}range_array", [@R.new(1, 2, :db_type=>"#{i}range")]).must_equal [@R.new(1, 2, :db_type=>"#{i}range")] end end it "should return PGRange value as is if they have the same subtype" do @db.typecast_value(:int4range, @o).must_be_same_as(@o) end it "should return new PGRange value as is if they have a different subtype" do @db.typecast_value(:int8range, @o).wont_be_same_as(@o) @db.typecast_value(:int8range, @o).must_equal @o2 end it "should return new PGRange value as is if they have a different subtype and value is empty" do @db.typecast_value(:int8range, @eo).must_equal @eo2 end it "should return new PGRange value if given a Range" do @db.typecast_value(:int4range, 1..2).must_equal @o @db.typecast_value(:int4range, 1..2).wont_equal @o2 @db.typecast_value(:int8range, 1..2).must_equal @o2 end it "should parse a string argument as the PostgreSQL output format" do @db.typecast_value(:int4range, '[1,2]').must_equal @o end it "should raise errors for unparsable formats" do proc{@db.typecast_value(:int8range, 'foo')}.must_raise(Sequel::InvalidValue) end it "should raise errors for unhandled values" do proc{@db.typecast_value(:int4range, 1)}.must_raise(Sequel::InvalidValue) end end it "should support registering custom range types" do @R.register('foorange') @db.typecast_value(:foorange, 1..2).must_be_kind_of(@R) @db.fetch = [{:name=>'id', :db_type=>'foorange'}] @db.schema(:items).map{|e| e[1][:type]}.must_equal [:foorange] end it "should support using a block as a custom conversion proc given as block" do @R.register('foo2range'){|s| (s*2).to_i} @db.typecast_value(:foo2range, '[1,2]').must_be :==, (11..22) end it "should support using a block as a custom conversion proc given as :converter option" do @R.register('foo3range', :converter=>proc{|s| (s*2).to_i}) @db.typecast_value(:foo3range, '[1,2]').must_be :==, (11..22) end it "should support using an existing scaler conversion proc via the :subtype_oid option" do @R.register('foo4range', :subtype_oid=>16) @db.typecast_value(:foo4range, '[t,f]').must_equal @R.new(true, false, :db_type=>'foo4range') end it "should raise an error if using :subtype_oid option with unexisting scalar conversion proc" do proc{@R.register('fooirange', :subtype_oid=>0)}.must_raise(Sequel::Error) end it "should raise an error if using :converter option and a block argument" do proc{@R.register('fooirange', :converter=>proc{}){}}.must_raise(Sequel::Error) end it "should raise an error if using :subtype_oid option and a block argument" do proc{@R.register('fooirange', :subtype_oid=>16){}}.must_raise(Sequel::Error) end it "should support registering custom types with :oid option" do @R.register('foo5range', :oid=>331) Sequel::Postgres::PG_TYPES[331].call('[1,3)').must_be_kind_of(@R) end it "should return correct results for Database#schema_type_class" do @db.schema_type_class(:int4range).must_equal Sequel::Postgres::PGRange @db.schema_type_class(:integer).must_equal Integer end describe "parser" do before do @p = Sequel::Postgres::PG_TYPES[3904] @sp = @R::Parser.new(nil) end it "should have db_type method to return the database type string" do @p.db_type.must_equal 'int4range' end it "should have converter method which returns a callable used for conversion" do @p.converter.call('1').must_equal 1 end it "should have call parse input string argument into PGRange instance" do @p.call('[1,2]').must_equal @R.new(1, 2, :db_type=>'int4range') end it "should handle empty ranges" do @p.call('empty').must_equal @R.new(nil, nil, :empty=>true, :db_type=>'int4range') end it "should handle exclusive beginnings and endings" do @p.call('(1,3]').must_equal @R.new(1, 3, :exclude_begin=>true, :db_type=>'int4range') @p.call('[1,3)').must_equal @R.new(1, 3, :exclude_end=>true, :db_type=>'int4range') @p.call('(1,3)').must_equal @R.new(1, 3, :exclude_begin=>true, :exclude_end=>true, :db_type=>'int4range') end it "should handle unbounded beginnings and endings" do @p.call('[,2]').must_equal @R.new(nil, 2, :db_type=>'int4range') @p.call('[1,]').must_equal @R.new(1, nil, :db_type=>'int4range') @p.call('[,]').must_equal @R.new(nil, nil, :db_type=>'int4range') end it "should unescape quoted beginnings and endings" do @sp.call('["\\\\ \\"","\\" \\\\"]').must_equal @R.new("\\ \"", "\" \\") end it "should treat empty quoted string not as unbounded" do @sp.call('["","z"]').must_equal @R.new("", "z") @sp.call('["a",""]').must_equal @R.new("a", "") @sp.call('["",""]').must_equal @R.new("", "") end end it "should set appropriate timestamp range conversion procs when resetting conversion procs" do @db.reset_conversion_procs procs = @db.conversion_procs procs[3908].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').must_be :==, (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14))) procs[3910].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').must_be :==, (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14))) end it "should set appropriate timestamp range array conversion procs when resetting conversion procs" do @db.reset_conversion_procs procs = @db.conversion_procs procs[3909].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').must_be :==, [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)] procs[3911].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').must_be :==, [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)] end describe "a PGRange instance" do before do @r1 = @R.new(1, 2) @r2 = @R.new(3, nil, :exclude_begin=>true, :db_type=>'int4range') @r3 = @R.new(nil, 4, :exclude_end=>true, :db_type=>'int8range') end it "should have #begin return the beginning of the range" do @r1.begin.must_equal 1 @r2.begin.must_equal 3 @r3.begin.must_equal nil end it "should have #end return the end of the range" do @r1.end.must_equal 2 @r2.end.must_equal nil @r3.end.must_equal 4 end it "should have #db_type return the range's database type" do @r1.db_type.must_equal nil @r2.db_type.must_equal 'int4range' @r3.db_type.must_equal 'int8range' end it "should be able to be created by Sequel.pg_range" do Sequel.pg_range(1..2).must_equal @r1 end it "should have Sequel.pg_range be able to take a database type" do Sequel.pg_range(1..2, :int4range).must_equal @R.new(1, 2, :db_type=>:int4range) end it "should have Sequel.pg_range return a PGRange as is" do a = Sequel.pg_range(1..2) Sequel.pg_range(a).must_be_same_as(a) end it "should have Sequel.pg_range return a new PGRange if the database type differs" do a = Sequel.pg_range(1..2, :int4range) b = Sequel.pg_range(a, :int8range) a.to_range.must_equal b.to_range a.wont_be_same_as(b) a.db_type.must_equal :int4range b.db_type.must_equal :int8range end it "should have #initialize raise if requesting an empty range with beginning or ending" do proc{@R.new(1, nil, :empty=>true)}.must_raise(Sequel::Error) proc{@R.new(nil, 2, :empty=>true)}.must_raise(Sequel::Error) proc{@R.new(nil, nil, :empty=>true, :exclude_begin=>true)}.must_raise(Sequel::Error) proc{@R.new(nil, nil, :empty=>true, :exclude_end=>true)}.must_raise(Sequel::Error) end it "should quack like a range" do if RUBY_VERSION >= '1.9' @r1.cover?(1.5).must_equal true @r1.cover?(2.5).must_equal false @r1.first(1).must_equal [1] @r1.last(1).must_equal [2] end @r1.to_a.must_equal [1, 2] @r1.first.must_equal 1 @r1.last.must_equal 2 a = [] @r1.step{|x| a << x} a.must_equal [1, 2] end it "should only consider PGRanges equal if they have the same db_type" do @R.new(1, 2, :db_type=>'int4range').must_equal @R.new(1, 2, :db_type=>'int4range') @R.new(1, 2, :db_type=>'int8range').wont_equal @R.new(1, 2, :db_type=>'int4range') end it "should only consider empty PGRanges equal with other empty PGRanges" do @R.new(nil, nil, :empty=>true).must_equal @R.new(nil, nil, :empty=>true) @R.new(nil, nil, :empty=>true).wont_equal @R.new(nil, nil) @R.new(nil, nil).wont_equal @R.new(nil, nil, :empty=>true) end it "should only consider empty PGRanges equal if they have the same bounds" do @R.new(1, 2).must_equal @R.new(1, 2) @R.new(1, 2).wont_equal @R.new(1, 3) end it "should only consider empty PGRanges equal if they have the same bound exclusions" do @R.new(1, 2, :exclude_begin=>true).must_equal @R.new(1, 2, :exclude_begin=>true) @R.new(1, 2, :exclude_end=>true).must_equal @R.new(1, 2, :exclude_end=>true) @R.new(1, 2, :exclude_begin=>true).wont_equal @R.new(1, 2, :exclude_end=>true) @R.new(1, 2, :exclude_end=>true).wont_equal @R.new(1, 2, :exclude_begin=>true) end it "should consider PGRanges equal with a Range they represent" do @R.new(1, 2).must_be :==, (1..2) @R.new(1, 2, :exclude_end=>true).must_be :==, (1...2) @R.new(1, 3).wont_be :==, (1..2) @R.new(1, 2, :exclude_end=>true).wont_be :==, (1..2) end it "should not consider a PGRange equal with a Range if it can't be expressed as a range" do @R.new(nil, nil).wont_be :==, (1..2) end it "should not consider a PGRange equal to other objects" do @R.new(nil, nil).wont_equal 1 end it "should have #=== be true if given an equal PGRange" do @R.new(1, 2).must_be :===, @R.new(1, 2) @R.new(1, 2).wont_be :===, @R.new(1, 3) end it "should have #=== be true if it would be true for the Range represented by the PGRange" do @R.new(1, 2).must_be :===, 1.5 @R.new(1, 2).wont_be :===, 2.5 end it "should have #=== be false if the PGRange cannot be represented by a Range" do @R.new(nil, nil).wont_be :===, 1.5 end it "should have #empty? indicate whether the range is empty" do @R.empty.must_be :empty? @R.new(1, 2).wont_be :empty? end it "should have #exclude_begin? and #exclude_end indicate whether the beginning or ending of the range is excluded" do @r1.exclude_begin?.must_equal false @r1.exclude_end?.must_equal false @r2.exclude_begin?.must_equal true @r2.exclude_end?.must_equal false @r3.exclude_begin?.must_equal false @r3.exclude_end?.must_equal true end it "should have #to_range raise an exception if the PGRange cannot be represented by a Range" do proc{@R.new(nil, 1).to_range}.must_raise(Sequel::Error) proc{@R.new(1, nil).to_range}.must_raise(Sequel::Error) proc{@R.new(0, 1, :exclude_begin=>true).to_range}.must_raise(Sequel::Error) proc{@R.empty.to_range}.must_raise(Sequel::Error) end it "should have #to_range return the represented range" do @r1.to_range.must_be :==, (1..2) end it "should have #to_range cache the returned value" do @r1.to_range.must_be_same_as(@r1.to_range) end it "should have #unbounded_begin? and #unbounded_end indicate whether the beginning or ending of the range is unbounded" do @r1.unbounded_begin?.must_equal false @r1.unbounded_end?.must_equal false @r2.unbounded_begin?.must_equal false @r2.unbounded_end?.must_equal true @r3.unbounded_begin?.must_equal true @r3.unbounded_end?.must_equal false end it "should have #valid_ruby_range? return true if the PGRange can be represented as a Range" do @r1.valid_ruby_range?.must_equal true @R.new(1, 2, :exclude_end=>true).valid_ruby_range?.must_equal true end it "should have #valid_ruby_range? return false if the PGRange cannot be represented as a Range" do @R.new(nil, 1).valid_ruby_range?.must_equal false @R.new(1, nil).valid_ruby_range?.must_equal false @R.new(0, 1, :exclude_begin=>true).valid_ruby_range?.must_equal false @R.empty.valid_ruby_range?.must_equal false end end end