From 650ecdd97fa12ac9852aff6f0e2869eff4ff5fc7 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Mon, 6 Jan 2025 15:33:14 +0100 Subject: [PATCH 01/12] Add failing test to reproduce the unexpected behaviour --- spec/type_nested_spec.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb index 958fd56..6020e56 100644 --- a/spec/type_nested_spec.rb +++ b/spec/type_nested_spec.rb @@ -42,6 +42,12 @@ class TypeNestedTest < CouchbaseOrm::Base obj.others[1].child = SubTypeTest.new(name: "baz") obj.save! + expect(obj.others[0].name).to eq "foo" + expect(obj.others[0].tags).to eq ["foo", "bar"] + expect(obj.others[1].name).to eq "bar" + expect(obj.others[1].tags).to eq ["bar", "baz"] + expect(obj.others[1].child.name).to eq "baz" + obj = TypeNestedTest.find(obj.id) expect(obj.others[0].name).to eq "foo" expect(obj.others[0].tags).to eq ["foo", "bar"] @@ -116,7 +122,8 @@ class TypeNestedTest < CouchbaseOrm::Base obj.others[1].name = "baz" obj.flags[0] = true - obj.save! + expect { obj.save! }.to_not change { [obj.main.name, obj.others[0].name, obj.others[1].name, obj.flags] } + obj = TypeNestedTest.find(obj.id) expect(obj.main.name).to eq "bar" expect(obj.others[0].name).to eq "bar" From 1f7ac0400a1ef9cf2361061fbea83d5a70f4a0df Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Mon, 6 Jan 2025 18:36:27 +0100 Subject: [PATCH 02/12] remove Active Model dirty --- lib/couchbase-orm/base.rb | 2 +- lib/couchbase-orm/changeable.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 9a3bdb7..e3d301b 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -33,7 +33,7 @@ module CouchbaseOrm class Document include Inspectable include ::ActiveModel::Model - include ::ActiveModel::Dirty + # include ::ActiveModel::Dirty include Changeable # override some methods from ActiveModel::Dirty (keep it included after) include ::ActiveModel::Attributes include ::ActiveModel::Serializers::JSON diff --git a/lib/couchbase-orm/changeable.rb b/lib/couchbase-orm/changeable.rb index ba852eb..2f369df 100644 --- a/lib/couchbase-orm/changeable.rb +++ b/lib/couchbase-orm/changeable.rb @@ -80,7 +80,7 @@ def move_changes def changes_applied move_changes - super + # super end def reset_object! From a5ec52a5711f0ab39ecce6b22a21bb05ccc7cd6c Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Mon, 6 Jan 2025 18:36:48 +0100 Subject: [PATCH 03/12] dirty fix with a reload --- lib/couchbase-orm/persistence.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index ea85e87..8b09790 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -97,6 +97,7 @@ def save(**options) # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) self.class.fail_validate!(self) unless self.save(**options) + self.reload # this fix the issue but it's not acceptable since it doing an extra request self end @@ -263,12 +264,15 @@ def _create_record(*_args, **options) if options[:transcoder].nil? options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config) end - resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + pp self.some_time + resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + pp self.some_time # Ensure the model is up to date @__metadata__.cas = resp.cas changes_applied + pp self.some_time true end end From fa9bca5edb5c0bd49a45769bce06d216ff1bab06 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Tue, 7 Jan 2025 17:33:50 +0100 Subject: [PATCH 04/12] remove dirty fix NB : in AR, the cast is done at assignment time. Lets try to do it. --- lib/couchbase-orm/persistence.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 8b09790..6e69edc 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -97,7 +97,7 @@ def save(**options) # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) self.class.fail_validate!(self) unless self.save(**options) - self.reload # this fix the issue but it's not acceptable since it doing an extra request + # self.reload # this fix the issue but it's not acceptable since it doing an extra request self end @@ -264,15 +264,12 @@ def _create_record(*_args, **options) if options[:transcoder].nil? options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config) end - - pp self.some_time + pp serialized_attributes resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) - pp self.some_time # Ensure the model is up to date @__metadata__.cas = resp.cas changes_applied - pp self.some_time true end end From 53f052ef7e0d371da79ef333266da397891998c1 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Tue, 7 Jan 2025 18:07:17 +0100 Subject: [PATCH 05/12] Working but strange implemntation. TODO : fix move casting stuff from serialize and place them in cast obviously --- lib/couchbase-orm/changeable.rb | 4 +++- lib/couchbase-orm/persistence.rb | 1 - lib/couchbase-orm/utilities/query_helper.rb | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/couchbase-orm/changeable.rb b/lib/couchbase-orm/changeable.rb index 2f369df..bf9691e 100644 --- a/lib/couchbase-orm/changeable.rb +++ b/lib/couchbase-orm/changeable.rb @@ -367,8 +367,10 @@ def create_dirty_methods(name, meth) def create_setters(name) define_method("#{name}=") do |new_attribute_value| + type = self.class.attribute_types[name.to_s] + casted_value = type.cast( type.serialize type.cast new_attribute_value) previous_value = attributes[name.to_s] - ret = super(new_attribute_value) + ret = super(casted_value) if previous_value != attributes[name.to_s] changed_attributes.merge!(Hash[name, [previous_value, attributes[name.to_s]]]) end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 6e69edc..9e0a32e 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -264,7 +264,6 @@ def _create_record(*_args, **options) if options[:transcoder].nil? options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config) end - pp serialized_attributes resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date @__metadata__.cas = resp.cas diff --git a/lib/couchbase-orm/utilities/query_helper.rb b/lib/couchbase-orm/utilities/query_helper.rb index 0df2f55..581c409 100644 --- a/lib/couchbase-orm/utilities/query_helper.rb +++ b/lib/couchbase-orm/utilities/query_helper.rb @@ -120,10 +120,10 @@ def serialize_value(key, value_before_type_cast) value = if value_before_type_cast.is_a?(Array) value_before_type_cast.map do |v| - attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v)) + attribute_types[key.to_s].serialize(v) end else - attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) + attribute_types[key.to_s].serialize(value_before_type_cast) end CouchbaseOrm.logger.debug { "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" } value From a181cfcda4d957ad00544ca34a354326df9b59da Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Wed, 8 Jan 2025 09:27:44 +0100 Subject: [PATCH 06/12] Fix weird implem --- lib/couchbase-orm/changeable.rb | 2 +- lib/couchbase-orm/types/timestamp.rb | 2 +- spec/type_spec.rb | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/couchbase-orm/changeable.rb b/lib/couchbase-orm/changeable.rb index bf9691e..6e88d7a 100644 --- a/lib/couchbase-orm/changeable.rb +++ b/lib/couchbase-orm/changeable.rb @@ -368,7 +368,7 @@ def create_dirty_methods(name, meth) def create_setters(name) define_method("#{name}=") do |new_attribute_value| type = self.class.attribute_types[name.to_s] - casted_value = type.cast( type.serialize type.cast new_attribute_value) + casted_value = type.cast new_attribute_value previous_value = attributes[name.to_s] ret = super(casted_value) if previous_value != attributes[name.to_s] diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index d3643e8..da43019 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -2,11 +2,11 @@ module CouchbaseOrm module Types class Timestamp < ActiveModel::Type::DateTime def cast(value) + value = super(value) return nil if value.nil? return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ return value.utc if value.is_a?(Time) - super(value).utc end def serialize(value) diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 6cf2290..c2553b3 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -4,9 +4,20 @@ require "couchbase-orm/types" class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime - def serialize(value) - value&.iso8601(3) - end + def initialize + super + @precision=3 + end +# def serialize(value) +# value&.iso8601(precision) +# end +# def cast(value) +# value = super(value) +# return nil if value.nil? +# return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) +# return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ +# return value if value.is_a?(Time) +# end end ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) @@ -17,7 +28,7 @@ class TypeTest < CouchbaseOrm::Base attribute :size, :float attribute :renewal_date, :date attribute :subscribed_at, :datetime - attribute :some_time, :timestamp + attribute :some_time, :timestamp, precision: 0 attribute :precision3_time, :datetime3decimal attribute :precision6_time, :datetime, precision: 6 From 3d71024297df291f91624355691d2df3d8c9779b Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Thu, 16 Jan 2025 16:37:05 +0100 Subject: [PATCH 07/12] Apply precision on timestamp for any input type And keep unchanged the serialize_value needed for queries (ie find_by_some_time) --- lib/couchbase-orm/types/timestamp.rb | 19 +++++++++++++------ lib/couchbase-orm/utilities/query_helper.rb | 6 +++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index da43019..60b3502 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -2,13 +2,20 @@ module CouchbaseOrm module Types class Timestamp < ActiveModel::Type::DateTime def cast(value) - value = super(value) - return nil if value.nil? - return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) - return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ - return value.utc if value.is_a?(Time) + return nil if value.nil? + + value = if value.is_a?(Integer) || value.is_a?(Float) + Time.at(value) + elsif value.is_a?(String) && value =~ /^[0-9]+$/ + Time.at(value.to_i) + elsif value.is_a?(Time) + value.utc + else + value + end + super(value) end - + def serialize(value) value&.to_i end diff --git a/lib/couchbase-orm/utilities/query_helper.rb b/lib/couchbase-orm/utilities/query_helper.rb index 581c409..a8b3d35 100644 --- a/lib/couchbase-orm/utilities/query_helper.rb +++ b/lib/couchbase-orm/utilities/query_helper.rb @@ -117,13 +117,13 @@ def build_not_match(key, value) end def serialize_value(key, value_before_type_cast) - value = + value = if value_before_type_cast.is_a?(Array) value_before_type_cast.map do |v| - attribute_types[key.to_s].serialize(v) + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v)) end else - attribute_types[key.to_s].serialize(value_before_type_cast) + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) end CouchbaseOrm.logger.debug { "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" } value From a5feceb123126336defcc06690f8481213c010ae Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Thu, 16 Jan 2025 16:55:12 +0100 Subject: [PATCH 08/12] cleannup --- lib/couchbase-orm/base.rb | 3 +-- lib/couchbase-orm/changeable.rb | 1 - lib/couchbase-orm/persistence.rb | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index e3d301b..e7ce801 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -33,8 +33,7 @@ module CouchbaseOrm class Document include Inspectable include ::ActiveModel::Model - # include ::ActiveModel::Dirty - include Changeable # override some methods from ActiveModel::Dirty (keep it included after) + include Changeable include ::ActiveModel::Attributes include ::ActiveModel::Serializers::JSON diff --git a/lib/couchbase-orm/changeable.rb b/lib/couchbase-orm/changeable.rb index 6e88d7a..4d05d6c 100644 --- a/lib/couchbase-orm/changeable.rb +++ b/lib/couchbase-orm/changeable.rb @@ -80,7 +80,6 @@ def move_changes def changes_applied move_changes - # super end def reset_object! diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 9e0a32e..ea85e87 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -97,7 +97,6 @@ def save(**options) # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) self.class.fail_validate!(self) unless self.save(**options) - # self.reload # this fix the issue but it's not acceptable since it doing an extra request self end @@ -265,6 +264,7 @@ def _create_record(*_args, **options) options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config) end resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + # Ensure the model is up to date @__metadata__.cas = resp.cas From 0765ab95bc094779ab90ce1aa2c12fa1f24d22d0 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Thu, 16 Jan 2025 17:08:12 +0100 Subject: [PATCH 09/12] Rm comments --- spec/type_spec.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spec/type_spec.rb b/spec/type_spec.rb index c2553b3..a59b63f 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -8,16 +8,6 @@ def initialize super @precision=3 end -# def serialize(value) -# value&.iso8601(precision) -# end -# def cast(value) -# value = super(value) -# return nil if value.nil? -# return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) -# return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ -# return value if value.is_a?(Time) -# end end ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) From 5c56c7744b943b9e33cc0579ad935afb0895b8c0 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Tue, 24 Jun 2025 17:34:32 +0200 Subject: [PATCH 10/12] undo unwanted changes --- lib/couchbase-orm/types/timestamp.rb | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index 60b3502..d3643e8 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -2,20 +2,13 @@ module CouchbaseOrm module Types class Timestamp < ActiveModel::Type::DateTime def cast(value) - return nil if value.nil? - - value = if value.is_a?(Integer) || value.is_a?(Float) - Time.at(value) - elsif value.is_a?(String) && value =~ /^[0-9]+$/ - Time.at(value.to_i) - elsif value.is_a?(Time) - value.utc - else - value - end - super(value) + return nil if value.nil? + return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) + return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ + return value.utc if value.is_a?(Time) + super(value).utc end - + def serialize(value) value&.to_i end From b4a62800a7acd775bfcd4ccdbcb588f20e8b83b4 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Tue, 24 Jun 2025 17:38:24 +0200 Subject: [PATCH 11/12] undo unwanted change --- spec/type_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/type_spec.rb b/spec/type_spec.rb index a59b63f..6cf2290 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -4,10 +4,9 @@ require "couchbase-orm/types" class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime - def initialize - super - @precision=3 - end + def serialize(value) + value&.iso8601(3) + end end ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) @@ -18,7 +17,7 @@ class TypeTest < CouchbaseOrm::Base attribute :size, :float attribute :renewal_date, :date attribute :subscribed_at, :datetime - attribute :some_time, :timestamp, precision: 0 + attribute :some_time, :timestamp attribute :precision3_time, :datetime3decimal attribute :precision6_time, :datetime, precision: 6 From 915306226aa9380b7bddd3cc5f0a2bfaace5d11e Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Mon, 25 Aug 2025 17:52:20 +0200 Subject: [PATCH 12/12] Fix type casting and precision issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove double casting in create_setters to avoid type inconsistencies - Fix Timestamp type precision by applying floor() during cast - Fix DateTimeWith3Decimal precision handling in both cast and serialize 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/couchbase-orm/changeable.rb | 4 +--- lib/couchbase-orm/types/timestamp.rb | 8 ++++---- spec/type_spec.rb | 5 +++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/couchbase-orm/changeable.rb b/lib/couchbase-orm/changeable.rb index 4d05d6c..712acf6 100644 --- a/lib/couchbase-orm/changeable.rb +++ b/lib/couchbase-orm/changeable.rb @@ -366,10 +366,8 @@ def create_dirty_methods(name, meth) def create_setters(name) define_method("#{name}=") do |new_attribute_value| - type = self.class.attribute_types[name.to_s] - casted_value = type.cast new_attribute_value previous_value = attributes[name.to_s] - ret = super(casted_value) + ret = super(new_attribute_value) if previous_value != attributes[name.to_s] changed_attributes.merge!(Hash[name, [previous_value, attributes[name.to_s]]]) end diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index d3643e8..9552de7 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -3,10 +3,10 @@ module Types class Timestamp < ActiveModel::Type::DateTime def cast(value) return nil if value.nil? - return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) - return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ - return value.utc if value.is_a?(Time) - super(value).utc + return Time.at(value).floor if value.is_a?(Integer) || value.is_a?(Float) + return Time.at(value.to_i).floor if value.is_a?(String) && value =~ /^[0-9]+$/ + return value.utc.floor if value.is_a?(Time) + super(value).utc.floor end def serialize(value) diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 6cf2290..e74cba2 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -4,6 +4,11 @@ require "couchbase-orm/types" class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime + def cast(value) + result = super(value) + result&.floor(3) + end + def serialize(value) value&.iso8601(3) end