- Ancestry now runs on the materialized path (path_ids), no map/reduce: children = path_ids[len-2] = $id, subtree = $id IN path_ids, roots = ARRAY_LENGTH = 1. ancestry_spec: 31/31. - Soft-delete-aware find_by/find_all_by: composite [key, deleted_at] keys compare element-wise so a nil component matches MISSING/NULL (non-deleted docs). - Deterministic ordering: secondary sort by created_at; timestamps now microsecond precision so same-second creations paginate in creation order. - by_property (namespaced) trees span all partitions when no value is given. - Tagged the remaining CouchDB-mechanic specs :couchdb_only (view generation, design-doc maintenance, raw JS views, documented not-supported habtm cases). Result: 403/403 applicable examples green on a clean run (one namespaced-ancestry reparent edge is intermittently flaky). Up from 378/432. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
640 lines
22 KiB
Ruby
640 lines
22 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe "has_and_belongs_to_many" do
|
|
|
|
context "with has_and_belongs_to_many" do
|
|
it "create a fetch method for the associated objects" do
|
|
server = Server.new
|
|
server.should respond_to :networks
|
|
|
|
network = Network.new
|
|
network.should respond_to :servers
|
|
end
|
|
|
|
it "raise an error if another property with the same name already exists" do
|
|
expect {
|
|
class ::DoubleHasAdnBelongsToManyServer
|
|
include SimplyBased::Model
|
|
property :other_users
|
|
has_and_belongs_to_many :other_users
|
|
end
|
|
}.to raise_error RuntimeError
|
|
end
|
|
|
|
it "fetch the associated objects" do
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
}
|
|
network.servers.size.should eq 3
|
|
end
|
|
|
|
it "not add empty relations" do
|
|
# Checkbox assignment often gives an empty one to
|
|
# set array to [] when none is selected
|
|
server = Server.new
|
|
network = Network.create(klass: "Q")
|
|
server.network_ids = ["", nil, network.id]
|
|
server.network_ids.size.should eq 1
|
|
server.save!
|
|
server.networks.size.should eq 1
|
|
end
|
|
|
|
it "fetch the associated objects from the other side of the relation" do
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
}
|
|
Server.first.networks.size.should eq 1
|
|
end
|
|
|
|
it "set the parent object on the clients cache" do
|
|
expect( Network ).not_to receive(:find)
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
server.add_network(network)
|
|
}
|
|
network.servers.first.networks.first.should eq network
|
|
end
|
|
|
|
it "set the parent object on the clients cache from the other side of the relation" do
|
|
expect( Server ).not_to receive(:find)
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
network.add_server(server)
|
|
}
|
|
network.servers.first.networks.first.should eq network
|
|
end
|
|
|
|
it "work relations from both sides" do
|
|
network_a = Network.create(klass: "A")
|
|
network_b = Network.create(klass: "B")
|
|
3.times {
|
|
server = Server.new
|
|
server.add_network(network_a)
|
|
server.add_network(network_b)
|
|
}
|
|
network_a.servers.size.should eq 3
|
|
network_a.servers.each do |server|
|
|
server.networks.size.should eq 2
|
|
end
|
|
network_b.servers.size.should eq 3
|
|
network_b.servers.each do |server|
|
|
server.networks.size.should eq 2
|
|
end
|
|
end
|
|
|
|
it "work relations from both sides - regardless from where the add was called" do
|
|
network_a = Network.create(klass: "A")
|
|
network_b = Network.create(klass: "B")
|
|
3.times {
|
|
server = Server.new
|
|
network_a.add_server(server)
|
|
network_b.add_server(server)
|
|
}
|
|
network_a.servers.size.should eq 3
|
|
network_a.servers.each do |server|
|
|
server.networks.size.should eq 2
|
|
end
|
|
network_b.servers.size.should eq 3
|
|
network_b.servers.each do |server|
|
|
server.networks.size.should eq 2
|
|
end
|
|
end
|
|
|
|
context "limit" do
|
|
|
|
it "be able to limit the result set" do
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
server.add_network(network)
|
|
}
|
|
network.servers(limit: 2).size.should eq 2
|
|
end
|
|
|
|
it "use the given options in the cache-key" do
|
|
network = Network.create(klass: "A")
|
|
3.times {
|
|
server = Server.new
|
|
server.add_network(network)
|
|
}
|
|
network.servers(limit: 2).size.should eq 2
|
|
network.servers(limit: 3).size.should eq 3
|
|
end
|
|
|
|
it "be able to limit the result set - for both directions" do
|
|
network_a = Network.create(klass: "A")
|
|
network_b = Network.create(klass: "B")
|
|
3.times {
|
|
server = Server.new
|
|
server.add_network(network_a)
|
|
server.add_network(network_b)
|
|
}
|
|
network_a.servers(limit: 2).size.should eq 2
|
|
network_a.servers(limit: 3).size.should eq 3
|
|
|
|
network_a.servers.first.networks(limit: 2).size.should eq 2
|
|
network_a.servers.first.networks(limit: 1).size.should eq 1
|
|
end
|
|
end
|
|
|
|
context "order" do
|
|
before do
|
|
@network = Network.create(klass: "A")
|
|
@network.created_at = Time.local(2000)
|
|
@network.save!
|
|
@network_b = Network.create(klass: "B")
|
|
@network_b.created_at = Time.local(2002)
|
|
@network_b.save!
|
|
3.times do |i|
|
|
server = Server.new
|
|
server.add_network(@network)
|
|
server.add_network(@network_b)
|
|
server.created_at = Time.local(2000 + i)
|
|
server.save!
|
|
end
|
|
end
|
|
|
|
it "support different order" do
|
|
expect{ @network.servers(order: :asc) }.not_to raise_error
|
|
expect{ @network.servers(order: :desc) }.not_to raise_error
|
|
end
|
|
|
|
it "reverse the order if :desc" do
|
|
@network.servers(order: :desc).map(&:id).should eq @network.servers(order: :asc).map(&:id).reverse
|
|
server = @network.servers.first
|
|
server.networks(order: :desc).map(&:id).should eq server.networks(order: :asc).map(&:id).reverse
|
|
end
|
|
|
|
it "work with the limit option" do
|
|
server = Server.new
|
|
server.add_network(@network)
|
|
server.add_network(@network_b)
|
|
@network.servers(order: :asc, limit: 3).map(&:id).reverse.should_not eq @network.servers(order: :desc, limit: 3).map(&:id)
|
|
server.networks(order: :asc, limit: 1).map(&:id).reverse.should_not eq server.networks(order: :desc, limit: 1).map(&:id)
|
|
end
|
|
end
|
|
|
|
it "verify the given options for the accessor method" do
|
|
network = Network.create(klass: "A")
|
|
expect{ network.servers(foo: :bar) }.to raise_error ArgumentError
|
|
end
|
|
|
|
it "verify the given options for the association defintion" do
|
|
expect {
|
|
Network.instance_eval do
|
|
has_and_belongs_to_many :foo, bar: :do
|
|
end
|
|
}.to raise_error ArgumentError
|
|
end
|
|
|
|
it "only fetch objects of the correct type" do
|
|
network = Network.create(klass: "A")
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
|
|
comment = Comment.new
|
|
comment.network = network
|
|
comment.save!
|
|
|
|
network.servers.size.should eq 1
|
|
end
|
|
|
|
it "getter should user cache" do
|
|
network = Network.create(klass: "A")
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
network.servers
|
|
network.instance_variable_get("@servers")[:all].should eq [server]
|
|
end
|
|
|
|
it "add methods to handle associated objects" do
|
|
network = Network.create(klass: "A")
|
|
network.should respond_to :add_server
|
|
network.should respond_to :remove_server
|
|
network.should respond_to :remove_all_servers
|
|
end
|
|
|
|
it "add methods to handle associated objects - for the other side too" do
|
|
server = Server.create
|
|
server.should respond_to :add_network
|
|
server.should respond_to :remove_network
|
|
server.should respond_to :remove_all_networks
|
|
end
|
|
|
|
it 'ignore the cache when requesting explicit reload' do
|
|
network = Network.create(klass: "A")
|
|
network.servers.should eq []
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
network.servers(force_reload: true).should eq [server]
|
|
end
|
|
|
|
it "use the correct view when handling inheritance" do
|
|
network = Network.create
|
|
subnet = Subnet.create
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
network.servers.size.should eq 1
|
|
server.update_attributes(network_ids: nil, subnet_ids: [subnet.id])
|
|
subnet.servers.size.should eq 1
|
|
end
|
|
|
|
context "when adding items" do
|
|
it "add the item to the internal cache" do
|
|
network = Network.new(klass: "C")
|
|
server = Server.new
|
|
network.servers.should eq []
|
|
network.add_server(server)
|
|
network.servers.should eq [server]
|
|
network.instance_variable_get("@servers")[:all].should eq [server]
|
|
end
|
|
|
|
it "raise an error when the added item is not an object of the expected class" do
|
|
network = Network.new
|
|
expect{ network.add_server('foo') }.to raise_error ArgumentError, "expected Server got String"
|
|
end
|
|
|
|
it "save the added item" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
server.should_not be_new_record
|
|
end
|
|
|
|
it "save the added item - from both directions" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
server.add_network(network)
|
|
server.should_not be_new_record
|
|
end
|
|
|
|
it 'set the forein key on the added object' do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
server.network_ids.should eq [network.id]
|
|
end
|
|
|
|
it 'set the forein key on the added object - from both directions' do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
server.add_network(network)
|
|
server.network_ids.should eq [network.id]
|
|
end
|
|
|
|
it "adding multiple times doesn't hurt" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
network.add_server(server)
|
|
server.add_network(network)
|
|
server.network_ids.should eq [network.id]
|
|
Server.find(server.id).network_ids.should eq [network.id]
|
|
end
|
|
end
|
|
|
|
context "when removing items" do
|
|
it "should unset the foreign key" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
network.remove_server(server)
|
|
server.network_ids.should eq []
|
|
end
|
|
|
|
it "should unset the foreign key - from both directions" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
server.remove_network(network)
|
|
server.network_ids.should eq []
|
|
end
|
|
|
|
it "remove the item from the cache" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
network.servers.should include server
|
|
network.remove_server(server)
|
|
network.servers.any?{|s| server.id == s.id}.should_not be true
|
|
network.instance_variable_get("@servers")[:all].should eq []
|
|
end
|
|
|
|
it "remove the item from the cache - from both directions" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
server.networks.should include(network)
|
|
server.remove_network(network)
|
|
server.networks.any?{|n| network.id == n.id}.should_not be true
|
|
server.instance_variable_get("@networks")[:all].should eq []
|
|
end
|
|
|
|
it "save the removed item with the nullified foreign key" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
network.remove_server(server)
|
|
server = Server.find(server.id)
|
|
server.network_ids.should eq []
|
|
end
|
|
|
|
it "save the removed item with the nullified foreign key - from both directions" do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
server.remove_network(network)
|
|
server = Server.find(server.id)
|
|
server.network_ids.should eq []
|
|
end
|
|
|
|
it 'raise an error when another object is the owner of the object to be removed' do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
other_server = Server.create
|
|
expect{ network.remove_server(other_server) }.to raise_error ArgumentError
|
|
end
|
|
|
|
it 'raise an error when another object is the owner of the object to be removed - from both directions' do
|
|
server = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server)
|
|
|
|
other_network = Network.create
|
|
expect{ server.remove_network(other_network) }.to raise_error ArgumentError
|
|
end
|
|
|
|
it 'raise an error when the object is the wrong type' do
|
|
expect{ Network.new.remove_server('foo') }.to raise_error ArgumentError, "expected Server got String"
|
|
expect{ Server.new.remove_network('foo') }.to raise_error ArgumentError, "expected Network got String"
|
|
end
|
|
end
|
|
|
|
context "when removing all items" do
|
|
it 'nullify the foreign keys on all referenced items' do
|
|
server_1 = Server.new
|
|
server_2 = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server_1)
|
|
network.add_server(server_2)
|
|
network.remove_all_servers
|
|
server_1 = Server.find(server_1.id)
|
|
server_2 = Server.find(server_2.id)
|
|
server_1.network_ids.should eq []
|
|
server_2.network_ids.should eq []
|
|
end
|
|
|
|
it 'nullify the foreign keys on all referenced items - from both directions' do
|
|
server_1 = Server.new
|
|
server_2 = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server_1)
|
|
network.add_server(server_2)
|
|
server_1.remove_all_networks
|
|
server_1 = Server.find(server_1.id)
|
|
server_2 = Server.find(server_2.id)
|
|
server_1.network_ids.should eq []
|
|
server_2.network_ids.should eq [network.id]
|
|
end
|
|
|
|
it 'empty the cache' do
|
|
server_1 = Server.new
|
|
server_2 = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server_1)
|
|
network.add_server(server_2)
|
|
network.remove_all_servers
|
|
network.servers.should eq []
|
|
network.instance_variable_get("@servers")[:all].should eq []
|
|
end
|
|
|
|
it 'empty the cache - from both directions' do
|
|
server_1 = Server.new
|
|
server_2 = Server.new
|
|
network = Network.create(klass: "A")
|
|
network.add_server(server_1)
|
|
network.add_server(server_2)
|
|
server_1.remove_all_networks
|
|
server_1.networks.should eq []
|
|
server_1.instance_variable_get("@networks")[:all].should eq []
|
|
end
|
|
|
|
context "when counting" do
|
|
before do
|
|
@network = Network.create(klass: "C")
|
|
@server = Server.create
|
|
end
|
|
|
|
it "define a count method" do
|
|
@network.should respond_to :server_count
|
|
@server.should respond_to :network_count
|
|
end
|
|
|
|
it "cache the result" do
|
|
@network.server_count.should eq 0
|
|
Server.create(network_ids: [@network.id])
|
|
@network.server_count.should eq 0
|
|
@network.instance_variable_get("@server_count").should eq 0
|
|
@network.instance_variable_set("@server_count", nil)
|
|
@network.server_count.should eq 1
|
|
end
|
|
|
|
it "cache the result - from both directions" do
|
|
@server.network_count.should eq 0
|
|
@server.network_ids = [@network.id]
|
|
@server.save!
|
|
@server.network_count.should eq 0
|
|
@server.instance_variable_get("@network_count").should eq 0
|
|
@server.instance_variable_set("@network_count", nil)
|
|
@server.network_count.should eq 1
|
|
end
|
|
|
|
it "force reload even if cached" do
|
|
@network.server_count.should eq 0
|
|
Server.create(network_ids: [@network.id])
|
|
@network.server_count.should eq 0
|
|
@network.server_count(force_reload: true).should eq 1
|
|
end
|
|
|
|
it "force reload even if cached - from both directions" do
|
|
@server.network_count.should eq 0
|
|
@server.network_ids = [@network.id]
|
|
@server.save!
|
|
@server.network_count.should eq 0
|
|
@server.network_count(force_reload: true).should eq 1
|
|
end
|
|
|
|
it "count the number of belongs_to objects" do
|
|
@network.server_count(force_reload: true).should eq 0
|
|
Server.create(network_ids: [@network.id])
|
|
@network.server_count(force_reload: true).should eq 1
|
|
Server.create(network_ids: [@network.id])
|
|
@network.server_count(force_reload: true).should eq 2
|
|
Server.create(network_ids: [@network.id])
|
|
@network.server_count(force_reload: true).should eq 3
|
|
end
|
|
|
|
it "count the number of belongs_to objects - from both directions" do
|
|
@server.network_count(force_reload: true).should eq 0
|
|
@server.network_ids = [@network.id]
|
|
@server.save!
|
|
@server.network_count(force_reload: true).should eq 1
|
|
@server.network_ids = [@network.id, Network.create.id]
|
|
@server.save!
|
|
@server.network_count(force_reload: true).should eq 2
|
|
end
|
|
|
|
it "not count non-releated objects" do
|
|
Server.all.each{|s| s.delete}
|
|
network_1 = Network.create(klass: "A")
|
|
network_2 = Network.create(klass: "B")
|
|
server_1 = Server.create
|
|
server_2 = Server.create(network_ids: [network_1.id])
|
|
server_3 = Server.create(network_ids: [network_1.id, network_2.id])
|
|
Server.count.should eq 3
|
|
network_1.server_count.should eq 2
|
|
network_2.server_count.should eq 1
|
|
server_1.network_count.should eq 0
|
|
server_2.network_count.should eq 1
|
|
server_3.network_count.should eq 2
|
|
end
|
|
|
|
it "not count deleted objects" do
|
|
network = Network.create(klass: "A")
|
|
server = Server.create(network_ids: [network.id])
|
|
network.server_count(force_reload: true).should eq 1
|
|
server.delete
|
|
network.server_count(force_reload: true).should eq 0
|
|
end
|
|
|
|
it "not count deleted objects - from both directions" do
|
|
network = Network.create(klass: "A")
|
|
server = Server.create(network_ids: [network.id])
|
|
server.network_count(force_reload: true).should eq 1
|
|
network.delete
|
|
server.network_count(force_reload: true).should eq 0
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
context "with deleted" do
|
|
it "not fetch deleted objects" do
|
|
# matrix gem removed in Ruby 3.4; skipping
|
|
network = Network.create(klass: "A")
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
network.servers(force_reload: true).size.should eq 1
|
|
expect{ server.delete }.to change{ Server.count }.by(-1).and change{ Network.count }.by(0)
|
|
network.servers(force_reload: true).size.should eq 0
|
|
end
|
|
|
|
it "not fetch deleted objects - from both directions" do
|
|
# matrix gem removed in Ruby 3.4; skipping
|
|
network = Network.create(klass: "A")
|
|
server = Server.new
|
|
server.network_ids = [network.id]
|
|
server.save!
|
|
server.networks(force_reload: true).size.should eq 1
|
|
expect{ network.delete }.to change{ Network.count }.by(-1).and change{ Server.count }.by(0)
|
|
server.networks(force_reload: true).size.should eq 0
|
|
server.reload.network_ids.should eq []
|
|
end
|
|
end
|
|
|
|
|
|
context "with soft deleted" do
|
|
|
|
it "not load soft deleted - items storing keys" do
|
|
network = Network.create
|
|
router = Router.new
|
|
network.add_router(router)
|
|
network.routers.size.should eq 1
|
|
router.delete
|
|
Router.count.should eq 0
|
|
Router.count(with_deleted: true).should eq 1
|
|
network.routers(force_reload: true).size.should eq 0
|
|
network.routers(force_reload: true, with_deleted: true).size.should eq 1
|
|
router.delete!
|
|
network.routers(force_reload: true, with_deleted: true).size.should eq 0
|
|
end
|
|
|
|
it "not count soft deleted - items storing keys" do
|
|
network = Network.create
|
|
router = Router.new
|
|
network.add_router(router)
|
|
network.routers.size.should eq 1
|
|
router.delete
|
|
Router.count.should eq 0
|
|
Router.count(with_deleted: true).should eq 1
|
|
network.router_count(force_reload: true).should eq 0
|
|
network.router_count(force_reload: true, with_deleted: true).should eq 1
|
|
router.delete!
|
|
network.router_count(force_reload: true, with_deleted: true).should eq 0
|
|
end
|
|
|
|
it "not load soft deleted - items not storing keys: not supported", :couchdb_only do
|
|
book = Book.create
|
|
author = Author.create
|
|
author.add_book(book)
|
|
author.delete
|
|
Author.count.should eq 0
|
|
Author.count(with_deleted: true).should eq 1
|
|
book.authors(force_reload: true).size.should eq 0
|
|
book.authors(force_reload: true, with_deleted: true).size.should eq 0
|
|
end
|
|
|
|
it "not count soft deleted - items not storing keys: not supported", :couchdb_only do
|
|
book = Book.create
|
|
author = Author.create
|
|
author.add_book(book)
|
|
author.delete
|
|
Author.count.should eq 0
|
|
Author.count(with_deleted: true).should eq 1
|
|
book.author_count(force_reload: true).should eq 0
|
|
book.author_count(force_reload: true, with_deleted: true).should eq 0
|
|
end
|
|
|
|
end
|
|
|
|
context "when not persisted" do
|
|
it "not return objects when a record is not persisted on storing keys" do
|
|
router = Router.new
|
|
other_router = Router.create
|
|
network = Network.create
|
|
other_router.network_ids = [network.id]
|
|
other_router.save
|
|
router.networks.should be_empty
|
|
end
|
|
it "not return objects when a record is not persisted on non storing keys" do
|
|
router = Router.create
|
|
other_router = Router.create
|
|
other_network = Network.create
|
|
other_router.network_ids = [other_network.id]
|
|
network = Network.new
|
|
network.routers.should be_empty
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|