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)).should == "'[2011-01-02,2011-03-02)'"
    @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)'"
    @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)'"
    @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)'"
    @db.literal(1..2).should == "'[1,2]'"
    @db.literal(1.0..2.0).should == "'[1.0,2.0]'"
    @db.literal(BigDecimal.new('1.0')..BigDecimal.new('2.0')).should == "'[1.0,2.0]'"
    @db.literal(Sequel.lit('a')..Sequel.lit('z')).should == "'[a,z]'"
    @db.literal(''..'()[]",\\2').should == "'[\"\",\\(\\)\\[\\]\\\"\\,\\\\2]'"
  end

  it "should literalize PGRange instances to strings correctly" do
    @db.literal(@R.new(1, 2)).should == "'[1,2]'"
    @db.literal(@R.new(true, false)).should == "'[true,false]'"
    @db.literal(@R.new(1, 2, :exclude_begin=>true)).should == "'(1,2]'"
    @db.literal(@R.new(1, 2, :exclude_end=>true)).should == "'[1,2)'"
    @db.literal(@R.new(nil, 2)).should == "'[,2]'"
    @db.literal(@R.new(1, nil)).should == "'[1,]'"
    @db.literal(@R.new(1, 2, :db_type=>'int8range')).should == "'[1,2]'::int8range"
    @db.literal(@R.new(nil, nil, :empty=>true)).should == "'empty'"
    @db.literal(@R.new("", 2)).should == "'[\"\",2]'"
  end

  it "should not affect literalization of custom objects" do
    o = Object.new
    def o.sql_literal(ds) 'v' end
    @db.literal(o).should == 'v'
  end

  it "should support using Range instances as bound variables" do
    @db.bound_variable_arg(1..2, nil).should == "[1,2]"
  end

  it "should support using PGRange instances as bound variables" do
    @db.bound_variable_arg(@R.new(1, 2), nil).should == "[1,2]"
  end

  it "should support using arrays of Range instances as bound variables" do
    @db.bound_variable_arg([1..2,2...3], nil).should == '{"[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).should == '{"[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]}.should == [: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]}.should == [: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")).should == @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")]).should be_a_kind_of(Sequel::Postgres::PGArray)
        @db.typecast_value(:"#{i}range_array", [@R.new(1, 2, :db_type=>"#{i}range")]).should == [@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).should equal(@o)
    end

    it "should return new PGRange value as is if they have a different subtype" do
      @db.typecast_value(:int8range, @o).should_not equal(@o)
      @db.typecast_value(:int8range, @o).should == @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).should == @eo2
    end

    it "should return new PGRange value if given a Range" do
      @db.typecast_value(:int4range, 1..2).should == @o
      @db.typecast_value(:int4range, 1..2).should_not == @o2
      @db.typecast_value(:int8range, 1..2).should == @o2
    end

    it "should parse a string argument as the PostgreSQL output format" do
      @db.typecast_value(:int4range, '[1,2]').should == @o
    end

    it "should raise errors for unparsable formats" do
      proc{@db.typecast_value(:int8range, 'foo')}.should raise_error(Sequel::InvalidValue)
    end

    it "should raise errors for unhandled values" do
      proc{@db.typecast_value(:int4range, 1)}.should raise_error(Sequel::InvalidValue)
    end
  end

  it "should support registering custom range types" do
    @R.register('foorange')
    @db.typecast_value(:foorange, 1..2).should be_a_kind_of(@R)
    @db.fetch = [{:name=>'id', :db_type=>'foorange'}]
    @db.schema(:items).map{|e| e[1][:type]}.should == [: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]').should == (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]').should == (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]').should == @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)}.should raise_error(Sequel::Error)
  end

  it "should raise an error if using :converter option and a block argument" do
    proc{@R.register('fooirange', :converter=>proc{}){}}.should raise_error(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){}}.should raise_error(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)').should be_a_kind_of(@R)
  end

  it "should return correct results for Database#schema_type_class" do
    @db.schema_type_class(:int4range).should == Sequel::Postgres::PGRange
    @db.schema_type_class(:integer).should == 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.should == 'int4range'
    end

    it "should have converter method which returns a callable used for conversion" do
      @p.converter.call('1').should == 1
    end

    it "should have call parse input string argument into PGRange instance" do
      @p.call('[1,2]').should == @R.new(1, 2, :db_type=>'int4range')
    end

    it "should handle empty ranges" do
      @p.call('empty').should == @R.new(nil, nil, :empty=>true, :db_type=>'int4range')
    end

    it "should handle exclusive beginnings and endings" do
      @p.call('(1,3]').should == @R.new(1, 3, :exclude_begin=>true, :db_type=>'int4range')
      @p.call('[1,3)').should == @R.new(1, 3, :exclude_end=>true, :db_type=>'int4range')
      @p.call('(1,3)').should == @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]').should == @R.new(nil, 2, :db_type=>'int4range')
      @p.call('[1,]').should == @R.new(1, nil, :db_type=>'int4range')
      @p.call('[,]').should == @R.new(nil, nil, :db_type=>'int4range')
    end

    it "should unescape quoted beginnings and endings" do
      @sp.call('["\\\\ \\"","\\" \\\\"]').should == @R.new("\\ \"", "\" \\")
    end

    it "should treat empty quoted string not as unbounded" do
      @sp.call('["","z"]').should == @R.new("", "z")
      @sp.call('["a",""]').should == @R.new("a", "")
      @sp.call('["",""]').should == @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]').should == (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]').should == (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]"}').should == [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]"}').should == [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.should == 1
      @r2.begin.should == 3
      @r3.begin.should == nil
    end

    it "should have #end return the end of the range" do
      @r1.end.should == 2
      @r2.end.should == nil
      @r3.end.should == 4
    end

    it "should have #db_type return the range's database type" do
      @r1.db_type.should == nil
      @r2.db_type.should == 'int4range'
      @r3.db_type.should == 'int8range'
    end

    it "should be able to be created by Sequel.pg_range" do
      Sequel.pg_range(1..2).should == @r1
    end

    it "should have Sequel.pg_range be able to take a database type" do
      Sequel.pg_range(1..2, :int4range).should == @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).should equal(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.should == b.to_range
      a.should_not equal(b)
      a.db_type.should == :int4range
      b.db_type.should == :int8range
    end

    it "should have #initialize raise if requesting an empty range with beginning or ending" do
      proc{@R.new(1, nil, :empty=>true)}.should raise_error(Sequel::Error)
      proc{@R.new(nil, 2, :empty=>true)}.should raise_error(Sequel::Error)
      proc{@R.new(nil, nil, :empty=>true, :exclude_begin=>true)}.should raise_error(Sequel::Error)
      proc{@R.new(nil, nil, :empty=>true, :exclude_end=>true)}.should raise_error(Sequel::Error)
    end

    it "should quack like a range" do
      if RUBY_VERSION >= '1.9'
        @r1.cover?(1.5).should be_true
        @r1.cover?(2.5).should be_false
        @r1.first(1).should == [1]
        @r1.last(1).should == [2]
      end
      @r1.to_a.should == [1, 2]
      @r1.first.should == 1
      @r1.last.should == 2
      a = []
      @r1.step{|x| a << x}
      a.should == [1, 2]
    end

    it "should only consider PGRanges equal if they have the same db_type" do
      @R.new(1, 2, :db_type=>'int4range').should == @R.new(1, 2, :db_type=>'int4range')
      @R.new(1, 2, :db_type=>'int8range').should_not == @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).should == @R.new(nil, nil, :empty=>true)
      @R.new(nil, nil, :empty=>true).should_not == @R.new(nil, nil)
      @R.new(nil, nil).should_not == @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).should == @R.new(1, 2)
      @R.new(1, 2).should_not == @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).should == @R.new(1, 2, :exclude_begin=>true)
      @R.new(1, 2, :exclude_end=>true).should == @R.new(1, 2, :exclude_end=>true)
      @R.new(1, 2, :exclude_begin=>true).should_not == @R.new(1, 2, :exclude_end=>true)
      @R.new(1, 2, :exclude_end=>true).should_not == @R.new(1, 2, :exclude_begin=>true)
    end

    it "should consider PGRanges equal with a Range they represent" do
      @R.new(1, 2).should == (1..2)
      @R.new(1, 2, :exclude_end=>true).should == (1...2)
      @R.new(1, 3).should_not == (1..2)
      @R.new(1, 2, :exclude_end=>true).should_not == (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).should_not == (1..2)
    end

    it "should not consider a PGRange equal to other objects" do
      @R.new(nil, nil).should_not == 1
    end

    it "should have #=== be true if given an equal PGRange" do
      @R.new(1, 2).should === @R.new(1, 2)
      @R.new(1, 2).should_not === @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).should === 1.5
      @R.new(1, 2).should_not === 2.5
    end

    it "should have #=== be false if the PGRange cannot be represented by a Range" do
      @R.new(nil, nil).should_not === 1.5
    end

    it "should have #empty? indicate whether the range is empty" do
      @R.empty.should be_empty
      @R.new(1, 2).should_not 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?.should be_false
      @r1.exclude_end?.should be_false
      @r2.exclude_begin?.should be_true
      @r2.exclude_end?.should be_false
      @r3.exclude_begin?.should be_false
      @r3.exclude_end?.should be_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}.should raise_error(Sequel::Error)
      proc{@R.new(1, nil).to_range}.should raise_error(Sequel::Error)
      proc{@R.new(0, 1, :exclude_begin=>true).to_range}.should raise_error(Sequel::Error)
      proc{@R.empty.to_range}.should raise_error(Sequel::Error)
    end

    it "should have #to_range return the represented range" do
      @r1.to_range.should == (1..2)
    end

    it "should have #to_range cache the returned value" do
      @r1.to_range.should equal(@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?.should be_false
      @r1.unbounded_end?.should be_false
      @r2.unbounded_begin?.should be_false
      @r2.unbounded_end?.should be_true
      @r3.unbounded_begin?.should be_true
      @r3.unbounded_end?.should be_false
    end

    it "should have #valid_ruby_range? return true if the PGRange can be represented as a Range" do
      @r1.valid_ruby_range?.should be_true
      @R.new(1, 2, :exclude_end=>true).valid_ruby_range?.should be_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?.should be_false
      @R.new(1, nil).valid_ruby_range?.should be_false
      @R.new(0, 1, :exclude_begin=>true).valid_ruby_range?.should be_false
      @R.empty.valid_ruby_range?.should be_false
    end
  end
end
