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>
This commit is contained in:
bterkuileandClaude Sonnet 5 committed 2026-07-06 18:35:51 -05:00
1 parent d2202bda03
commit 1beb8ead11
4 files changed
+41 -11

No files matched your search

+16 -1
View File
@@ -94,8 +94,23 @@ module SimplyBased
:include_docs => true
end
# The CouchDB implementation used raw JS views (all_documents /
# all_documents_for_count) that emitted one row per embedded
# element. Those views are no-ops on Couchbase (see
# ClassMethods::Base#view), so enumerate through the parent
# documents instead: embedded records only exist inside their
# parent, and the parent getter wires parent_object/index.
# NOTE: this loads all parent documents — same order of work as
# the CouchDB view scan, but client-side. Fine for small/medium
# collections; revisit with an UNNEST query if it ever matters.
define_method :all do |*args|
parent_klass = Object.const_get(embedded_in_class_name)
embedded_property = self.name.property_name.pluralize
parent_klass.all.flat_map { |p| Array.wrap(p.send(embedded_property)) }
end
define_method :count do |options = {}|
database.view(all_documents_for_count(options.merge(:reduce => true)))['rows'].try(:first).try('[]', 'value').to_i
all.size
end
end
+13 -8
View File
@@ -145,17 +145,22 @@ module SimplyBased
def build(object, json)
values = Array.wrap(json[name]).map do |v|
obj = if v.is_a?(Hash)
options[:class_name].constantize.new.tap { |o| o._document = v }
else
v
end
obj.parent_object = object
obj
if v.is_a?(Hash)
options[:class_name].constantize.new.tap { |o| o._document = v }
else
v
end
end
values.define_singleton_method(:parent_object_set?) { true }
values.each_with_index { |v, i| v.index = i }
# Install the collection on the parent *before* wiring parent_object:
# parent_object= reaches back into the parent's collection getter (via
# the embedded_in "#{name}=" writer's include? check), and while the
# ivar is unset that getter re-enters build — infinite mutual
# recursion (SystemStackError).
object.instance_variable_set("@#{name}", values)
values.each_with_index { |v, i| v.index = i }
values.each { |v| v.parent_object = object }
values
end
def initialize(owner_clazz, name, options = {})