Files
GregandClaude Opus 4.8 86299b8827 SimplyBased 0.1.0 — Couchbase-native ActiveModel ORM
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>
2026-06-20 13:58:47 -05:00

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