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.:createreturns{}.after_snapshot(record, change_type)— per-column hash keyed by string column names.:destroyreturns{}.
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
-
#after_snapshot(value_record, change_type) ⇒ Object
Post-change snapshot keyed by string column names.
-
#apply_default(value_record) ⇒ Object
Writes this field's configured default to
value_record. -
#before_snapshot(value_record, change_type) ⇒ Object
Pre-change snapshot keyed by string column names.
-
#cast_query_operand(operator, raw) ⇒ Object
Normalize values before QueryBuilder emits SQL.
-
#logical_value_missing?(value_record) ⇒ Boolean
A logical value is missing only when every physical storage cell is nil.
-
#read_value(value_record) ⇒ Object
Returns the logical value for this field as stored on
value_record. -
#value_changed?(value_record) ⇒ Boolean
True iff ANY of the field's value_columns had a saved change in the just-committed save.
-
#write_value(value_record, casted) ⇒ Object
Writes a casted value to
value_record.
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)
228 229 230 231 232 233 234 235 236 237 |
# File 'lib/typed_eav/field/typed_storage.rb', line 228 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)
210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/typed_eav/field/typed_storage.rb', line 210 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 |
#cast_query_operand(operator, raw) ⇒ Object
Normalize values before QueryBuilder emits SQL. Keeping this beside the write caster makes query and write semantics use the same rules.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/typed_eav/field/typed_storage.rb', line 167 def cast_query_operand(operator, raw) case operator.to_sym when :between bounds = if raw.is_a?(Range) [raw.begin, raw.end] elsif raw.is_a?(Array) && raw.length == 2 raw end raise ArgumentError, ":between expects a Range or two-element Array" unless bounds casted = bounds.map { |bound| cast_query_value(bound) } casted.first..casted.last when :any_eq raise ArgumentError, ":any_eq expects a single array element" if raw.is_a?(Array) values = cast_query_value([raw]) values&.first when :all_eq raise ArgumentError, ":all_eq expects an Array" unless raw.is_a?(Array) cast_query_value(raw) else cast_query_value(raw) end end |
#logical_value_missing?(value_record) ⇒ Boolean
A logical value is missing only when every physical storage cell is nil. Multi-cell fields may legitimately be partially populated; such a row is present and must not be overwritten by a default backfill.
161 162 163 |
# File 'lib/typed_eav/field/typed_storage.rb', line 161 def logical_value_missing?(value_record) self.class.value_columns.all? { |column| value_record[column].nil? } 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).
199 200 201 202 203 |
# File 'lib/typed_eav/field/typed_storage.rb', line 199 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 |