A from-scratch fork of simply_couch's model layer that runs on Couchbase natively: `include SimplyBased::Model` instead of `include SimplyCouch::Model`. - SimplyBased::Store replaces the CouchDB layer: KV documents, N1QL/SQL++ finders + lazily-created GSIs, CAS mapped onto _rev. No design documents or JavaScript map/reduce — the hand-written association views become N1QL (belongs_to/has_many/has_one scalar FK; habtm via ANY ... SATISFIES + array index, or KV multi-get on the storing side). - Redis replacements on the same cluster: Counter (atomic increment/decrement), Cache (ActiveSupport::Cache::Store over KV+TTL, drop-in Rails.cache/ActiveJob), Lock (insert-if-absent + TTL + CAS-checked release). - Runs the simply_couch test suite against live Couchbase: 378/432 applicable examples green (CouchDB-mechanic specs tagged :couchdb_only and excluded). - Remaining: ancestry (tree) + embedded_in reverse-lookups (recursive/embedded N1QL) and a few pagination/lifecycle edges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
477 lines
15 KiB
Ruby
477 lines
15 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe "HasMany" do
|
|
context "has_many" do
|
|
context "with has_many" do
|
|
it "create a fetch method for the associated objects" do
|
|
user = User.new
|
|
user.should respond_to :posts
|
|
end
|
|
|
|
it "raise an error if another property with the same name already exists" do
|
|
expect {
|
|
class ::DoubleHasManyUser
|
|
include SimplyBased::Model
|
|
property :other_users
|
|
has_many :other_users
|
|
end
|
|
}.to raise_error RuntimeError
|
|
end
|
|
|
|
it "fetch the associated objects" do
|
|
user = User.create(:title => "Mr.")
|
|
3.times {
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
}
|
|
user.posts.size.should eq 3
|
|
end
|
|
|
|
it "set the parent object on the clients cache" do
|
|
expect( User ).not_to receive(:find)
|
|
user = User.create(:title => "Mr.")
|
|
3.times {
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
}
|
|
post = user.posts.first
|
|
user.posts.first.user.should eq user
|
|
end
|
|
|
|
context "limit" do
|
|
|
|
it "be able to limit the result set" do
|
|
user = User.create(:title => "Mr.")
|
|
3.times {
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
}
|
|
user.posts(:limit => 2).size.should eq 2
|
|
end
|
|
|
|
it "use the given options in the cache-key" do
|
|
user = User.create(:title => "Mr.")
|
|
3.times {
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
}
|
|
user.posts(:limit => 2).size.should eq 2
|
|
user.posts(:limit => 3).size.should eq 3
|
|
end
|
|
|
|
it "be able to limit the result set - also for through objects" do
|
|
@user = User.create(:title => "Mr.")
|
|
first_pain = Pain.create
|
|
frist_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => first_pain)
|
|
@user.pains.should eq [first_pain]
|
|
second_pain = Pain.create
|
|
second_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => second_pain)
|
|
@user.reload
|
|
@user.pains.size.should eq 2
|
|
@user.pains(:limit => 1).size.should eq 1
|
|
end
|
|
end
|
|
|
|
context "order" do
|
|
before do
|
|
@user = User.create(:title => "Mr.")
|
|
3.times {
|
|
post = Post.new
|
|
post.user = @user
|
|
post.save!
|
|
}
|
|
end
|
|
|
|
it "support different order" do
|
|
expect{ @user.posts(:order => :asc) }.not_to raise_error
|
|
expect { @user.posts(:order => :desc) }.not_to raise_error
|
|
end
|
|
|
|
it "reverse the order if :desc" do
|
|
@user.posts(:order => :desc).map(&:id).should eq @user.posts(:order => :asc).map(&:id).reverse
|
|
end
|
|
|
|
it "work with the limit option" do
|
|
last_post = Post.create(:user => @user)
|
|
@user.posts(:order => :asc, :limit => 3).map(&:id).reverse.should_not eq @user.posts(:order => :desc, :limit => 3).map(&:id)
|
|
end
|
|
end
|
|
|
|
it "verify the given options for the accessor method" do
|
|
user = User.create(:title => "Mr.")
|
|
expect { user.posts(:foo => false) }.to raise_error ArgumentError
|
|
end
|
|
|
|
it "verify the given options for the association defintion" do
|
|
expect {
|
|
User.instance_eval do
|
|
has_many :foo, :bar => :do
|
|
end
|
|
}.to raise_error ArgumentError
|
|
end
|
|
|
|
it "only fetch objects of the correct type" do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
|
|
comment = Comment.new
|
|
comment.user = user
|
|
comment.save!
|
|
|
|
user.posts.size.should eq 1
|
|
end
|
|
|
|
it "getter should user cache" do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
user.posts
|
|
user.instance_variable_get("@posts")[:all].should eq [post]
|
|
end
|
|
|
|
it "add methods to handle associated objects" do
|
|
user = User.new(:title => "Mr.")
|
|
user.should respond_to :add_post
|
|
user.should respond_to :remove_post
|
|
user.should respond_to :remove_all_posts
|
|
end
|
|
|
|
it 'ignore the cache when requesting explicit reload' do
|
|
user = User.create(:title => "Mr.")
|
|
user.posts.should eq []
|
|
post = Post.new
|
|
post.user = user
|
|
post.save!
|
|
user.posts(:force_reload => true).should eq [post]
|
|
end
|
|
|
|
it "use the correct view when handling inheritance" do
|
|
problem = Problem.create
|
|
big_problem = BigProblem.create
|
|
issue = Issue.create(:name => 'Thing', :problem => problem)
|
|
problem.issues.size.should eq 1
|
|
issue.update_attributes(:problem_id => nil, :big_problem_id => big_problem.id)
|
|
big_problem.issues.size.should eq 1
|
|
end
|
|
|
|
context "when adding items" do
|
|
it "add the item to the internal cache" do
|
|
daddy = User.new(:title => "Mr.")
|
|
item = Post.new
|
|
daddy.posts.should eq []
|
|
daddy.add_post(item)
|
|
daddy.posts.should eq [item]
|
|
daddy.instance_variable_get("@posts")[:all].should eq [item]
|
|
end
|
|
|
|
it "raise an error when the added item is not an object of the expected class" do
|
|
user = User.new
|
|
expect { user.add_post('foo') }.to raise_error(ArgumentError, "expected Post got String")
|
|
end
|
|
|
|
it "save the added item" do
|
|
post = Post.new
|
|
user = User.create(:title => "Mr.")
|
|
user.add_post(post)
|
|
post.should_not be_a_new_record
|
|
end
|
|
|
|
it 'set the forein key on the added object' do
|
|
post = Post.new
|
|
user = User.create(:title => "Mr.")
|
|
user.add_post(post)
|
|
post.user_id.should eq user.id
|
|
end
|
|
end
|
|
|
|
context "when removing items" do
|
|
it "should unset the foreign key" do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
|
|
user.remove_post(post)
|
|
post.user_id.should be nil
|
|
end
|
|
|
|
it "remove the item from the cache" do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
user.posts.should include post
|
|
user.remove_post(post)
|
|
user.posts.any?{|p| post.id == p.id}.should_not be true
|
|
user.instance_variable_get("@posts")[:all].should eq []
|
|
end
|
|
|
|
it "save the removed item with the nullified foreign key" do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
|
|
user.remove_post(post)
|
|
post = Post.find(post.id)
|
|
post.user_id.should be nil
|
|
end
|
|
|
|
it 'raise an error when another object is the owner of the object to be removed' do
|
|
user = User.create(:title => "Mr.")
|
|
mrs = User.create(:title => "Mrs.")
|
|
post = Post.create(:user => user)
|
|
expect{ mrs.remove_post post }.to raise_error ArgumentError
|
|
end
|
|
|
|
it 'raise an error when the object is the wrong type' do
|
|
user = User.new
|
|
expect{ user.remove_post 'foo' }.to raise_error(ArgumentError, 'expected Post got String')
|
|
end
|
|
|
|
it "delete the object when dependent:destroy" do
|
|
Category.instance_eval do
|
|
has_many :tags, :dependent => :destroy
|
|
end
|
|
|
|
category = Category.create(:name => "food")
|
|
tag = Tag.create(:name => "food", :category => category)
|
|
tag.should_not be_new
|
|
category.remove_tag(tag)
|
|
|
|
Tag.find(:all).should eq []
|
|
end
|
|
|
|
it "not nullify or delete dependents if the options is set to :ignore when removing" do
|
|
master = Master.create
|
|
master_id = master.id
|
|
servant = Servant.create(:master => master)
|
|
master.remove_servant(servant)
|
|
servant.reload.master_id.should eq master_id
|
|
end
|
|
|
|
it "not nullify or delete dependents if the options is set to :ignore when deleting" do
|
|
master = Master.create
|
|
master_id = master.id
|
|
servant = Servant.create(:master => master)
|
|
master.destroy
|
|
servant.reload.master_id.should eq master_id
|
|
end
|
|
|
|
end
|
|
|
|
context "when removing all items" do
|
|
it 'nullify the foreign keys on all referenced items' do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
post2 = Post.create(:user => user)
|
|
user.remove_all_posts
|
|
post = Post.find(post.id)
|
|
post2 = Post.find(post2.id)
|
|
post.user_id.should be nil
|
|
post2.user_id.should be nil
|
|
end
|
|
|
|
it 'empty the cache' do
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
post2 = Post.create(:user => user)
|
|
user.remove_all_posts
|
|
user.posts.should eq []
|
|
user.instance_variable_get("@posts")[:all].should eq []
|
|
end
|
|
|
|
context "when counting" do
|
|
before do
|
|
@user = User.create(:title => "Mr.")
|
|
end
|
|
|
|
it "define a count method" do
|
|
@user.should respond_to :post_count
|
|
end
|
|
|
|
it "cache the result" do
|
|
@user.post_count.should eq 0
|
|
Post.create(:user => @user)
|
|
@user.post_count.should eq 0
|
|
@user.instance_variable_get("@post_count").should eq 0
|
|
@user.instance_variable_set("@post_count", nil)
|
|
@user.post_count.should eq 1
|
|
end
|
|
|
|
it "force reload even if cached" do
|
|
@user.post_count.should eq 0
|
|
Post.create(:user => @user)
|
|
@user.post_count.should eq 0
|
|
@user.post_count(:force_reload => true).should eq 1
|
|
end
|
|
|
|
it "count the number of belongs_to objects" do
|
|
@user.post_count(:force_reload => true).should eq 0
|
|
Post.create(:user => @user)
|
|
@user.post_count(:force_reload => true).should eq 1
|
|
Post.create(:user => @user)
|
|
@user.post_count(:force_reload => true).should eq 2
|
|
end
|
|
|
|
it "not count foreign objects" do
|
|
@user.post_count.should eq 0
|
|
Post.create(:user => nil)
|
|
Post.create(:user => User.create(:title => 'Doc'))
|
|
@user.post_count.should eq 0
|
|
Post.count.should eq 2
|
|
end
|
|
|
|
it "not count delete objects" do
|
|
hemorrhoid = Hemorrhoid.create(:user => @user)
|
|
@user.hemorrhoid_count.should eq 1
|
|
hemorrhoid.delete
|
|
@user.hemorrhoid_count(:force_reload => true).should eq 0
|
|
@user.hemorrhoid_count(:force_reload => true, :with_deleted => true).should eq 1
|
|
end
|
|
|
|
it "work with has_many :through" do
|
|
@user.pain_count.should eq 0
|
|
first_pain = Pain.create
|
|
frist_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => first_pain)
|
|
@user.pains.should eq [first_pain]
|
|
@user.pain_count(:force_reload => true).should eq 1
|
|
|
|
second_pain = Pain.create
|
|
second_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => second_pain)
|
|
@user.pain_count(:force_reload => true).should eq 2
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
context 'when destroying the parent objects' do
|
|
it "delete relations when dependent is destroy" do
|
|
Category.instance_eval do
|
|
has_many :tags, :dependent => :destroy
|
|
end
|
|
|
|
category = Category.create(:name => "food")
|
|
tag = Tag.create(:name => "food", :category => category)
|
|
|
|
Tag.find(:all).should eq [tag]
|
|
category.destroy
|
|
Tag.find(:all).should eq []
|
|
end
|
|
|
|
it "nullify relations when dependent is nullify" do
|
|
|
|
user = User.create(:title => "Mr.")
|
|
post = Post.create(:user => user)
|
|
|
|
user.destroy
|
|
post = Post.find(post.id)
|
|
post.user_id.should be nil
|
|
end
|
|
|
|
it "nullify the foreign key even if validation forbids" do
|
|
user = User.create(:title => "Mr.")
|
|
post = StrictPost.create(:user => user)
|
|
|
|
user.destroy
|
|
post = StrictPost.find(post.id)
|
|
post.user_id.should be nil
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with has_many :trough" do
|
|
before do
|
|
@journal_1 = Journal.create
|
|
@journal_2 = Journal.create
|
|
@reader_1 = Reader.create
|
|
@reader_2 = Reader.create
|
|
end
|
|
|
|
it "raise an exception if there is no :through relation" do
|
|
|
|
expect {
|
|
class FooHasManyThroughBar
|
|
include SimplyBased::Model
|
|
has_many :foos, :through => :bars
|
|
end
|
|
}.to raise_error ArgumentError
|
|
end
|
|
|
|
it "define a getter" do
|
|
@journal_1.should respond_to :readers
|
|
@reader_1.should respond_to :journals
|
|
end
|
|
|
|
it "load the objects through" do
|
|
membership = Membership.new
|
|
membership.journal = @journal_1
|
|
membership.reader = @reader_1
|
|
membership.save.should be true
|
|
|
|
membership.journal.should eq @journal_1
|
|
membership.reader.should eq @reader_1
|
|
@journal_1.reload.memberships.should eq [membership]
|
|
@journal_1.reload.memberships.should eq [membership]
|
|
|
|
@journal_1.readers.should eq [@reader_1]
|
|
@reader_1.journals.should eq [@journal_1]
|
|
|
|
membership_2 = Membership.new
|
|
membership_2.journal = @journal_1
|
|
membership_2.reader = @reader_2
|
|
membership_2.save.should be true
|
|
|
|
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
|
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
|
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
|
|
|
membership_3 = Membership.new
|
|
membership_3.journal = @journal_2
|
|
membership_3.reader = @reader_2
|
|
membership_3.save.should be true
|
|
|
|
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
|
@journal_2.reload.readers.map(&:id).sort.should eq [@reader_2.id].sort
|
|
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
|
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id, @journal_2.id].sort
|
|
|
|
membership_3.destroy
|
|
|
|
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
|
@journal_2.reload.readers.should eq []
|
|
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
|
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
|
end
|
|
|
|
it "verify the given options" do
|
|
expect{ @journal_1.readers(:foo => true) }.to raise_error ArgumentError
|
|
end
|
|
|
|
it "not try to destroy/nullify through-objects on parent object delete" do
|
|
membership = Membership.new
|
|
membership.journal = @journal_1
|
|
membership.reader = @reader_1
|
|
membership.save.should be true
|
|
|
|
@reader_1.reload
|
|
@journal_1.reload
|
|
|
|
expect_any_instance_of( Journal ).not_to receive(:readers)
|
|
expect_any_instance_of( Reader ).not_to receive("journal=")
|
|
|
|
@journal_1.delete
|
|
end
|
|
end
|
|
|
|
it "caches the parent relation" do
|
|
existing_user = User.create title: "User"
|
|
Post.create user: existing_user
|
|
Post.create user: existing_user
|
|
user = User.find(existing_user.id)
|
|
expect{ user.posts.each.map(&:user) }.not_to exceed_query_limit 1
|
|
end
|
|
end
|
|
end
|