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. Audit custom overrides of changes_applied / move_changes
ActiveModel::Dirty is no longer included in CouchbaseOrm::Base. The public dirty-tracking API (changed?, changes, attribute_changed?, <attr>_was, <attr>_changed?, <attr>_change, <attr>_previously_was, saved_change_to_<attr>, will_save_change_to_<attr>?, attribute_will_change!, reset_attribute!, previous_changes, …) is still available: it is re-implemented natively by CouchbaseOrm::Changeable. Existing call sites that use those methods do not need to change.
What does change is the inheritance chain. If you have custom modules or concerns that call super inside dirty-tracking lifecycle hooks (changes_applied, move_changes, …), that super used to dispatch to ActiveModel::Dirty and is now unreachable.
# BEFORE
def changes_applied
move_changes
super # ← reached ActiveModel::Dirty#changes_applied
end
# AFTER
def changes_applied
move_changes
# do NOT call super — ActiveModel::Dirty is no longer in the chain
endA small number of ActiveModel::Dirty helpers that Changeable does not re-implement are also gone. If your code uses any of these, migrate them:
Removed (from ActiveModel::Dirty) |
Replacement in Changeable |
|---|---|
restore_attributes |
iterate over changed and call reset_attribute! |
restore_<attr>! |
reset_<attr>! (or reset_attribute!(:<attr>)) |
clear_changes_information |
reset_object! |
clear_attribute_changes(names) |
call reset_attribute!(name) per name |
<attr>_will_change! (per-attribute) |
attribute_will_change!(:<attr>) |
2. 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 on the public API: minimal. The dirty-tracking helpers used in day-to-day code (changed?, changes, attribute_changed?, <attr>_was, <attr>_changed?, <attr>_change, <attr>_previously_was, saved_change_to_<attr>, will_save_change_to_<attr>?, attribute_will_change!, reset_attribute!, previous_changes, changed_attributes, …) are re-implemented in Changeable and continue to work unchanged.
What actually breaks:
supercalls inside custom overrides ofchanges_applied,move_changes, and similar lifecycle hooks now raiseNoMethodError(noActiveModel::Dirtylink in the chain).- A small set of
ActiveModel::Dirtyhelpers thatChangeabledoes not expose:restore_attributes,restore_<attr>!,clear_changes_information,clear_attribute_changes, and the per-attribute<attr>_will_change!. See the migration table in Upgrade Step 1.
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 ofdoc.created_atre-rancast(integer), which evaluated asTime.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.