Files
simply_based/spec/support/adapter_contract.rb
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

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