Files
simply_based/lib/simply_based/model/has_many_embedded.rb
T
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

194 lines
7.1 KiB
Ruby

module SimplyBased
module Model
module HasManyEmbedded
def has_many_embedded(name, options = {})
check_existing_properties(name, SimplyBased::Model::HasMany::Property)
properties << SimplyBased::Model::HasManyEmbedded::Property.new(self, name, options)
end
def define_has_many_embedded_getter(name, options)
# Make an alias to the property getter.
property_getter_method = "#{name}_property_getter"
alias_method property_getter_method, name
define_method(name) do |*args|
current = instance_variable_get("@#{name}")
if current && current.respond_to?(:parent_object_set?)
current.each { |o| o.parent_object = self }
return current
end
# Rebuild current, ensure parent object
current = Array.wrap(self.send(property_getter_method)).map do |h|
if h.is_a?(options[:class_name].constantize)
o = h
o.parent_object = self
else
o = options[:class_name].constantize.new
o._document = h
o.parent_object = self
end
o
end
def current.parent_object_set?
true
end
instance_variable_set("@#{name}", current)
return current
end
end
def define_has_many_embedded_setter(name, options)
define_method("#{name}=") do |values|
klass = self.class.get_class_from_name(name)
raise ArgumentError, "expected Array got #{values.class}" unless values.is_a?(Array)
instance_variable_set("@#{name}", []) unless instance_variable_get("@#{name}")
iid = 0
for value in values
if value.is_a?(Hash)
newval = klass.new
newval._document = value
newval.updated_at ||= Time.now
newval.created_at ||= Time.now
else
newval = value
end
newval.index = iid
instance_variable_get("@#{name}") << newval
iid += 1
end
current = instance_variable_get("@#{name}")
current.define_singleton_method(:parent_object_set?) { true }
save
end
end
def define_has_many_embedded_setter_add(name, options)
define_method("add_#{name.to_s.singularize}") do |value|
klass = self.class.get_class_from_name(name)
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
if value.is_a?(Hash)
newval = klass.new
newval._document = value
newval.updated_at ||= Time.now
newval.created_at ||= Time.now
else
newval = value
end
newval.index = (instance_variable_get("@#{name}") || []).size
instance_variable_get("@#{name}") << newval
save
end
end
def define_has_many_embedded_setter_remove(name, options)
define_method "remove_#{name.to_s.singularize}" do |value|
klass = self.class.get_class_from_name(name)
if value.is_a?(klass)
found = instance_variable_get("@#{name}").delete(value)
if found
self.send("reset_#{name}_index_values")
end
else
raise ArgumentError, "expected #{klass} got #{value.class}"
end
return save
end
end
def define_reset_index_values(name, options)
define_method "reset_#{name}_index_values" do
i = 0
for embedded_document in (instance_variable_get("@#{name}") || [])
embedded_document.index = i
i += 1
end
end
end
def define_has_many_embedded_setter_remove_all(name, options)
define_method "remove_all_#{name}" do
all = send("#{name}", force_reload: true)
all.collect { |i| i }.each do |item|
send("remove_#{name.to_s.singularize}", item)
end
end
end
def define_has_many_embedded_count(name, options, through = nil)
method_name = name.to_s.singularize.underscore.gsub('/', '__') + '_count'
define_method(method_name) do |*args|
local_options = args.first && args.first.is_a?(Hash) && args.first
if local_options
local_options.assert_valid_keys(:force_reload, :with_deleted)
forced_reload = local_options[:force_reload]
with_deleted = local_options[:with_deleted]
else
forced_reload = false
with_deleted = false
end
if forced_reload || instance_variable_get("@#{method_name}").nil?
instance_variable_set("@#{method_name}", count_associated(through || options[:class_name], self.class, with_deleted: with_deleted, foreign_key: options[:foreign_key]))
end
instance_variable_get("@#{method_name}")
end
end
def set_parent_has_many_embedded_association_object(parent, child_collection)
child_collection.each do |child|
if child.respond_to?("#{parent.class.name.to_s.singularize.downcase}=")
child.send("#{parent.class.name.to_s.singularize.camelize.downcase}=", parent)
end
end
end
class Property < SimplyBased::Model::AssociationProperty
def build(object, json)
values = Array.wrap(json[name]).map do |v|
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 }
# 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 = {})
options = {
dependent: :nullify,
through: nil,
class_name: name.to_s.singularize.camelize,
foreign_key: nil
}.update(options)
@name, @options = name, options
options.assert_valid_keys(:dependent, :through, :class_name, :foreign_key)
owner_clazz.class_eval do
property name, type: Array
_define_cache_accessors(name, options)
define_has_many_embedded_getter(name, options)
define_has_many_embedded_setter(name, options)
define_has_many_embedded_setter_add(name, options)
define_has_many_embedded_setter_remove(name, options)
define_has_many_embedded_setter_remove_all(name, options)
define_reset_index_values(name, options)
define_has_many_embedded_count(name, options)
end
end
end
end
end
end