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
      dataset._fetch = proc do |sql|
        sql =~ /WHERE \(?(\w+\.)?(\w+) = (\d)\)?/
        {$2.to_sym=>$3.to_i}
      end
      def self.waw_identity_map(&block) # with and without
        with_identity_map(&block)
        db.reset
        yield
      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 == 0
      @c[2].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 1
    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 == 0
      o.destroy
      MODEL_DB.sqls.length.should == 1
      @c[1].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 1

      MODEL_DB.reset
      o = @c[2]
      MODEL_DB.sqls.length.should == 1
      o.foo = 1
      @c[2].should == o
      MODEL_DB.sqls.length.should == 0
      o.delete
      MODEL_DB.sqls.length.should == 1
      @c[2].foo.should_not == o.foo
      MODEL_DB.sqls.length.should == 1
    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 == 0
      o = @c1.load(:id=>3, :another_id=>3, :artist_id=>3)
      o.artist.should_not == a
      MODEL_DB.sqls.length.should == 1
    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 == 0
      o = @c1.load(:id=>3, :artist_id=>3)
      o.artist.should_not == a
      MODEL_DB.sqls.length.should == 1
    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 == 1
      o.artist.should == a
      MODEL_DB.sqls.length.should == 1
    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 == 1
    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 == 1
    end
  end

  it "should not override custom :eager_loaders for many_to_many associations" do
    @c1.columns :id
    @c2.columns :id
    c = @c2
    @c1.many_to_many :artists, :class=>@c2, :left_key=>:album_id, :right_key=>:artist_id, :join_table=>:aa, :eager_loader=>(proc do |eo|
       eo[:rows].each{|object| object.associations[:artists] = [c.load(:id=>object.id)]}
    end)
    @c1.dataset._fetch = [{:id=>1}, {:id=>2}, {:id=>3}]

    @c.waw_identity_map do
      MODEL_DB.sqls.length.should == 0
      a = @c1.eager(:artists).all
      MODEL_DB.sqls.length.should == 1
      a.should == [@c1.load(:id=>1), @c1.load(:id=>2), @c1.load(:id=>3)]
      a.map{|x| x.artists}.should == [[@c2.load(:id=>1)], [@c2.load(:id=>2)], [@c2.load(:id=>3)]]
      MODEL_DB.sqls.length.should == 0
    end
  end

  it "should work correctly when eagerly loading many_to_many associations" do
    @c1.columns :id
    @c2.columns :id
    @c1.many_to_many :artists, :class=>@c2, :left_key=>:album_id, :right_key=>:artist_id, :join_table=>:aa
    @c1.dataset._fetch = [{:id=>1}, {:id=>2}, {:id=>3}]
    @c2.dataset._fetch = [{:id=>1, :x_foreign_key_x=>1}, {:id=>1, :x_foreign_key_x=>2}, {:id=>2, :x_foreign_key_x=>1}, {:id=>2, :x_foreign_key_x=>2}, {:id=>3, :x_foreign_key_x=>1}, {:id=>3, :x_foreign_key_x=>1}]

    @c.waw_identity_map do
      MODEL_DB.sqls.length.should == 0
      a = @c1.eager(:artists).all
      MODEL_DB.sqls.length.should == 2
      a.should == [@c1.load(:id=>1), @c1.load(:id=>2), @c1.load(:id=>3)]
      a.map{|x| x.artists}.should == [[@c2.load(:id=>1), @c2.load(:id=>2), @c2.load(:id=>3), @c2.load(:id=>3)], [@c2.load(:id=>1), @c2.load(:id=>2)], []]
      MODEL_DB.sqls.length.should == 0
    end
  end

  it "should work correctly when eagerly loading many_to_many associations with composite keys" do
    @c1.columns :id, :id2
    @c2.columns :id
    @c1.set_primary_key :id, :id2
    @c1.many_to_many :artists, :class=>@c2, :left_key=>[:album_id1, :album_id2], :right_key=>:artist_id, :join_table=>:aa
    @c1.dataset._fetch = [{:id=>1, :id2=>4}, {:id=>2, :id2=>5}, {:id=>3, :id2=>6}]
    @c2.dataset._fetch = [ {:id=>1, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>1, :x_foreign_key_0_x=>2, :x_foreign_key_1_x=>5}, {:id=>2, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>2, :x_foreign_key_0_x=>2, :x_foreign_key_1_x=>5}, {:id=>3, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>3, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}]

    @c.waw_identity_map do
      MODEL_DB.sqls.length.should == 0
      a = @c1.eager(:artists).all
      MODEL_DB.sqls.length.should == 2
      a.should == [@c1.load(:id=>1, :id2=>4), @c1.load(:id=>2, :id2=>5), @c1.load(:id=>3, :id2=>6)]
      a.map{|x| x.artists}.should == [[@c2.load(:id=>1), @c2.load(:id=>2), @c2.load(:id=>3), @c2.load(:id=>3)], [@c2.load(:id=>1), @c2.load(:id=>2)], []]
      MODEL_DB.sqls.length.should == 0
    end
  end

  it "should work correctly when eagerly loading many_through_many associations" do
    @c1.columns :id
    @c2.columns :id
    @c1.plugin :many_through_many
    @c1.many_through_many :artists, [[:aa, :album_id, :artist_id]], :class=>@c2
    @c1.dataset._fetch = [{:id=>1}, {:id=>2}, {:id=>3}]
    @c2.dataset._fetch = [{:id=>1, :x_foreign_key_x=>1}, {:id=>1, :x_foreign_key_x=>2}, {:id=>2, :x_foreign_key_x=>1}, {:id=>2, :x_foreign_key_x=>2}, {:id=>3, :x_foreign_key_x=>1}, {:id=>3, :x_foreign_key_x=>1}]

    @c.waw_identity_map do
      MODEL_DB.sqls.length.should == 0
      a = @c1.eager(:artists).all
      MODEL_DB.sqls.length.should == 2
      a.should == [@c1.load(:id=>1), @c1.load(:id=>2), @c1.load(:id=>3)]
      a.map{|x| x.artists}.should == [[@c2.load(:id=>1), @c2.load(:id=>2), @c2.load(:id=>3), @c2.load(:id=>3)], [@c2.load(:id=>1), @c2.load(:id=>2)], []]
      MODEL_DB.sqls.length.should == 0
    end
  end

  it "should work correctly when eagerly loading many_to_many associations with composite keys" do
    @c1.columns :id, :id2
    @c2.columns :id
    @c1.set_primary_key :id, :id2
    @c1.plugin :many_through_many
    @c1.many_through_many :artists, [[:aa, [:album_id1, :album_id2], :artist_id]], :class=>@c2
    @c1.dataset._fetch = [{:id=>1, :id2=>4}, {:id=>2, :id2=>5}, {:id=>3, :id2=>6}]
    @c2.dataset._fetch = [ {:id=>1, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>1, :x_foreign_key_0_x=>2, :x_foreign_key_1_x=>5}, {:id=>2, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>2, :x_foreign_key_0_x=>2, :x_foreign_key_1_x=>5}, {:id=>3, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}, {:id=>3, :x_foreign_key_0_x=>1, :x_foreign_key_1_x=>4}]

    @c.waw_identity_map do
      MODEL_DB.sqls.length.should == 0
      a = @c1.eager(:artists).all
      MODEL_DB.sqls.length.should == 2
      a.should == [@c1.load(:id=>1, :id2=>4), @c1.load(:id=>2, :id2=>5), @c1.load(:id=>3, :id2=>6)]
      a.map{|x| x.artists}.should == [[@c2.load(:id=>1), @c2.load(:id=>2), @c2.load(:id=>3), @c2.load(:id=>3)], [@c2.load(:id=>1), @c2.load(:id=>2)], []]
      MODEL_DB.sqls.length.should == 0
    end
  end
end
