Releases: Couchbase-Ecosystem/couchbase-ruby-orm
3.0.1
CouchbaseORM 3.0.1 — Release Notes
Diff : 2.0.6...3.0.1
Upgrade Steps
1. Replace ActiveModel::Dirty API calls
ActiveModel::Dirty is no longer included in CouchbaseOrm::Base. Any code
that relied on its API must be migrated to the equivalent Changeable methods.
# BEFORE — ActiveModel::Dirty API (no longer available)
doc.changed? # => true/false
doc.changes # => { "field" => [old, new] }
doc.attribute_changed?(:name) # => true/false
doc.name_was # => old value
doc.name_changed? # => true/false
# AFTER — use CouchbaseOrm::Changeable instead
doc.changed? # still works (defined in Changeable)
doc.changes # still works (defined in Changeable)
# attribute_changed?, _was, _changed? helpers from ActiveModel::Dirty
# are NO LONGER available; inspect doc.changes directly:
doc.changes.key?("name") # => true if :name changed
doc.changes["name"]&.first # => old value
If you have custom modules or concerns that call super inside dirty-tracking
callbacks (changes_applied, move_changes, etc.), remove those super calls
or guard them:
# BEFORE
def changes_applied
move_changes
super # ← called ActiveModel::Dirty#changes_applied, now raises NoMethodError
end
# AFTER
def changes_applied
move_changes
# do NOT call super — no longer safe
end2. Audit timestamp fields for sub-second precision loss
Timestamp#cast now applies .floor (truncate to whole seconds) on every
input path. If your application stores or queries timestamps with millisecond
or microsecond precision, you will silently lose that precision on read.
# BEFORE — sub-second precision preserved on cast
doc.created_at = Time.now # e.g. 2024-03-01 12:00:00.987654 UTC
doc.created_at # => 2024-03-01 12:00:00.987654 UTC
# AFTER — precision is floored to the second
doc.created_at = Time.now
doc.created_at # => 2024-03-01 12:00:00.000000 UTCIf your domain requires sub-second timestamps, define a custom type and
override both cast and serialize:
class MillisecondTimestamp < CouchbaseOrm::Types::Timestamp
def cast(value)
result = super(value)
result&.floor(3) # keep millisecond precision
end
def serialize(value)
value&.to_f # store as float with ms precision
end
end3. Update custom DateTime subtype overrides
If you subclass CouchbaseOrm::Types::DateTime and only override serialize,
you must now also override cast to keep precision consistent between the
in-memory value and the serialized value.
# BEFORE — only serialize was overridden
class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime
def serialize(value)
value&.iso8601(3) # 3 decimal places on write
end
# cast was inherited → returned full precision in memory
end
# AFTER — also override cast to match
class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime
def cast(value)
super(value)&.floor(3) # truncate to 3 decimal places on read
end
def serialize(value)
value&.iso8601(3)
end
endBreaking Changes
Removal of ActiveModel::Dirty
ActiveModel::Dirty is no longer included in CouchbaseOrm::Base. The
library now relies exclusively on its own Changeable module for change
tracking.
Impact: Any call to ActiveModel::Dirty-specific methods
(attribute_changed?, *_was, *_changed?, restore_attributes, etc.)
will raise NoMethodError at runtime.
Motivation: ActiveModel::Dirty#changes_applied was resetting the
internal change-tracking state and silently discarding nested document
changes after a save!. Removing the dependency fixes correctness for
nested documents and gives Changeable full control over the lifecycle.
Timestamp values are now floored to whole seconds
The Timestamp type truncates sub-second precision during cast. This
affects every path: assignment from Integer, Float, String (numeric),
Time, and the generic super path.
Impact: Any timestamp that had sub-second precision will lose it as
soon as the attribute is read back from the model. Existing documents in
Couchbase are not retroactively affected, but the value returned by the
accessor will differ from what was originally stored.
Cast is now applied at assignment time, not at save time
In v2.0.6, ActiveModel::Dirty was included alongside Changeable. Its
changes_applied method — called internally by save! — invoked
forget_attribute_assignments, a Rails hook that serializes every attribute
to its database form and replaces the in-memory @attributes set with the
resulting "from-database" objects.
For Timestamp attributes, this meant:
- Before
save!: readingdoc.created_atreturned the cached cast
value, which in v2.0.6 wasvalue.utc— full sub-second precision
preserved. - After
save!:forget_attribute_assignmentsserialized the timestamp
to an integer (Time#to_i), replacing the cached object. The next read of
doc.created_atre-rancast(integer), which evaluated as
Time.at(integer)— whole-second precision only.
In other words, in v2.0.6 sub-second precision was silently lost not at
assignment, but at the first read following save!.
With the removal of ActiveModel::Dirty in v3.0.1, forget_attribute_assignments
is never called. The attribute object set by the user is kept in memory
unchanged across save! calls. To preserve the invariant that the in-memory
value matches what a round-trip through the database would return, cast now
applies .floor eagerly — at the first read after assignment, which in
practice occurs immediately inside the attribute setter (via
Changeable#create_setters).
The net effect: precision loss that used to be deferred until after save!
now happens as soon as the attribute is set.
New Features
No new features in this release.
Bug Fixes
Nested document changes lost after save!
Symptom: After calling save! on a document containing nested
sub-documents, subsequent reads of those nested fields on the same
in-memory object (without a reload) returned stale or incorrect values,
even though the data was correctly persisted to Couchbase.
Root cause: ActiveModel::Dirty#changes_applied was being called via
super inside Changeable#changes_applied, which reset the dirty-tracking
state in a way that conflicted with Changeable's own nested-document
tracking.
Fix: Removed the super call from Changeable#changes_applied and
removed the ActiveModel::Dirty inclusion entirely. The in-memory object
now correctly reflects its state immediately after save!, without
requiring a find/reload.
Improvements
Consistent timestamp precision across all cast paths
Before this release, the floor applied to Timestamp#cast was inconsistent:
some input types (e.g. passing a raw Integer epoch) preserved sub-second
precision while others did not. All four cast branches now uniformly apply
.floor, making the behaviour predictable regardless of input type.
Test coverage for in-memory nested document state
Specs for nested documents now assert correctness of the in-memory object
immediately after save! (not only after a round-trip find). This closes
the gap where the bug was invisible to the test suite until a reload.