# SimplyBased A **Couchbase-native** ActiveModel ORM — the same API as [simply_couch](https://github.com/bterkuile/simply_couch) (CouchDB), running on Couchbase via the official SDK. Swap one line: ```ruby - 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. ```ruby 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: ```sql 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: ```ruby 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::Store` over KV + native TTL; drop-in for `Rails.cache` and 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. - **378 / 432 applicable examples green (~88%).** - 12 CouchDB-mechanic specs are excluded (`:couchdb_only`) — native inline attachments and JS-view token-safety have no Couchbase equivalent (use `has_s3_attached` / `has_local_attached`; N1QL uses bound parameters). - **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, bulk, eager-loading, the Redis primitives. - **Remaining (tracked):** `ancestry` (tree/subtree — recursive N1QL) and `embedded_in` reverse-lookups, plus a handful of pagination/lifecycle edge cases. These were the recursive/embedded custom views; they need per-pattern N1QL and are the next work item. ```bash 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.