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[col]
  • :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[col] (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 {amount: r[:decimal_value], currency: r[:string_value]}).



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