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

describe "Sequel::Plugins::IdentityMap" do
  before do
    class ::IdentityMapModel < Sequel::Model
      plugin :identity_map
      attr_accessor :foo
      ds = dataset
      def ds.fetch_rows(sql)
        c = @opts[:where].args.first
        c = c.column if c.is_a?(Sequel::SQL::QualifiedIdentifier)
        h = {c=>@opts[:where].args.last}
        execute(sql)
        yield h
      end
    end
    class ::IdentityMapAlbum < ::IdentityMapModel
      columns :artist_id
    end
    class ::IdentityMapArtist < ::IdentityMapModel
    end
    @c = ::IdentityMapModel
    @c1 = ::IdentityMapAlbum
    @c2 = ::IdentityMapArtist
    MODEL_DB.reset
  end
  after do
    Object.send(:remove_const, :IdentityMapAlbum)
    Object.send(:remove_const, :IdentityMapArtist)
    Object.send(:remove_const, :IdentityMapModel)
  end

  it "#identity_map should return a hash if an identity map is currently being used" do
    @c.with_identity_map{@c.identity_map.should == {}}
  end

  it "#identity_map should return nil if an identity map is not currently being used" do
    @c.identity_map.should == nil
  end

  it "#identity_map_key should be the same for the same class and pk" do
    @c.identity_map_key(1).should == @c.identity_map_key(1)
  end

  it "#identity_map_key should be different for a different class" do
    @c1.identity_map_key(1).should_not == @c2.identity_map_key(1)
  end

  it "#identity_map_key should be different for different anonymous classes" do
    Class.new(@c).identity_map_key(1).should_not == Class.new(@c).identity_map_key(1)
  end

  it "#identity_map_key should be different for a different pk" do
    @c.identity_map_key(1).should_not == @c.identity_map_key(2)
  end

  it "#identity_map_key should be different if the pk is nil" do
    @c.identity_map_key(nil).should_not == @c.identity_map_key(nil)
  end

  it "#load should return an object if there is no current identity map" do
    o = @c.load(:id=>1)
    o.should be_a_kind_of(@c)
    o.values.should == {:id=>1}
  end

  it "#load should return an object if there is a current identity map" do
    @c.with_identity_map do
      o = @c.load(:id=>1)
      o.should be_a_kind_of(@c)
      o.values.should == {:id=>1}
    end
  end

  it "#load should store the object in the current identity map if it isn't already there" do
    @c.with_identity_map do
      @c.identity_map[@c.identity_map_key(1)].should == nil
      o = @c.load(:id=>1)
      @c.identity_map[@c.identity_map_key(1)].should == o
    end
  end

  it "#load should update the record in the current identity map if new fields if it is already there" do
    @c.with_identity_map do
      o = @c.load(:id=>1, :a=>2)
      o.values.should == {:id=>1, :a=>2}
      o = @c.load(:id=>1, :b=>3)
      o.values.should == {:id=>1, :a=>2, :b=>3}
    end
  end

  it "#load should not update existing fields in the record if the record is in the current identity map" do
    @c.with_identity_map do
      o = @c.load(:id=>1, :a=>2)
      o.values.should == {:id=>1, :a=>2}
      o = @c.load(:id=>1, :a=>4)
      o.values.should == {:id=>1, :a=>2}
    end
  end

  it "should use the identity map as a lookup cache in Model.[] to save on database queries" do
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c[1]
      MODEL_DB.sqls.length.should == 1
      o.foo = 1
      @c[1].foo.should == o.foo
      MODEL_DB.sqls.length.should == 1
      @c[2].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 2
    end
  end

  it "should remove instances from the identity map if they are deleted or destroyed" do
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c[1]
      MODEL_DB.sqls.length.should == 1
      o.foo = 1
      @c[1].should == o
      MODEL_DB.sqls.length.should == 1
      o.destroy
      MODEL_DB.sqls.length.should == 2
      @c[1].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 3

      MODEL_DB.reset
      o = @c[2]
      MODEL_DB.sqls.length.should == 1
      o.foo = 1
      @c[2].should == o
      MODEL_DB.sqls.length.should == 1
      o.delete
      MODEL_DB.sqls.length.should == 2
      @c[2].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 3
    end
  end

  it "should use the identity map as a lookup cache when retrieving many_to_one associated records via a composite key" do
    @c1.columns :another_id
    @c1.many_to_one :artist, :class=>@c2, :key=>[:id, :another_id]
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c1.load(:id=>1, :another_id=>1, :artist_id=>2)
      a = o.artist
      a.should be_a_kind_of(@c2)
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>1, :another_id=>2, :artist_id=>2)
      o.artist.should == a
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>3, :another_id=>3, :artist_id=>3)
      o.artist.should_not == a
      MODEL_DB.sqls.length.should == 2
    end
  end

  it "should use the identity map as a lookup cache when retrieving many_to_one associated records" do
    @c1.many_to_one :artist, :class=>@c2
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c1.load(:id=>1, :artist_id=>2)
      a = o.artist
      a.should be_a_kind_of(@c2)
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>2, :artist_id=>2)
      o.artist.should == a
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>3, :artist_id=>3)
      o.artist.should_not == a
      MODEL_DB.sqls.length.should == 2
    end
  end

  it "should not use the identity map as a lookup cache for a one_to_one association" do
    c = @c2
    @c2.one_to_one :artist, :class=>@c1, :key=>:artist_id
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c2.load(:id=>2)
      a = o.artist
      a.should be_a_kind_of(@c1)
      MODEL_DB.sqls.length.should == 1
      o.reload
      MODEL_DB.sqls.length.should == 2
      o.artist.should == a
      MODEL_DB.sqls.length.should == 3
    end
  end

  it "should not use the identity map as a lookup cache if the assocation has a nil :key option" do
    c = @c2
    @c1.many_to_one :artist, :class=>@c2, :key=>nil, :dataset=>proc{c.filter(:artist_id=>artist_id)}
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c1.load(:id=>1, :artist_id=>2)
      a = o.artist
      a.should be_a_kind_of(@c2)
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>2, :artist_id=>2)
      o.artist.should == a
      MODEL_DB.sqls.length.should == 2
    end
  end

  it "should not use the identity map as a lookup cache if the assocation's :primary_key option doesn't match the primary key of the associated class" do
    @c1.many_to_one :artist, :class=>@c2, :primary_key=>:artist_id
    @c.with_identity_map do
      MODEL_DB.sqls.length.should == 0
      o = @c1.load(:id=>1, :artist_id=>2)
      a = o.artist
      a.should be_a_kind_of(@c2)
      MODEL_DB.sqls.length.should == 1
      o = @c1.load(:id=>2, :artist_id=>2)
      o.artist.should == a
      MODEL_DB.sqls.length.should == 2
    end
  end
end
