- database.view now raises SimplyBased::UnsupportedView when a declared view carries CouchDB-era options the N1QL translation silently ignored before: :conditions (a JS fragment) and :emit_value, plus CouchDB list functions. Silently dropping :conditions was the worst of these — a 'view :completed, key: :name, conditions: ...' query returned *unfiltered* results with no hint anything was wrong. Covered by unsupported_view_spec. - All column/property names interpolated into N1QL are now routed through quote_ident, which rejects backticks/backslashes. These names come from the model DSL rather than user input, but a smuggled backtick must never become query text. Class names in index predicates go through a proper string literal quoting helper for the same reason. - ensure_index: failures are still swallowed (an index build in progress must not take down queries) but now emit a warning naming the index, because the silent version left every subsequent query on a primary-index scan with no trace. The ensured-indexes bookkeeping is mutex-guarded for multithreaded servers (Puma). - bulk_load / bulk_destroy now actually batch (collection.get_multi / remove_multi — one KV round trip). The README already advertised 'bulk (batched KV)' but the code looped one get/remove per id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SimplyBased
A Couchbase-native ActiveModel ORM — the same API as simply_couch (CouchDB), running on Couchbase via the official SDK. Swap one line:
- include SimplyCouch::Model
+ include SimplyBased::Model
…and your models, associations, validations, callbacks and finders run on Couchbase: key/value for documents, N1QL/SQL++ + GSI for queries, CAS for optimistic concurrency. There is no CouchDB design document or JavaScript map/reduce anywhere — the hand-written association views become N1QL.
class User
include SimplyBased::Model
property :name
has_many :posts
validates :name, presence: true
end
SimplyBased.adapter ||= nil
SimplyBased.database_url =
"couchbase://Administrator:password@localhost/app?scope=_default&collection=_default"
user = User.create(name: "Ada") # KV insert
User.find(user.id) # KV get
User.all # N1QL SELECT
User.find_by_name("Ada") # N1QL WHERE name = $key
user.posts # N1QL WHERE user_id = $id (GSI auto-created)
user.post_count # N1QL COUNT(*)
Why it works with so little change
simply_couch's model layer (properties, type-casting, validations, callbacks,
dirty tracking, the association DSL, soft-delete, pagination) is already storage
-neutral. SimplyBased keeps all of it and replaces exactly one layer — the
SimplyBased::Store — which speaks Couchbase directly. Every association lookup
funnels through a handful of Store methods (find_by_foreign_key,
find_by_array_contains, …) that emit N1QL instead of querying JS views. So the
has_and_belongs_to_many join that needed two hand-written map/reduce functions
in CouchDB is now:
SELECT c.*, META(c).id, META(c).cas
FROM `bucket`.`_default`.`_default` AS c
WHERE c.ruby_class = $rc
AND ANY x IN c.`network_ids` SATISFIES x = $v END
backed by an array GSI created on first use.
Replaces Redis too
Same cluster, no second datastore:
counter = SimplyBased::Counter.new
counter.increment("jobs:processed") # server-side atomic INCR
Rails.application.config.cache_store =
SimplyBased::Cache.new(namespace: "app") # ActiveSupport::Cache::Store (KV + TTL)
SimplyBased::Lock.new.with_lock("rebuild", ttl: 30) do
# critical section — distributed lock via insert-if-absent + CAS
end
- Counter — Couchbase binary increment/decrement (true atomic counters).
- Cache — an
ActiveSupport::Cache::Storeover KV + native TTL; drop-in forRails.cacheand therefore ActiveJob. - Lock — distributed lock: insert-if-absent + TTL auto-expiry + CAS-checked release so only the holder can unlock.
The one recommended pattern shift
v0 uses a single collection with a ruby_class discriminator (closest to
CouchDB, runs the whole suite as-is). For production, map each model to its own
Couchbase Collection — tighter GSIs, cleaner queries, idiomatic Couchbase. A
collection :name per-model DSL is the planned opt-in.
Status & test coverage
SimplyBased runs the simply_couch test suite against a live Couchbase.
- 403 / 403 applicable examples green on a clean run (one ancestry reparent edge in the namespaced cascade is intermittently flaky).
- Green: CRUD, finders (
find,all,first,find_by_*,count_by_*),belongs_to/has_many/has_one/has_and_belongs_to_many, validations, callbacks, dirty tracking, soft-delete (incl. soft-delete-aware finders), pagination, bulk, eager-loading, ancestry / trees, and the Redis primitives. - Ancestry is a materialized path (
path_ids, root→self): children =path_ids[len-2] = $id, subtree =$id IN path_ids, roots =ARRAY_LENGTH(path_ids) = 1— plain N1QL, no recursive view build. - Excluded by design:
:couchdb_onlyspecs — native inline attachments, JS-view token-safety, design-document maintenance, raw custom JS views: no Couchbase equivalent (usehas_s3_attached/has_local_attached; N1QL uses bound parameters).embedded_inreverse-lookups — out of scope for this gem.
docker run -d --name cb -p 8091-8096:8091-8096 -p 11210:11210 couchbase:community
# init a single-node cluster + a couchstore bucket named simply_based_test, then:
COUCHBASE_URL="couchbase://Administrator:password@localhost/simply_based_test" \
bundle exec rspec
License
MIT.