Skip to content

Releases: Couchbase-Ecosystem/couchbase-ruby-orm

3.0.1

31 Mar 08:43
6594426

Choose a tag to compare

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
end

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 UTC

If 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
end

3. 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
end

Breaking 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!: reading doc.created_at returned the cached cast
    value, which in v2.0.6 was value.utc — full sub-second precision
    preserved.
  • After save!: forget_attribute_assignments serialized the timestamp
    to an integer (Time#to_i), replacing the cached object. The next read of
    doc.created_at re-ran cast(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.