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>
This commit is contained in:
commit
86299b8827
84 files changed
+9904
No files matched your search
@@ -0,0 +1,112 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user