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>
112 lines
3.5 KiB
Ruby
112 lines
3.5 KiB
Ruby
# frozen_string_literal: true
|
|
#
|
|
# The contract every SimplyBased::Adapter must satisfy, as shared examples.
|
|
# A backend proves parity by including it:
|
|
#
|
|
# RSpec.describe SimplyBased::Adapters::CouchRest do
|
|
# it_behaves_like 'a simply_couch adapter'
|
|
# end
|
|
#
|
|
# This same file is what the simply_couch-couchbase gem runs against a live
|
|
# Couchbase to demonstrate the backend is swappable. It assumes the suite's
|
|
# before(:each) leaves SimplyBased.database pointing at a fresh, empty store.
|
|
|
|
# A minimal model used only by the contract — independent of the fixture graph
|
|
# so the examples port cleanly to any backend.
|
|
class ContractWidget
|
|
include SimplyBased::Model
|
|
property :title
|
|
property :score, type: Integer
|
|
validates :title, presence: true
|
|
end
|
|
|
|
RSpec.shared_examples 'a simply_couch adapter' do
|
|
let(:adapter) { SimplyBased.database }
|
|
|
|
def build_widget(attrs = {})
|
|
ContractWidget.new({ title: 'a' }.merge(attrs))
|
|
end
|
|
|
|
describe '#save_document + #load_document' do
|
|
it 'persists a new document and assigns _id/_rev' do
|
|
w = build_widget(title: 'hello', score: 3)
|
|
expect(adapter.save_document(w)).to eq(true)
|
|
expect(w._id).not_to be_nil
|
|
expect(w._rev).not_to be_nil
|
|
end
|
|
|
|
it 'reconstructs the stored doc into its model class via the ruby_class tag' do
|
|
w = build_widget(title: 'hello', score: 7)
|
|
adapter.save_document(w)
|
|
loaded = adapter.load_document(w._id)
|
|
expect(loaded).to be_a(ContractWidget)
|
|
expect(loaded.title).to eq('hello')
|
|
expect(loaded.score).to eq(7)
|
|
expect(loaded.database).to eq(adapter)
|
|
end
|
|
|
|
it 'returns nil (does not raise) when the id is absent' do
|
|
expect(adapter.load_document('does-not-exist')).to be_nil
|
|
end
|
|
|
|
it 'refuses to persist an invalid document' do
|
|
expect(adapter.save_document(build_widget(title: nil))).to eq(false)
|
|
end
|
|
|
|
it 'updates an existing doc and rolls _rev forward' do
|
|
w = build_widget(title: 'v1')
|
|
adapter.save_document(w)
|
|
first_rev = w._rev
|
|
w.title = 'v2'
|
|
adapter.save_document(w)
|
|
expect(w._rev).not_to eq(first_rev)
|
|
expect(adapter.load_document(w._id).title).to eq('v2')
|
|
end
|
|
end
|
|
|
|
describe '#destroy_document' do
|
|
it 'removes the document' do
|
|
w = build_widget
|
|
adapter.save_document(w)
|
|
id = w._id
|
|
adapter.destroy_document(w)
|
|
expect(adapter.load_document(id)).to be_nil
|
|
end
|
|
end
|
|
|
|
describe 'bulk operations' do
|
|
it 'bulk_saves, bulk_loads (reconstructed) and bulk_destroys' do
|
|
a = build_widget(title: 'a')
|
|
b = build_widget(title: 'b')
|
|
result = adapter.bulk_save([a, b])
|
|
expect(result[:saved].size).to eq(2)
|
|
expect(a._id).not_to be_nil
|
|
|
|
loaded = adapter.bulk_load([a._id, b._id])
|
|
expect(loaded).to all(be_a(ContractWidget))
|
|
expect(loaded.map(&:title).sort).to eq(%w[a b])
|
|
|
|
adapter.bulk_destroy([a, b])
|
|
expect(adapter.bulk_load([a._id, b._id])).to eq([])
|
|
end
|
|
|
|
it 'bulk_load drops ids that are missing' do
|
|
a = build_widget
|
|
adapter.save_document(a)
|
|
expect(adapter.bulk_load([a._id, 'missing']).map(&:_id)).to eq([a._id])
|
|
end
|
|
end
|
|
|
|
describe 'optimistic concurrency' do
|
|
it 'maps a stale-write clash to SimplyBased::Conflict' do
|
|
w = build_widget(title: 'orig')
|
|
adapter.save_document(w)
|
|
stale = adapter.load_document(w._id)
|
|
w.title = 'winner'
|
|
adapter.save_document(w) # advances the stored revision
|
|
stale.title = 'loser'
|
|
expect { adapter.save_document(stale) }.to raise_error(SimplyBased::Conflict)
|
|
end
|
|
end
|
|
end
|