Module: TypedEAV::Field::TypedStorage

Extended by:
ActiveSupport::Concern
Included in:
Base
Defined in:
lib/typed_eav/field/typed_storage.rb

Overview

One concern owns the entire native-typed-column storage seam.

Field types declare WHICH typed column(s) hold their value via the class-level DSL (‘value_column`, `value_columns`, `operators`, `operator_column`), and override three instance methods to compose multi-cell value shapes (`read_value`, `write_value`, `apply_default`). Snapshot/change-detection helpers (`value_changed?`, `before_snapshot`, `after_snapshot`) are concrete and derive from `value_columns`; they are NOT overridable — the snapshot shape is a versioning-coupled invariant.

## Class-level DSL

value_column :integer_value           # single-cell sugar; primary cell
value_columns :decimal_value, :string_value  # plural form
operators :eq, :gt, :is_null          # restrict supported operators
operator_column(:currency_eq)         # override to route ops to cells

Both ‘value_column` and `value_columns` share the same `@value_columns` class instance variable. `value_column` returns the first element of `value_columns`, preserving the single-cell sugar shape.

## Override-point instance methods (the entire extension surface)

  • ‘read_value(record)` — compose the logical value from the cells.

  • ‘write_value(record, casted)` — unpack the casted value across cells.

  • ‘apply_default(record)` — populate cells from the field’s default.

The default implementations target ‘value_columns.first` (single-cell behavior). Multi-cell types override ALL THREE — overriding just one creates an asymmetry where reads see the multi-cell shape but writes / defaults populate only one column (or vice versa).

## Concrete (non-overridable) snapshot helpers

  • ‘value_changed?(record)` — true iff ANY value_columns column has a saved_change_to_attribute? — used by the Value :update dispatch gate so multi-cell types fire the event when only the second cell changed.

  • ‘before_snapshot(record, change_type)` — per-column hash keyed by string column names. `:create` returns `{}`.

  • ‘after_snapshot(record, change_type)` — per-column hash keyed by string column names. `:destroy` returns `{}`.

Snapshot keys are stringified so query patterns like ‘WHERE before_value->>’integer_value’ = ‘42’‘ work uniformly.

Constant Summary collapse

DEFAULT_OPERATORS_BY_COLUMN =
{
  boolean_value: %i[eq not_eq is_null is_not_null],
  string_value: %i[eq not_eq contains not_contains starts_with ends_with is_null is_not_null],
  text_value: %i[eq not_eq contains not_contains starts_with ends_with is_null is_not_null],
  integer_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
  decimal_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
  date_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
  datetime_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
  json_value: %i[contains is_null is_not_null],
}.freeze
FALLBACK_OPERATORS =
%i[eq not_eq is_null is_not_null].freeze

Instance Method Summary collapse

Instance Method Details

#after_snapshot(value_record, change_type) ⇒ Object

Post-change snapshot keyed by string column names.

  • :create / :update → => value_record

  • :destroy → {} (no after state)



193
194
195
196
197
198
199
200
201
202
# File 'lib/typed_eav/field/typed_storage.rb', line 193

def after_snapshot(value_record, change_type)
  case change_type.to_sym
  when :create, :update
    self.class.value_columns.to_h { |column| [column.to_s, value_record[column]] }
  when :destroy
    {}
  else
    raise ArgumentError, "Unsupported change_type: #{change_type.inspect}"
  end
end

#apply_default(value_record) ⇒ Object

Writes this field’s configured default to ‘value_record`. Default writes `default_value` to the primary cell, bypassing Value#value= to avoid re-casting an already-cast default. Override in multi-cell types to populate multiple cells from a composite default.



154
155
156
# File 'lib/typed_eav/field/typed_storage.rb', line 154

def apply_default(value_record)
  value_record[self.class.value_columns.first] = default_value
end

#before_snapshot(value_record, change_type) ⇒ Object

Pre-change snapshot keyed by string column names.

  • :create → {} (no before state)

  • :update → => attribute_before_last_save(col)

  • :destroy → => value_record (in-memory on the destroyed AR record per Phase 03 P04 live-validation)



175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/typed_eav/field/typed_storage.rb', line 175

def before_snapshot(value_record, change_type)
  case change_type.to_sym
  when :create
    {}
  when :update
    self.class.value_columns.to_h do |column|
      [column.to_s, value_record.attribute_before_last_save(column.to_s)]
    end
  when :destroy
    self.class.value_columns.to_h { |column| [column.to_s, value_record[column]] }
  else
    raise ArgumentError, "Unsupported change_type: #{change_type.inspect}"
  end
end

#read_value(value_record) ⇒ Object

Returns the logical value for this field as stored on ‘value_record`. Default reads the primary cell. Override in multi-cell types to compose a hash (e.g., `Field::Currency` returns `r, currency: r`).



139
140
141
# File 'lib/typed_eav/field/typed_storage.rb', line 139

def read_value(value_record)
  value_record[self.class.value_columns.first]
end

#value_changed?(value_record) ⇒ Boolean

True iff ANY of the field’s value_columns had a saved change in the just-committed save. Used by Value’s :update dispatch gate so multi-cell types correctly fire the event when only the second cell changed (regression case Phase 5 D3).

Returns:



164
165
166
167
168
# File 'lib/typed_eav/field/typed_storage.rb', line 164

def value_changed?(value_record)
  self.class.value_columns.any? do |column|
    value_record.saved_change_to_attribute?(column)
  end
end

#write_value(value_record, casted) ⇒ Object

Writes a casted value to ‘value_record`. Default writes the primary cell. Override in multi-cell types to unpack the casted value across multiple cells.



146
147
148
# File 'lib/typed_eav/field/typed_storage.rb', line 146

def write_value(value_record, casted)
  value_record[self.class.value_columns.first] = casted
end