139 lines
5.8 KiB
Markdown
139 lines
5.8 KiB
Markdown
# 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),
|
|
with Redis INCR/INCRBY semantics: the first increment of a missing key
|
|
returns the delta. One divergence: Couchbase counters are unsigned, so
|
|
decrement clamps at 0 instead of going negative.
|
|
- **Cache** — an `ActiveSupport::Cache::Store` over KV + native TTL; drop-in for
|
|
`Rails.cache` and therefore ActiveJob. (`clear` is a no-op — entries expire
|
|
via TTL.)
|
|
- **Lock** — distributed lock: insert-if-absent + mandatory TTL auto-expiry +
|
|
CAS-checked release, so only the holder can unlock (`force_release!` is the
|
|
explicit override) and a crashed holder can never leave an immortal lock.
|
|
|
|
## 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** (plus its own additions)
|
|
against a live Couchbase. See `SIMPLY_BASED_REVIEW.md` for the full
|
|
independent review this status section is based on.
|
|
|
|
- **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 (true batched KV multi-ops), eager-loading, embedded documents
|
|
(`has_many_embedded` / `is_embedded_in`, minus reverse lookups),
|
|
**ancestry / trees**, the Redis primitives, adapter-contract shared examples,
|
|
and multi-threaded concurrency examples (CAS conflicts, atomic counters,
|
|
lock mutual exclusion).
|
|
- **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.
|
|
- **Views that cannot translate to N1QL raise `SimplyBased::UnsupportedView`**
|
|
at query time (`:conditions` JS fragments, `:emit_value`, list functions)
|
|
instead of silently returning unfiltered results.
|
|
- **Excluded by design:**
|
|
- `:couchdb_only` specs — native inline attachments, JS-view token-safety,
|
|
design-document maintenance, raw custom JS views: no Couchbase equivalent
|
|
(use `has_s3_attached` / `has_local_attached`; N1QL uses bound parameters).
|
|
- `embedded_in` reverse-lookups (`:embedded_in_reverse_lookup`) — querying a
|
|
foreign key stored *inside* an embedded array element would need an
|
|
UNNEST-based query path; not implemented.
|
|
|
|
### Known gaps (unchanged, deliberate)
|
|
|
|
- **No multi-document transactions** — single-document atomicity + CAS only.
|
|
Couchbase's SDK has transactions; this ORM does not surface them.
|
|
- **No index-migration framework** — GSIs are created lazily per association;
|
|
ad-hoc queries need deliberately designed indexes or they fall back to a
|
|
primary scan (a warning is emitted when lazy index creation fails).
|
|
- `Cache#clear` is a no-op (namespaced clear would need a key scan); entries
|
|
expire via their TTLs.
|
|
|
|
```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.
|