require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")

describe "pg_row extension" do
  before do
    @db = Sequel.connect('mock://postgres')
    @db.extend_datasets{def quote_identifiers?; false end}
    @db.extension(:pg_array, :pg_row)
    @m = Sequel::Postgres::PGRow
    @db.sqls
  end

  it "should parse record objects as arrays" do
    a = Sequel::Postgres::PG_TYPES[2249].call("(a,b,c)")
    a.class.must_equal(@m::ArrayRow)
    a.to_a.must_be_kind_of(Array)
    a[0].must_equal 'a'
    a.must_equal %w'a b c'
    a.db_type.must_be_nil
    @db.literal(a).must_equal "ROW('a', 'b', 'c')"
  end

  it "should parse arrays of record objects as arrays of arrays" do
    as = Sequel::Postgres::PG_TYPES[2287].call('{"(a,b,c)","(d,e,f)"}')
    as.must_equal [%w'a b c', %w'd e f']
    as.each do |a|
      a.class.must_equal(@m::ArrayRow)
      a.to_a.must_be_kind_of(Array)
      a.db_type.must_be_nil
    end
    @db.literal(as).must_equal "ARRAY[ROW('a', 'b', 'c'),ROW('d', 'e', 'f')]::record[]"
  end

  it "should be able to register custom parsing of row types as array-like objects" do
    klass = @m::ArrayRow.subclass(:foo)
    parser = @m::Parser.new(:converter=>klass)
    a = parser.call("(a,b,c)")
    a.class.must_equal(klass)
    a.to_a.must_be_kind_of(Array)
    a[0].must_equal 'a'
    a.must_equal %w'a b c'
    a.db_type.must_equal :foo
    @db.literal(a).must_equal "ROW('a', 'b', 'c')::foo"
  end

  it "should be able to register custom parsing of row types as hash-like objects" do
    klass = @m::HashRow.subclass(:foo, [:a, :b, :c])
    parser = @m::Parser.new(:converter=>klass, :columns=>[:a, :b, :c])
    a = parser.call("(a,b,c)")
    a.class.must_equal(klass)
    a.to_hash.must_be_kind_of(Hash)
    a[:a].must_equal 'a'
    a.must_equal(:a=>'a', :b=>'b', :c=>'c')
    a.db_type.must_equal :foo
    a.columns.must_equal [:a, :b, :c]
    @db.literal(a).must_equal "ROW('a', 'b', 'c')::foo"
  end

  it "should raise an error if attempting to literalize a HashRow without column information" do
    h = @m::HashRow.call(:a=>'a', :b=>'b', :c=>'c')
    proc{@db.literal(h)}.must_raise(Sequel::Error)
  end

  it "should be able to manually override db_type per ArrayRow instance" do
    a = @m::ArrayRow.call(%w'a b c')
    a.db_type = :foo
    @db.literal(a).must_equal "ROW('a', 'b', 'c')::foo"
  end

  it "should be able to manually override db_type and columns per HashRow instance" do
    h = @m::HashRow.call(:a=>'a', :c=>'c', :b=>'b')
    h.db_type = :foo
    h.columns = [:a, :b, :c]
    @db.literal(h).must_equal "ROW('a', 'b', 'c')::foo"
  end

  it "should correctly split an empty row" do
    @m::Splitter.new("()").parse.must_equal [nil]
  end

  it "should correctly split a row with a single value" do
    @m::Splitter.new("(1)").parse.must_equal %w'1'
  end

  it "should correctly split a row with multiple values" do
    @m::Splitter.new("(1,2)").parse.must_equal %w'1 2'
  end

  it "should correctly NULL values when splitting" do
    @m::Splitter.new("(1,)").parse.must_equal ['1', nil]
  end

  it "should correctly empty string values when splitting" do
    @m::Splitter.new('(1,"")').parse.must_equal ['1', '']
  end

  it "should handle quoted values when splitting" do
    @m::Splitter.new('("1","2")').parse.must_equal %w'1 2'
  end

  it "should handle escaped backslashes in quoted values when splitting" do
    @m::Splitter.new('("\\\\1","2\\\\")').parse.must_equal ['\\1', '2\\']
  end

  it "should handle doubled quotes in quoted values when splitting" do
    @m::Splitter.new('("""1","2""")').parse.must_equal ['"1', '2"']
  end

  it "should correctly convert types when parsing into an array" do
    @m::Parser.new(:column_converters=>[proc{|s| s*2}, proc{|s| s*3}, proc{|s| s*4}]).call("(a,b,c)").must_equal %w'aa bbb cccc'
  end

  it "should correctly convert types into hashes if columns are known" do
    @m::Parser.new(:columns=>[:a, :b, :c]).call("(a,b,c)").must_equal(:a=>'a', :b=>'b', :c=>'c')
  end

  it "should correctly handle type conversion when converting into hashes" do
    @m::Parser.new(:column_converters=>[proc{|s| s*2}, proc{|s| s*3}, proc{|s| s*4}], :columns=>[:a, :b, :c]).call("(a,b,c)").must_equal(:a=>'aa', :b=>'bbb', :c=>'cccc')
  end

  it "should correctly wrap arrays when converting" do
    @m::Parser.new(:converter=>proc{|s| [:foo, s]}).call("(a,b,c)").must_equal [:foo, %w'a b c']
  end

  it "should correctly wrap hashes when converting" do
    @m::Parser.new(:converter=>proc{|s| [:foo, s]}, :columns=>[:a, :b, :c]).call("(a,b,c)").must_equal [:foo, {:a=>'a', :b=>'b', :c=>'c'}]
  end

  it "should have parser store reflection information" do
    p = @m::Parser.new(:oid=>1, :column_oids=>[2], :columns=>[:a], :converter=>Array, :typecaster=>Hash, :column_converters=>[Array])
    p.oid.must_equal 1
    p.column_oids.must_equal [2]
    p.columns.must_equal [:a]
    p.converter.must_equal Array
    p.typecaster.must_equal Hash
    p.column_converters.must_equal [Array]
  end

  it "should reload registered row types when reseting conversion procs" do
    db = Sequel.mock(:host=>'postgres')
    db.extend_datasets{def quote_identifiers?; false end}
    db.extension(:pg_row)
    db.conversion_procs[4] = proc{|s| s.to_i}
    db.conversion_procs[5] = proc{|s| s * 2}
    db.sqls
    db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    db.register_row_type(:foo)
    db.sqls.must_equal ["SELECT pg_type.oid, typrelid, typarray FROM pg_type WHERE ((typtype = 'c') AND (typname = 'foo')) LIMIT 1",
      "SELECT attname, (CASE pg_type.typbasetype WHEN 0 THEN atttypid ELSE pg_type.typbasetype END) AS atttypid FROM pg_attribute INNER JOIN pg_type ON (pg_type.oid = pg_attribute.atttypid) WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]

    begin
      pgnt = Sequel::Postgres::PG_NAMED_TYPES.dup
      Sequel::Postgres::PG_NAMED_TYPES.clear
      db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
      db.reset_conversion_procs
      db.sqls.must_equal ["SELECT pg_type.oid, typrelid, typarray FROM pg_type WHERE ((typtype = 'c') AND (typname = 'foo')) LIMIT 1",
        "SELECT attname, (CASE pg_type.typbasetype WHEN 0 THEN atttypid ELSE pg_type.typbasetype END) AS atttypid FROM pg_attribute INNER JOIN pg_type ON (pg_type.oid = pg_attribute.atttypid) WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
    ensure
      Sequel::Postgres::PG_NAMED_TYPES.replace pgnt
    end
  end

  it "should handle ArrayRows and HashRows in bound variables" do
    @db.bound_variable_arg(1, nil).must_equal 1
    @db.bound_variable_arg(@m::ArrayRow.call(["1", "abc\\'\","]), nil).must_equal '("1","abc\\\\\'\\",")'
    @db.bound_variable_arg(@m::HashRow.subclass(nil, [:a, :b]).call(:a=>"1", :b=>"abc\\'\","), nil).must_equal '("1","abc\\\\\'\\",")'
  end

  it "should handle ArrayRows and HashRows in arrays in bound variables" do
    @db.bound_variable_arg(1, nil).must_equal 1
    @db.bound_variable_arg([@m::ArrayRow.call(["1", "abc\\'\","])], nil).must_equal '{"(\\"1\\",\\"abc\\\\\\\\\'\\\\\\",\\")"}'
    @db.bound_variable_arg([@m::HashRow.subclass(nil, [:a, :b]).call(:a=>"1", :b=>"abc\\'\",")], nil).must_equal '{"(\\"1\\",\\"abc\\\\\\\\\'\\\\\\",\\")"}'
  end

  it "should handle nils in bound variables" do
    @db.bound_variable_arg(@m::ArrayRow.call([nil, nil]), nil).must_equal '(,)'
    @db.bound_variable_arg(@m::HashRow.subclass(nil, [:a, :b]).call(:a=>nil, :b=>nil), nil).must_equal '(,)'
    @db.bound_variable_arg([@m::ArrayRow.call([nil, nil])], nil).must_equal '{"(,)"}'
    @db.bound_variable_arg([@m::HashRow.subclass(nil, [:a, :b]).call(:a=>nil, :b=>nil)], nil).must_equal '{"(,)"}'
  end
  
  it "should allow registering row type parsers by introspecting system tables" do
    @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
    @db.conversion_procs[5] = p5 = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo)
    @db.sqls.must_equal ["SELECT pg_type.oid, typrelid, typarray FROM pg_type WHERE ((typtype = 'c') AND (typname = 'foo')) LIMIT 1",
      "SELECT attname, (CASE pg_type.typbasetype WHEN 0 THEN atttypid ELSE pg_type.typbasetype END) AS atttypid FROM pg_attribute INNER JOIN pg_type ON (pg_type.oid = pg_attribute.atttypid) WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
    p1 = @db.conversion_procs[1]
    p1.columns.must_equal [:bar, :baz]
    p1.column_oids.must_equal [4, 5]
    p1.column_converters.must_equal [p4, p5]
    p1.oid.must_equal 1
    @db.send(:schema_column_type, 'foo').must_equal :pg_row_foo
    @db.send(:schema_column_type, 'integer').must_equal :integer

    c = p1.converter
    c.superclass.must_equal @m::HashRow
    c.columns.must_equal [:bar, :baz]
    c.db_type.must_equal :foo
    p1.typecaster.must_equal c

    p1.call('(1,b)').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo, %w'1 b').class.must_be :<, @m::HashRow
    @db.typecast_value(:pg_row_foo, %w'1 b').must_equal(:bar=>'1', :baz=>'b')
    @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').must_equal(:bar=>'1', :baz=>'b')
    @db.literal(p1.call('(1,b)')).must_equal "ROW(1, 'bb')::foo"
  end

  it "should allow registering row type parsers for schema qualify types" do
    @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
    @db.conversion_procs[5] = p5 = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo__bar)
    @db.sqls.must_equal ["SELECT pg_type.oid, typrelid, typarray FROM pg_type INNER JOIN pg_namespace ON ((pg_namespace.oid = pg_type.typnamespace) AND (pg_namespace.nspname = 'foo')) WHERE ((typtype = 'c') AND (typname = 'bar')) LIMIT 1",
      "SELECT attname, (CASE pg_type.typbasetype WHEN 0 THEN atttypid ELSE pg_type.typbasetype END) AS atttypid FROM pg_attribute INNER JOIN pg_type ON (pg_type.oid = pg_attribute.atttypid) WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
    p1 = @db.conversion_procs[1]
    p1.columns.must_equal [:bar, :baz]
    p1.column_oids.must_equal [4, 5]
    p1.column_converters.must_equal [p4, p5]
    p1.oid.must_equal 1

    c = p1.converter
    c.superclass.must_equal @m::HashRow
    c.columns.must_equal [:bar, :baz]
    c.db_type.must_equal :foo__bar
    p1.typecaster.must_equal c

    p1.call('(1,b)').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo__bar, %w'1 b').must_equal(:bar=>'1', :baz=>'b')
    @db.typecast_value(:pg_row_foo__bar, :bar=>'1', :baz=>'b').must_equal(:bar=>'1', :baz=>'b')
    @db.literal(p1.call('(1,b)')).must_equal "ROW(1, 'bb')::foo.bar"
  end

  it "should not allow registering on a frozen database" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[], [{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    c = proc{|h| [h]}
    @db.freeze
    proc{@db.register_row_type(:foo, :converter=>c)}.must_raise RuntimeError, TypeError
  end

  it "should allow registering with a custom converter" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    c = proc{|h| [h]}
    @db.register_row_type(:foo, :converter=>c)
    o = @db.conversion_procs[1].call('(1,b)')
    o.must_equal [{:bar=>1, :baz=>'bb'}]
    o.first.must_be_kind_of(Hash)
  end

  it "should allow registering with a custom typecaster" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2}})
    @db.typecast_value(:pg_row_foo, %w'1 b').must_be_kind_of(Hash)
    @db.typecast_value(:pg_row_foo, %w'1 b').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo, 'bar'=>'1', 'baz'=>'b').must_equal(:bar=>0, :baz=>'aa')
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar] || h['bar'] || 0).to_i, :baz=>(h[:baz] || h['baz'] || 'a')*2}})
    @db.typecast_value(:pg_row_foo, %w'1 b').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').must_equal(:bar=>1, :baz=>'bb')
    @db.typecast_value(:pg_row_foo, 'bar'=>'1', 'baz'=>'b').must_equal(:bar=>1, :baz=>'bb')
  end

  it "should handle conversion procs that aren't added until later" do
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    c = proc{|h| [h]}
    @db.register_row_type(:foo, :converter=>c)
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[1].call('(1,b)').must_equal [{:bar=>1, :baz=>'bb'}]
  end

  it "should handle nil values when converting columns" do
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}]]
    called = false
    @db.conversion_procs[4] = proc{|s| called = true; s}
    @db.register_row_type(:foo)
    @db.conversion_procs[1].call('()').must_equal(:bar=>nil)
    called.must_equal false
  end

  it "should registering array type for row type if type has an array oid" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2}})
    p3 = @db.conversion_procs[3]

    p3.call('{"(1,b)"}').must_equal [{:bar=>1, :baz=>'bb'}]
    @db.literal(p3.call('{"(1,b)"}')).must_equal "ARRAY[ROW(1, 'bb')::foo]::foo[]"
    @db.typecast_value(:foo_array, [{:bar=>'1', :baz=>'b'}]).must_equal [{:bar=>1, :baz=>'bb'}]
  end

  it "should allow creating unregisted row types via Database#row_type" do
    @db.literal(@db.row_type(:foo, [1, 2])).must_equal 'ROW(1, 2)::foo'
  end

  it "should allow typecasting of registered row types via Database#row_type" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| @m::HashRow.subclass(:foo, [:bar, :baz]).new({:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2})})
    @db.literal(@db.row_type(:foo, ['1', 'b'])).must_equal "ROW(1, 'bb')::foo"
    @db.literal(@db.row_type(:foo, {:bar=>'1', :baz=>'b'})).must_equal "ROW(1, 'bb')::foo"
  end

  it "should allow parsing when typecasting registered row types via Database#row_type" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| @m::HashRow.subclass(:foo, [:bar, :baz]).new(:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2)})
    @db.literal(@db.row_type(:foo, ['1', 'b'])).must_equal "ROW(1, 'bb')::foo"
  end

  it "should raise an error if attempt to use Database#row_type with an unregistered type and hash" do
    proc{@db.literal(@db.row_type(:foo, {:bar=>'1', :baz=>'b'}))}.must_raise(Sequel::InvalidValue)
  end

  it "should raise an error if attempt to use Database#row_type with an unhandled type" do
    proc{@db.literal(@db.row_type(:foo, 1))}.must_raise(Sequel::InvalidValue)
  end

  it "should return ArrayRow and HashRow values as-is" do
    h = @m::HashRow.call(:a=>1)
    a = @m::ArrayRow.call([1])
    @db.row_type(:foo, h).object_id.must_equal(h.object_id)
    @db.row_type(:foo, a).object_id.must_equal(a.object_id)
  end

  it "should have Sequel.pg_row return a plain ArrayRow" do
    @db.literal(Sequel.pg_row([1, 2, 3])).must_equal 'ROW(1, 2, 3)'
  end

  it "should raise an error if attempting to typecast a hash for a parser without columns" do
    proc{@m::Parser.new.typecast(:a=>1)}.must_raise(Sequel::Error)
  end

  it "should raise an error if attempting to typecast a unhandled value for a parser" do
    proc{@m::Parser.new.typecast(1)}.must_raise(Sequel::Error)
  end

  it "should handle typecasting for a parser without a typecaster" do
    @m::Parser.new.typecast([1]).must_equal [1]
  end

  it "should raise an error if no columns are returned when registering a custom row type" do
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}]]
    proc{@db.register_row_type(:foo)}.must_raise(Sequel::Error)
  end

  it "should raise an error when registering a custom row type if the type is found found" do
    @db.fetch = []
    proc{@db.register_row_type(:foo)}.must_raise(Sequel::Error)
  end

  it "should return correct results for Database#schema_type_class" do
    @db.conversion_procs[4] = proc{|s| s.to_i}
    @db.conversion_procs[5] = proc{|s| s * 2}
    @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
    @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2}})
    @db.schema_type_class(:pg_row_foo).must_equal [Sequel::Postgres::PGRow::HashRow, Sequel::Postgres::PGRow::ArrayRow]
    @db.schema_type_class(:integer).must_equal Integer
  end
end
