bterkuileandClaude Sonnet 5 1beb8ead11 Fix embedded documents: recursion crash, .all/.count, honest spec exclusion
Live-run finding: the full suite against Couchbase was 423 examples with 17
failures — all in embedding_spec. The session report claimed embedded specs
were 'excluded by design', but the exclusion filter targeted a :embedding tag
no example carries, so the file ran (and failed) all along.

Three fixes:
- HasManyEmbedded::Property#build wired parent_object before installing the
  collection ivar on the parent; parent_object= reaches back into the parent's
  collection getter, which re-entered build -> SystemStackError on any access
  to embedded documents loaded from the database. Install the ivar first.
- EmbeddedClass.all / .count referenced raw JS views (all_documents /
  all_documents_for_count) that the N1QL port silently drops, so .all returned
  [] and .count raised NoMethodError. Rebuilt them by enumerating the parent
  documents (same work as the old view scan, client-side; an UNNEST query can
  replace it later).
- Only reverse lookups (N1QL on a foreign key inside an embedded array) remain
  unsupported: tagged :embedded_in_reverse_lookup and excluded in spec_helper
  with an explanatory comment, replacing the broken :embedding filter line.

embedding_spec.rb: 16 examples, 0 failures against live Couchbase 8.0.1 CE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 18:35:51 -05:00

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::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.

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_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 — 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.

S
Description
No description provided
Readme
191 KiB
0 Stars 1 Watchers 0 Forks
Languages
Ruby 100%