require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper") describe "Model attribute setters" do before do @c = Class.new(Sequel::Model(:items)) do columns :id, :x, :y, :"x y" end @o = @c.new DB.reset end specify "refresh should return self" do @o = @c[1] @o.stub(:_refresh).and_return([]) @o.refresh.should == @o end it "should mark the column value as changed" do @o.changed_columns.should == [] @o.x = 2 @o.changed_columns.should == [:x] @o.y = 3 @o.changed_columns.should == [:x, :y] @o.changed_columns.clear @o[:x] = 2 @o.changed_columns.should == [:x] @o[:y] = 3 @o.changed_columns.should == [:x, :y] end it "should handle columns that can't be called like normal ruby methods" do @o.send(:"x y=", 3) @o.changed_columns.should == [:"x y"] @o.values.should == {:"x y"=>3} @o.send(:"x y").should == 3 end end describe "Model.def_column_alias" do before do @o = Class.new(Sequel::Model(:items)) do columns :id def_column_alias(:id2, :id) end.load(:id=>1) DB.reset end it "should create an getter alias for the column" do @o.id2.should == 1 end it "should create an setter alias for the column" do @o.id2 = 2 @o.id2.should == 2 @o.values.should == {:id => 2} end end describe Sequel::Model, "dataset" do before do @a = Class.new(Sequel::Model(:items)) @b = Class.new(Sequel::Model) class ::Elephant < Sequel::Model(:ele1); end class ::Maggot < Sequel::Model; end class ::ShoeSize < Sequel::Model; end class ::BootSize < ShoeSize; end end after do [:Elephant, :Maggot, :ShoeSize, :BootSize].each{|x| Object.send(:remove_const, x)} end specify "should default to the plural of the class name" do Maggot.dataset.sql.should == 'SELECT * FROM maggots' ShoeSize.dataset.sql.should == 'SELECT * FROM shoe_sizes' end specify "should return the dataset for the superclass if available" do BootSize.dataset.sql.should == 'SELECT * FROM shoe_sizes' end specify "should return the correct dataset if set explicitly" do Elephant.dataset.sql.should == 'SELECT * FROM ele1' @a.dataset.sql.should == 'SELECT * FROM items' end specify "should raise if no dataset is explicitly set and the class is anonymous" do proc {@b.dataset}.should raise_error(Sequel::Error) end specify "should disregard namespaces for the table name" do begin module ::BlahBlah class MwaHaHa < Sequel::Model end end BlahBlah::MwaHaHa.dataset.sql.should == 'SELECT * FROM mwa_ha_has' ensure Object.send(:remove_const, :BlahBlah) end end end describe Sequel::Model, ".def_dataset_method" do before do @c = Class.new(Sequel::Model(:items)) end it "should add a method to the dataset and model if called with a block argument" do @c.def_dataset_method(:return_3){3} @c.return_3.should == 3 @c.dataset.return_3.should == 3 end it "should handle weird method names" do @c.def_dataset_method(:"return 3"){3} @c.send(:"return 3").should == 3 @c.dataset.send(:"return 3").should == 3 end it "should not add a model method if the model already responds to the method" do @c.instance_eval do def foo 1 end private def bar 2 end def_dataset_method(:foo){3} def_dataset_method(:bar){4} end @c.foo.should == 1 @c.dataset.foo.should == 3 @c.send(:bar).should == 2 @c.dataset.bar.should == 4 end it "should add all passed methods to the model if called without a block argument" do @c.def_dataset_method(:return_3, :return_4) proc{@c.return_3}.should raise_error(NoMethodError) proc{@c.return_4}.should raise_error(NoMethodError) @c.dataset.instance_eval do def return_3; 3; end def return_4; 4; end end @c.return_3.should == 3 @c.return_4.should == 4 end it "should cache calls and readd methods if set_dataset is used" do @c.def_dataset_method(:return_3){3} @c.set_dataset :items @c.return_3.should == 3 @c.dataset.return_3.should == 3 end it "should readd methods to subclasses, if set_dataset is used in a subclass" do @c.def_dataset_method(:return_3){3} c = Class.new(@c) c.set_dataset :items c.return_3.should == 3 c.dataset.return_3.should == 3 end end describe Sequel::Model, ".dataset_module" do before do @c = Class.new(Sequel::Model(:items)) end it "should extend the dataset with the module if the model has a dataset" do @c.dataset_module{def return_3() 3 end} @c.dataset.return_3.should == 3 end it "should also extend the instance_dataset with the module if the model has a dataset" do @c.dataset_module{def return_3() 3 end} @c.instance_dataset.return_3.should == 3 end it "should add methods defined in the module to the class" do @c.dataset_module{def return_3() 3 end} @c.return_3.should == 3 end it "should add methods defined in the module outside the block to the class" do @c.dataset_module.module_eval{def return_3() 3 end} @c.return_3.should == 3 end it "should cache calls and readd methods if set_dataset is used" do @c.dataset_module{def return_3() 3 end} @c.set_dataset :items @c.return_3.should == 3 @c.dataset.return_3.should == 3 end it "should readd methods to subclasses, if set_dataset is used in a subclass" do @c.dataset_module{def return_3() 3 end} c = Class.new(@c) c.set_dataset :items c.return_3.should == 3 c.dataset.return_3.should == 3 end it "should only have a single dataset_module per class" do @c.dataset_module{def return_3() 3 end} @c.dataset_module{def return_3() 3 + (begin; super; rescue NoMethodError; 1; end) end} @c.return_3.should == 4 end it "should not have subclasses share the dataset_module" do @c.dataset_module{def return_3() 3 end} c = Class.new(@c) c.dataset_module{def return_3() 3 + (begin; super; rescue NoMethodError; 1; end) end} c.return_3.should == 6 end it "should accept a module object and extend the dataset with it" do @c.dataset_module Module.new{def return_3() 3 end} @c.dataset.return_3.should == 3 end it "should be able to call dataset_module with a module multiple times" do @c.dataset_module Module.new{def return_3() 3 end} @c.dataset_module Module.new{def return_4() 4 end} @c.dataset.return_3.should == 3 @c.dataset.return_4.should == 4 end it "should be able mix dataset_module calls with and without arguments" do @c.dataset_module{def return_3() 3 end} @c.dataset_module Module.new{def return_4() 4 end} @c.dataset.return_3.should == 3 @c.dataset.return_4.should == 4 end it "should have modules provided to dataset_module extend subclass datasets" do @c.dataset_module{def return_3() 3 end} @c.dataset_module Module.new{def return_4() 4 end} c = Class.new(@c) c.set_dataset :a c.dataset.return_3.should == 3 c.dataset.return_4.should == 4 end it "should return the dataset module if given a block" do Object.new.extend(@c.dataset_module{def return_3() 3 end}).return_3.should == 3 end it "should return the argument if given one" do Object.new.extend(@c.dataset_module Module.new{def return_3() 3 end}).return_3.should == 3 end it "should have dataset_module support a subset method" do @c.dataset_module{subset :released, :released} @c.released.sql.should == 'SELECT * FROM items WHERE released' @c.where(:foo).released.sql.should == 'SELECT * FROM items WHERE (foo AND released)' end it "should raise error if called with both an argument and ablock" do proc{@c.dataset_module(Module.new{def return_3() 3 end}){}}.should raise_error(Sequel::Error) end end describe "A model class with implicit table name" do before do class ::Donkey < Sequel::Model end end after do Object.send(:remove_const, :Donkey) end specify "should have a dataset associated with the model class" do Donkey.dataset.model.should == Donkey end end describe "A model inheriting from a model" do before do class ::Feline < Sequel::Model; end class ::Leopard < Feline; end end after do Object.send(:remove_const, :Leopard) Object.send(:remove_const, :Feline) end specify "should have a dataset associated with itself" do Feline.dataset.model.should == Feline Leopard.dataset.model.should == Leopard end end describe "Model.primary_key" do before do @c = Class.new(Sequel::Model) end specify "should default to id" do @c.primary_key.should == :id end specify "should be overridden by set_primary_key" do @c.set_primary_key :cid @c.primary_key.should == :cid @c.set_primary_key([:id1, :id2]) @c.primary_key.should == [:id1, :id2] end specify "should use nil for no primary key" do @c.no_primary_key @c.primary_key.should == nil end end describe "Model.primary_key_hash" do before do @c = Class.new(Sequel::Model) end specify "should handle a single primary key" do @c.primary_key_hash(1).should == {:id=>1} end specify "should handle a composite primary key" do @c.set_primary_key([:id1, :id2]) @c.primary_key_hash([1, 2]).should == {:id1=>1, :id2=>2} end specify "should raise an error for no primary key" do @c.no_primary_key proc{@c.primary_key_hash(1)}.should raise_error(Sequel::Error) end end describe "Model.qualified_primary_key_hash" do before do @c = Class.new(Sequel::Model(:items)) end specify "should handle a single primary key" do @c.qualified_primary_key_hash(1).should == {Sequel.qualify(:items, :id)=>1} end specify "should handle a composite primary key" do @c.set_primary_key([:id1, :id2]) @c.qualified_primary_key_hash([1, 2]).should == {Sequel.qualify(:items, :id1)=>1, Sequel.qualify(:items, :id2)=>2} end specify "should raise an error for no primary key" do @c.no_primary_key proc{@c.qualified_primary_key_hash(1)}.should raise_error(Sequel::Error) end specify "should allow specifying a different qualifier" do @c.qualified_primary_key_hash(1, :apple).should == {Sequel.qualify(:apple, :id)=>1} @c.set_primary_key([:id1, :id2]) @c.qualified_primary_key_hash([1, 2], :bear).should == {Sequel.qualify(:bear, :id1)=>1, Sequel.qualify(:bear, :id2)=>2} end end describe "Model.db" do before do @db = Sequel.mock @databases = Sequel::DATABASES.dup @model_db = Sequel::Model.db Sequel::Model.db = nil Sequel::DATABASES.clear end after do Sequel::Model.instance_variable_get(:@db).should == nil Sequel::DATABASES.replace(@databases) Sequel::Model.db = @model_db end specify "should be required when creating named model classes" do begin proc{class ModelTest < Sequel::Model; end}.should raise_error(Sequel::Error) ensure Object.send(:remove_const, :ModelTest) end end specify "should be required when creating anonymous model classes without a database" do proc{Sequel::Model(:foo)}.should raise_error(Sequel::Error) end specify "should not be required when creating anonymous model classes with a database" do Sequel::Model(@db).db.should == @db Sequel::Model(@db[:foo]).db.should == @db end specify "should work correctly when subclassing anonymous model classes with a database" do begin Class.new(Sequel::Model(@db)).db.should == @db Class.new(Sequel::Model(@db[:foo])).db.should == @db class ModelTest < Sequel::Model(@db) db.should == @db end class ModelTest2 < Sequel::Model(@db[:foo]) db.should == @db end ModelTest.instance_variable_set(:@db, nil) ModelTest.db.should == @db ensure Object.send(:remove_const, :ModelTest) Object.send(:remove_const, :ModelTest2) end end end describe "Model.db=" do before do @db1 = Sequel.mock @db2 = Sequel.mock @m = Class.new(Sequel::Model(@db1[:blue].filter(:x=>1))) end specify "should affect the underlying dataset" do @m.db = @db2 @m.dataset.db.should === @db2 @m.dataset.db.should_not === @db1 end specify "should keep the same dataset options" do @m.db = @db2 @m.dataset.sql.should == 'SELECT * FROM blue WHERE (x = 1)' end specify "should use the database for subclasses" do @m.db = @db2 Class.new(@m).db.should === @db2 end end describe Sequel::Model, ".(allowed|restricted)_columns " do before do @c = Class.new(Sequel::Model(:blahblah)) do columns :x, :y, :z end @c.strict_param_setting = false @c.instance_variable_set(:@columns, [:x, :y, :z]) end it "should set the allowed columns correctly" do @c.allowed_columns.should == nil @c.set_allowed_columns :x @c.allowed_columns.should == [:x] @c.set_allowed_columns :x, :y @c.allowed_columns.should == [:x, :y] end it "should only set allowed columns by default" do @c.set_allowed_columns :x, :y i = @c.new(:x => 1, :y => 2, :z => 3) i.values.should == {:x => 1, :y => 2} i.set(:x => 4, :y => 5, :z => 6) i.values.should == {:x => 4, :y => 5} @c.instance_dataset._fetch = @c.dataset._fetch = {:x => 7} i = @c.new i.update(:x => 7, :z => 9) i.values.should == {:x => 7} DB.sqls.should == ["INSERT INTO blahblah (x) VALUES (7)", "SELECT * FROM blahblah WHERE (id = 10) LIMIT 1"] end end describe Sequel::Model, ".(un)?restrict_primary_key\\??" do before do @c = Class.new(Sequel::Model(:blahblah)) do set_primary_key :id columns :x, :y, :z, :id end @c.strict_param_setting = false end it "should restrict updates to primary key by default" do i = @c.new(:x => 1, :y => 2, :id => 3) i.values.should == {:x => 1, :y => 2} i.set(:x => 4, :y => 5, :id => 6) i.values.should == {:x => 4, :y => 5} end it "should allow updates to primary key if unrestrict_primary_key is used" do @c.unrestrict_primary_key i = @c.new(:x => 1, :y => 2, :id => 3) i.values.should == {:x => 1, :y => 2, :id=>3} i.set(:x => 4, :y => 5, :id => 6) i.values.should == {:x => 4, :y => 5, :id=>6} end it "should have restrict_primary_key? return true or false depending" do @c.restrict_primary_key?.should == true @c.unrestrict_primary_key @c.restrict_primary_key?.should == false c1 = Class.new(@c) c1.restrict_primary_key?.should == false @c.restrict_primary_key @c.restrict_primary_key?.should == true c1.restrict_primary_key?.should == false c2 = Class.new(@c) c2.restrict_primary_key?.should == true end end describe Sequel::Model, ".strict_param_setting" do before do @c = Class.new(Sequel::Model(:blahblah)) do columns :x, :y, :z, :id set_allowed_columns :x, :y end end it "should be enabled by default" do @c.strict_param_setting.should == true end it "should raise an error if a missing/restricted column/method is accessed" do proc{@c.new(:z=>1)}.should raise_error(Sequel::Error) proc{@c.create(:z=>1)}.should raise_error(Sequel::Error) c = @c.new proc{c.set(:z=>1)}.should raise_error(Sequel::Error) proc{c.set_all(:use_after_commit_rollback => false)}.should raise_error(Sequel::Error) proc{c.set_only({:x=>1}, :y)}.should raise_error(Sequel::Error) proc{c.update(:z=>1)}.should raise_error(Sequel::Error) proc{c.update_all(:use_after_commit_rollback=>false)}.should raise_error(Sequel::Error) proc{c.update_only({:x=>1}, :y)}.should raise_error(Sequel::Error) end it "should be disabled by strict_param_setting = false" do @c.strict_param_setting = false @c.strict_param_setting.should == false proc{@c.new(:z=>1)}.should_not raise_error end end describe Sequel::Model, ".require_modification" do before do @ds1 = DB[:items] def @ds1.provides_accurate_rows_matched?() false end @ds2 = DB[:items] def @ds2.provides_accurate_rows_matched?() true end end after do Sequel::Model.require_modification = nil end it "should depend on whether the dataset provides an accurate number of rows matched by default" do Class.new(Sequel::Model).set_dataset(@ds1).require_modification.should == false Class.new(Sequel::Model).set_dataset(@ds2).require_modification.should == true end it "should obey global setting regardless of dataset support if set" do Sequel::Model.require_modification = true Class.new(Sequel::Model).set_dataset(@ds1).require_modification.should == true Class.new(Sequel::Model).set_dataset(@ds2).require_modification.should == true Sequel::Model.require_modification = false Class.new(Sequel::Model).set_dataset(@ds1).require_modification.should == false Class.new(Sequel::Model).set_dataset(@ds1).require_modification.should == false end end describe Sequel::Model, ".[] optimization" do before do @db = DB.clone @db.quote_identifiers = true @c = Class.new(Sequel::Model(@db)) end it "should set simple_pk to the literalized primary key column name if a single primary key" do @c.set_primary_key :id @c.simple_pk.should == '"id"' @c.set_primary_key :b @c.simple_pk.should == '"b"' @c.set_primary_key Sequel.identifier(:b__a) @c.simple_pk.should == '"b__a"' end it "should have simple_pk be blank if compound or no primary key" do @c.no_primary_key @c.simple_pk.should == nil @c.set_primary_key [:b, :a] @c.simple_pk.should == nil end it "should have simple table set if passed a Symbol to set_dataset" do @c.set_dataset :a @c.simple_table.should == '"a"' @c.set_dataset :b @c.simple_table.should == '"b"' @c.set_dataset :b__a @c.simple_table.should == '"b"."a"' end it "should have simple_table set if passed a simple select all dataset to set_dataset" do @c.set_dataset @db[:a] @c.simple_table.should == '"a"' @c.set_dataset @db[:b] @c.simple_table.should == '"b"' @c.set_dataset @db[:b__a] @c.simple_table.should == '"b"."a"' end it "should have simple_pk and simple_table respect dataset's identifier input methods" do ds = @db[:ab] ds.identifier_input_method = :reverse @c.set_dataset ds @c.simple_table.should == '"ba"' @c.set_primary_key :cd @c.simple_pk.should == '"dc"' @c.set_dataset ds.from(:ef__gh) @c.simple_table.should == '"fe"."hg"' end it "should have simple_table = nil if passed a non-simple select all dataset to set_dataset" do @c.set_dataset @c.db[:a].filter(:active) @c.simple_table.should == nil end it "should have simple_table inherit superclass's setting" do Class.new(@c).simple_table.should == nil @c.set_dataset :a Class.new(@c).simple_table.should == '"a"' end it "should use Dataset#with_sql if simple_table and simple_pk are true" do @c.set_dataset :a @c.instance_dataset._fetch = @c.dataset._fetch = {:id => 1} @c[1].should == @c.load(:id=>1) @db.sqls.should == ['SELECT * FROM "a" WHERE "id" = 1'] end it "should not use Dataset#with_sql if either simple_table or simple_pk is nil" do @c.set_dataset @db[:a].filter(:active) @c.dataset._fetch = {:id => 1} @c[1].should == @c.load(:id=>1) @db.sqls.should == ['SELECT * FROM "a" WHERE ("active" AND ("id" = 1)) LIMIT 1'] end end describe "Model datasets #with_pk with #with_pk!" do before do @c = Class.new(Sequel::Model(:a)) @ds = @c.dataset @ds._fetch = {:id=>1} DB.reset end it "should be callable on the model class with optimized SQL" do @c.with_pk(1).should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE id = 1"] @c.with_pk!(1).should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE id = 1"] end it "should return the first record where the primary key matches" do @ds.with_pk(1).should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"] @ds.with_pk!(1).should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"] end it "should handle existing filters" do @ds.filter(:a=>2).with_pk(1) DB.sqls.should == ["SELECT * FROM a WHERE ((a = 2) AND (a.id = 1)) LIMIT 1"] @ds.filter(:a=>2).with_pk!(1) DB.sqls.should == ["SELECT * FROM a WHERE ((a = 2) AND (a.id = 1)) LIMIT 1"] end it "should work with string values" do @ds.with_pk("foo") DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 'foo') LIMIT 1"] @ds.with_pk!("foo") DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 'foo') LIMIT 1"] end it "should handle an array for composite primary keys" do @c.set_primary_key [:id1, :id2] @ds.with_pk([1, 2]) sqls = DB.sqls ["SELECT * FROM a WHERE ((a.id1 = 1) AND (a.id2 = 2)) LIMIT 1", "SELECT * FROM a WHERE ((a.id2 = 2) AND (a.id1 = 1)) LIMIT 1"].should include(sqls.pop) sqls.should == [] @ds.with_pk!([1, 2]) sqls = DB.sqls ["SELECT * FROM a WHERE ((a.id1 = 1) AND (a.id2 = 2)) LIMIT 1", "SELECT * FROM a WHERE ((a.id2 = 2) AND (a.id1 = 1)) LIMIT 1"].should include(sqls.pop) sqls.should == [] end it "should have with_pk return nil and with_pk! raise if no rows match" do @ds._fetch = [] @ds.with_pk(1).should == nil DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"] proc{@ds.with_pk!(1)}.should raise_error(Sequel::NoMatchingRow) DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"] end it "should have with_pk return nil and with_pk! raise if no rows match when calling the class method" do @ds._fetch = [] @c.with_pk(1).should == nil DB.sqls.should == ["SELECT * FROM a WHERE id = 1"] proc{@c.with_pk!(1)}.should raise_error(Sequel::NoMatchingRow) DB.sqls.should == ["SELECT * FROM a WHERE id = 1"] end it "should have #[] consider an integer as a primary key lookup" do @ds[1].should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"] end it "should not have #[] consider a string as a primary key lookup" do @ds['foo'].should == @c.load(:id=>1) DB.sqls.should == ["SELECT * FROM a WHERE (foo) LIMIT 1"] end end