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>
194 lines
7.1 KiB
Ruby
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
|