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')
    @R = Sequel::Postgres::PGRange
    @db.extend_datasets do
      def supports_timestamp_timezones?; false end
      def supports_timestamp_usecs?; false end
      def quote_identifiers?; false end
    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 "int8range(1,2,'[]')"
    @db.literal(@R.new(nil, nil, :empty=>true)).must_equal "'empty'"
    @db.literal(@R.new(nil, nil, :empty=>true, :db_type=>'int8range')).must_equal "'empty'::int8range"
    @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 not support registering custom range types on a per-Database basis for frozen databases" do
    @db.freeze
    proc{@db.register_range_type('banana', :oid=>7865){|s| s}}.must_raise RuntimeError, TypeError
  end

  it "should support registering custom range types on a per-Database basis" do
    @db.register_range_type('banana', :oid=>7865){|s| s}
    @db.typecast_value(:banana, '[1,2]').class.must_equal(Sequel::Postgres::PGRange)
    @db.fetch = [{:name=>'id', :db_type=>'banana'}]
    @db.schema(:items).map{|e| e[1][:type]}.must_equal [:banana]
    @db.conversion_procs.must_include(7865)
    @db.respond_to?(:typecast_value_banana, true).must_equal true

    db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
    db.extend_datasets(Module.new{def supports_timestamp_timezones?; false; end; def supports_timestamp_usecs?; false; end})
    db.extension(:pg_range)
    db.fetch = [{:name=>'id', :db_type=>'banana'}]
    db.schema(:items).map{|e| e[1][:type]}.must_equal [nil]
    db.conversion_procs.wont_include(7865)
    db.respond_to?(:typecast_value_banana, true).must_equal false
  end

  it "should automatically look up the range and subtype oids when registering per-Database types" do
    @db.fetch = [[{:rngsubtype=>21, :rngtypid=>7866}], [{:name=>'id', :db_type=>'banana'}]]
    @db.register_range_type('banana', :subtype_typecast=>:integer)
    @db.sqls.must_equal ["SELECT rngtypid, rngsubtype FROM pg_range INNER JOIN pg_type ON (pg_type.oid = pg_range.rngtypid) WHERE (typname = 'banana') LIMIT 1"]
    @db.schema(:items).map{|e| e[1][:type]}.must_equal [:banana]
    @db.conversion_procs[7866].call("[1,3)").must_be :==, (1...3)
    @db.typecast_value(:banana, '[1,2]').must_be :==, (1..2)
  end

  it "should not automatically look up oids if given both subtype and range oids" do
    @db.register_range_type('banana', :oid=>7866, :subtype_oid=>21)
    @db.sqls.must_equal []
    @db.conversion_procs[7866].call("[1,3)").must_be :==, (1...3)
    @db.typecast_value(:banana, '[1,2]').must_be :==, (1..2)
  end

  it "should not automatically look up oids if given range oid and block" do
    @db.register_range_type('banana', :oid=>7866){|s| s.to_i}
    @db.sqls.must_equal []
    @db.conversion_procs[7866].call("[1,3)").must_be :==, (1...3)
    @db.typecast_value(:banana, '[1,2]').must_be :==, (1..2)
  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_be_nil
    end

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

    it "should have #db_type return the range's database type" do
      @r1.db_type.must_be_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
      @r1.cover?(1.5).must_equal true
      @r1.cover?(2.5).must_equal false
      if RUBY_VERSION >= '1.9'
        @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 have cover? handle empty, unbounded, and exclusive beginning ranges" do
      @R.empty.cover?(1).must_equal false

      r = @R.new(1, nil)
      r.cover?(0).must_equal false
      r.cover?(1).must_equal true
      r.cover?(2).must_equal true
      r.cover?(3).must_equal true

      r = @R.new(nil, 2)
      r.cover?(0).must_equal true
      r.cover?(1).must_equal true
      r.cover?(2).must_equal true
      r.cover?(3).must_equal false

      r = @R.new(1, 2, :exclude_begin=>true)
      r.cover?(0).must_equal false
      r.cover?(1).must_equal false
      r.cover?(2).must_equal true
      r.cover?(3).must_equal false

      r = @R.new(1, 2, :exclude_end=>true)
      r.cover?(0).must_equal false
      r.cover?(1).must_equal true
      r.cover?(2).must_equal false
      r.cover?(3).must_equal false
    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
