Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Fixed
- Keep documentation pages within the mobile viewport when they contain wide code examples or tables; overflow scrolls inside those elements.
0.8.1 - 2026-09-08
Documentation-focused patch release; no runtime API or database migration changes.
Added
- Browsable documentation site with grouped sidebar navigation, a public API and configuration reference, and a user-facing summary of design decisions.
- Guides for CSV mapping, schema import/export and snapshots, defaults, missing-value queries, bulk-write results, and public partition helpers.
- GitHub Pages build and deployment workflow with local link and anchor checks. The site renders its changelog from the canonical root file.
- Public documentation Markdown included in the gem package.
Changed
- Shortened the README into an installation and discovery entry point, preserving detailed examples and contracts in linked guides.
- Corrected scope-resolver examples to use the documented tuple return shape.
- Required documentation review after every repository change in both agent instruction files.
0.8.0 - 2026-09-04
New SQL-backed query APIs, selective/preloaded bulk reads, logical dirty tracking, and read-only schema previews. Existing read defaults are preserved; no database migrations or new runtime dependencies are required.
Added
- Read-only
SchemaPortability.preview_schemacompares exact-partition portable schemas with current definitions, exposing field/section/option differences, conditional import actions, and risk flags. Type swaps remain blocked and omitted definitions are not treated as deletions. Previewing never applies changes or guarantees a later import’s success. (#51) saved_typed_eav_changesexposes the last successful host save’s logical changes insideafter_savecallbacks. Failed saves preserve the prior snapshot; no-op saves, reload, and rollback have explicit lifecycle semantics independent of audit/versioning. (#48)- Database-backed
aggregate_typed_eavmin/max/sum for Integer and Decimal field families, including Percentage. Decimal precision, caller host sets, and explicit empty/NULL semantics are preserved; references and multi-cell Currency are rejected instead of silently aggregated. (#50) - SQL-backed distinct typed values, exact distinct counts, and bounded per-value host counts. Caller host filters/pagination and field-definition precedence are retained; explicit NULL, false, and empty strings have documented semantics without host or Value hydration. (#49)
- Explicit
source: :preloadedbulk reads reuse caller-loaded Value graphs, including unsaved edits, with one batched definition lookup and no duplicate Value hydration. Incomplete required preloads fail clearly; fresh database reads remain the default. (#46) - Pending logical typed-value changes via
typed_eav_changes, covering host-associated named/nested edits and marked removals without loading untouched Value collections. Failed saves retain validation feedback and pending changes; this API is separate from persisted audit history. (#47) - Database-backed
order_typed_eavfor scalar fields, with explicit NULL placement and stable primary-key tie-breaking. Caller host filters and partition-definition precedence are preserved without Value hydration. (#44) - Field-selective bulk reads with
typed_eav_hash_for(records, fields: names). Selected winning field IDs narrow value loading before hydration; omittedfields:preserves all-fields reads. Empty, duplicate, unknown, and partition-specific names have explicit projection semantics. (#45)
Compatibility and usage notes
- Typed sorting and summaries select one effective field definition. Scope arguments do not authorize or filter host records: retain tenant and access filters on the caller’s Active Record relation. All-partitions mode and unsupported collection/multi-cell operations fail explicitly.
- Distinct lists and grouped counts default to 100 values (maximum 1,000), ordered by value rather than frequency. Exact distinct counts include the explicit NULL category; missing rows are omitted. Numeric min/max/sum retain Integer/BigDecimal types and do not aggregate reference IDs or currencies.
- Preloaded reads are explicit in-memory value snapshots with a current definition lookup; database reads remain the default. Dirty/saved changes describe host-associated editing and save state, not durable audit history or independently executed bulk SQL writes.
- Schema previews are advisory, exact-partition comparisons. They neither apply changes nor reserve the schema or guarantee a later import’s success.
- Local verification: 1,404 examples, 0 failures; 160 Ruby files lint-clean. The release workflow additionally verifies the supported compatibility matrix and the exact packaged artifact before publication.
0.7.1 - 2026-09-04
A focused patch release for inherited Active Record hosts and multi-partition bulk upserts. No new database migrations or public API changes are required.
Fixed
- Resolve inherited and namespaced Active Record host definitions, filters, bulk reads, writes, and registry/versioning opt-ins through Rails’ canonical polymorphic name. STI subclasses now consistently share their base class’s EAV schema and stored values while queries retain their host class restriction. Partition isolation and most-specific field precedence are preserved. (#42)
Performance
- Batch BulkUpsert field-definition resolution into one SELECT per transaction unit while preserving exact tuple isolation and definition precedence. A 20-partition regression case now issues one definition SELECT instead of 20; chunked transactions intentionally issue one per chunk. BulkRead and BulkUpsert share the internal batched resolver. This is a query-count improvement, not a universal throughput claim. (#43)
Compatibility and verification
- BulkUpsert’s reduced-semantics acknowledgement, validation, transaction boundaries, and callback/versioning behavior are unchanged.
- Regression coverage includes true STI and namespaced hosts, public read/filter/write paths, partition isolation, definition precedence, and all/chunk transaction behavior.
0.7.0 - 2026-08-18
This release hardens TypedEAV’s correctness and operational behavior, replaces the highest-impact read N+1 path, and adds explicitly bounded bulk-maintenance APIs. It keeps the typed-column architecture and existing semantic write path; it does not claim that one storage design or batch size wins every workload.
Highlights
- Batch partition resolution in
typed_eav_hash_for(records)so the accepted 1,000-scope BulkRead shape executes three SQL statements instead of 1,002. - Replace the six scalar value indexes with smaller partial-covering indexes that omit irrelevant NULL cells while preserving index-only entity reads.
- Add an explicit reduced-semantics PostgreSQL bulk-upsert API and opt-in chunked transactions for applications that knowingly prefer throughput or bounded commits over the full semantic write envelope.
- Write
ValueVersionaudit rows in the same source transaction as eachValue, so either both persist or both roll back. - Add SQL-narrowed default backfills and resumable, callback-preserving, keyset-batched field deletion.
Changed
- Query operands are normalized and validated by the owning Field before SQL is built. This keeps scalar, range, array, Currency, Reference, and text-search operands aligned with write semantics; Active Record remains responsible for SQL bind plumbing.
typed_eav_hash_for(records)resolves all effective partition definitions in one batched query, loads values once, and preloads field associations once. Its public return shape, logical-missing behavior, partition precedence, and orphan filtering are unchanged.- Versioning now installs synchronous
Valuelifecycle callbacks instead of writing audit rows from an internal after-commit subscriber. A failed audit insert now rolls back the source mutation instead of leaving an unversioned committedValue. - The six scalar indexes now use
(field_id, value) INCLUDE (entity_id) WHERE value IS NOT NULL. The migration creates every replacement concurrently before dropping its legacy index; rollback recreates legacy indexes before removing replacements. - Field and Section partition mutations validate their own pending tuple rather than leaking scope changes through shared lookup state. Value pending state is also preserved across validation and lifecycle boundaries.
- JSONB and TypedEAV are documented as workload-dependent storage choices. Applications can own expression B-tree indexes for stable JSONB paths and GIN indexes for containment; TypedEAV supplies stable typed columns and ordinary per-type indexes. No final storage winner is claimed without representative workload evidence.
Added
- SQL-narrowed default backfills through an exact-host relation, while retaining partition checks, batching, callbacks, validations, idempotence, versions, error reporting, and Field-owned logical-missing detection across multi-cell storage.
Field::Base#destroy_with_values_in_batches!, a callback-preserving, keyset-batched exact-field deletion path with locked bounded finalization. A failed batch leaves the Field available for inspection and retry.bulk_upsert_typed_eav_values, an explicitly reduced-semantics PostgreSQL upsert. Callers must passacknowledge_reduced_semantics: true; values are cast and validated before SQL, while host saves, persistence callbacks, versioning, delete shorthand, and per-record savepoints are intentionally skipped.transaction: :chunks, chunk_size: Nfor both semantic BulkWrite and the reduced upsert. Completed chunks remain committed if a later chunk fails;transaction: :allremains the default.
Fixed
- Validate Field defaults through the same typed domain rules used by ordinary writes, including range, option, reference, array, Currency, and multi-cell logical-missing semantics.
- Fail closed when versioning callbacks are missing, duplicated, installed on the wrong lifecycle kind, or configured across different connection pools.
- Lock and drain only the exact Field’s remaining Values during deletion, preserving callback/version ordering across retry and race boundaries.
- Keep Strong Migrations-compatible constraint validation outside a migration transaction while preserving the generated-consumer migration path.
- Reject normalized duplicate bulk-upsert keys such as
:ageand"age"before issuing SQL.
Performance
- BulkRead characterization reduced the 1,002 statements observed across 1,000 scopes to three for the same shape. This is a statement-count result, not a representative throughput claim.
- BulkWrite evidence remains bounded to the exercised 100- and 1,000-host lanes; no 10k/100k throughput or universal batch-size claim is made.
- Representative PostgreSQL 15, 16, and 18 migration/catalog checks passed for the partial-covering indexes. PostgreSQL 17 planner and benchmark timings are retained as co-tenant diagnostic evidence rather than portable latency promises.
Reliability
- ValueVersion rows are written in the source transaction, preserving atomic rollback with the Value mutation and avoiding a false after-commit rollback assumption.
- The parent-scope check-constraint migration validates existing rows before adding constraints with Strong Migrations-compatible nontransactional DDL; the unchanged consumer migration canary passed.
Upgrade notes
- Copy/install the latest engine migrations and run the application’s normal
migration command. The parent-scope validation and scalar-index migrations
use
disable_ddl_transaction!; do not wrap them in an application-level transaction. Concurrent create-before-drop ordering avoids an index-coverage gap during normal PostgreSQL deployment. - Versioned applications should expect an audit-write failure to abort the
Valuemutation.ValueandValueVersionmust share one connection pool; versioning fails closed when they do not. - Semantic BulkWrite still runs host validations/callbacks, Value persistence
callbacks, and versioning. Under the default
transaction: :all, individual validation/save failures are isolated by savepoints and successful records may commit, while an uncaught exception rolls back the outer transaction.transaction: :chunksdeliberately permits earlier chunks to remain committed after a later failure. bulk_set_typed_eav_values_per_recorduses records as Hash keys; duplicate Active Record instances for the same persisted row collapse to one entry. Use ordered separate calls when two updates to the same row must both occur.- The reduced bulk upsert accepts persisted, unique records and one uniform values Hash, returns the number of upserted value rows, and intentionally does not provide semantic successes/errors or callback/version guarantees.
- The supported runtime contract remains Ruby 3.3–4.0, Rails 7.2–8.1, and PostgreSQL 15–18.
Evidence boundaries
- The BulkRead result is an exact SQL-statement-count improvement for the characterized public path, not a universal throughput or latency guarantee.
- BulkWrite measurements cover 100- and 1,000-host lanes only. Applications should choose chunk sizes from their own callback cost, transaction duration, lock contention, and failure-recovery needs.
- Public chained-IN multi-filter SQL remains the supported strategy. Optional
pg_trgmand dependency statistics stay application-owned and measured; TypedEAV does not automatically install extensions, specialized indexes, or extended statistics. - No representative storage tournament selected TypedEAV, JSONB, per-type EAV, or conventional columns as a universal winner.
0.6.0 - 2026-07-13
Hardens correctness, query efficiency, installation confidence, and release safety while defining the supported runtime window. Existing public method signatures and valid-record behavior remain unchanged. The compatibility floor is intentionally raised to the release lines exercised by CI.
Changed
- Define the supported runtime contract as Ruby 3.3–4.0, Rails 7.2–8.1, and PostgreSQL 15–18. Runtime dependency bounds now reject unverified future Ruby and Rails release lines; prereleases remain outside the support guarantee. Issue #26.
Fixed
- Prevent single-value reads from returning a stale value from a losing partition, and enforce the parent-scope partition invariant in PostgreSQL. Issues #22 and #23.
- Exercise packaged migrations in a generated Rails consumer application so missing or non-idempotent installation paths fail before release. Issue #25.
Performance
- Remove obsolete option-cache invalidation and batch-load fields for already loaded typed values. Issues #28 and #29.
- Bound scaffold visibility queries and preload existing values during default backfills. Issues #30 and #31.
- Keep all-partitions filter unions inside SQL and batch schema-import lookups and option writes. Issues #32 and #33.
Documentation
- Refresh maintainer, schema, and architecture guidance for the 0.5+ behavior and current database invariants. Issue #34.
Release engineering
- Gate trusted publishing on the exact dereferenced tag commit, the complete supported compatibility matrix, lint, package-content inspection, and a real-host migration installation using the same checksummed gem artifact that is sent to RubyGems. Issue #27.
References
- Issues #22, #23, and #25–#34.
0.5.0 - 2026-06-01
Adds a human display label for fields, distinct from the immutable machine
slug name (issue #21). Fully additive — every change defaults to the
pre-0.5.0 behavior. Existing rows (with label NULL) render exactly as
before, and consumers that never set a label observe no difference.
Added
-
label— a nullable, free-text column ontyped_eav_fields(20260507000000_add_label_to_typed_eav_fields.rb). No index, no default, no backfill:labelnever participates in uniqueness, lookup, partitioning, or ordering, so an index would be dead weight. Runrails typed_eav:install:migrations(or copy the migration) and migrate to pick it up.namestays the immutable machine key. -
TypedEAV::Field::Base#display_name— the canonical human-facing display string. Returns the free-textlabelwhen present, otherwise falls back toname.humanize. A blank ("") label falls back too (viapresence). This is the ONE accessor all rendering should use; existing rows render unchanged. -
SchemaPortabilityround-trips the label.export_schemaemits the raw"label"soimport_schemareproduces it verbatim and divergence detection treats a differing label as a difference; legacy payloads with no"label"key import as NULL with no version gate.export_snapshot_schemaemits the resolved"display_name"instead (render-oriented snapshots hand the consumer a ready-to-render string — intentionally asymmetric to the raw-label regular export).
Changed
- A label-only edit on a Field dispatches the
:updateevent, not:rename.:renameremains reserved for changes to the machinename. Regression-pinned inspec/regressions/issue_21_label_no_rename_spec.rb.
References
- Issue #21 — Field display label /
display_namecontract.
0.4.0 - 2026-05-26
Closes four follow-up gaps (PRD #15) surfaced when a downstream Rails app
consolidated onto 0.3.2 and found four places where the gem’s public
surface forced workarounds: a missing per-record-varying bulk-write entry
point, a dedup defect on unsaved entities with in-memory typed_values
builds, an :is_null operator that couldn’t honor the user-intuitive “is
empty” semantic, and a portable-schema shape that was wrong for in-app
snapshot stores. A fifth gap (G5) was scoped down to a documentation
promotion on the existing Partition module rather than a new wrapper.
All five changes are additive — no public-API breakage. New entry points
default to current shapes; existing callers of bulk_set_typed_eav_values,
with_field/where_typed_eav (without the new kwarg), export_schema,
and initialize_typed_values (on persisted records with no in-memory
builds) keep their behavior byte-for-byte. New ADR: ADR-0006 pins the G3
include_missing strategy as set-complement at the FilterQuery altitude
(rejects the LEFT JOIN framing the PRD originally sketched).
Added
-
Entity.bulk_set_typed_eav_values_per_record(values_by_record, version_grouping: :default)— per-record-varying sibling tobulk_set_typed_eav_values. Takes aHash<host_record, Hash<field_name, value>>and routes each record’s value-set through the same outer-transaction-plus-savepoint-per-record envelope, returning the same{ successes: [...], errors_by_record: { record => errors_hash } }shape. Supports sparse-update semantics (unlisted fields untouched),{ _destroy: true }value-removal shorthand, mixed-scope records (each record honors its own[scope, parent_scope]even insideTypedEAV.unscoped { ... }), and:per_fieldUUID allocation across the union of field names. Empty input short-circuits without opening a transaction. Internally, both public executors (BulkWrite.executeandBulkWrite.execute_per_record) now share a singleexecute_pairs(pairs, effective_grouping, field_uuids)helper that takes ordered[record, vbn]pairs — preservingexecute’s byte- for-byte behavior on duplicate in-memory instances of the same persisted row (Hash-key collision is documented as a gotcha only on the new API). G1 (issue #18). -
Entity.with_fieldandEntity.where_typed_eavaccept an opt-ininclude_missing:keyword (defaultfalse). Threaded through toFilterQuery#initialize. When paired with:is_null, the operator matches hosts with no non-NULL value for the field — including hosts that have notyped_eav_valuesrow at all (Reading A: the user-intuitive “is empty” semantic). Implemented as a set-complement against:is_not_nullat theFilterQueryaltitude;QueryBuilderis not modified. With:is_not_nullthe kwarg is a no-op; with any other operator (:eq,:gt,:contains,:references,:between,:starts_with, etc.) it is silently ignored — filter UIs can pass the kwarg uniformly without branching per operator. On the multimap (ALL_SCOPES) branch, “no non-NULL value” reads across all matching field definitions for the name: a host matches iff none of the per-tenant field defs have a non-NULL value for it. G3 (issue #19). See ADR-0006. -
TypedEAV::SchemaPortability.export_snapshot_schema(entity_type:, scope: nil, parent_scope: nil)— sibling toexport_schemathat returns a lean, restore-oriented projection in a versioned envelope:{ "snapshot_schema_version" => 1, "fields" => [...] }. Per-field entries carry onlyname,field_type_name,required,sort_order,options, and (for optionable types)options_data—entity_type,scope,parent_scope,type(AR STI class name),field_dependent, anddefault_value_metaare omitted. Non-optionable fields omit theoptions_datakey entirely (absent, not nil). Thesnapshot_schema_versioninteger will be bumped explicitly when the inner shape evolves — it is not frozen forever. Fields are ordered bysort_orderandoptions_datamirrors the loaded/unloaded ordering rule used byexport_schema. G4 (PRD #15).
Documentation
TypedEAV::Partition.find_visible_section!is documented-public going forward. Apps building admin UIs that need to authorize a section lookup before editing, rendering, or destroying it should call this rather thanSection.find(id). Method shape and behavior do not change — this is a documentation clarification that promotes an existing, already-shipping method into the documented surface area, alongside the siblingPartitionmethods (visible_fields,effective_fields_by_name,definitions_by_name,definitions_multimap_by_name,visible_sections). G5 (issue #20).
Fixed
InstanceMethods#initialize_typed_valuesno longer builds duplicate rows on entities that already have in-memorytyped_valuesbuilds (form path withfield_id, scripting path viatyped_eav_attributes=, or directtyped_values.build(...)on a persisted record). Covers three cases: (1) new record + nested attributes, (2) new record + scripting setter, (3) persisted record + unloaded association + a build that lives intargetwithout flipping@loaded. The persisted-no-builds fast path still usespluckonly — no extra association load. Dedup also tolerates an in-memory build whosefield_idis nil but whosefieldassociation is set (field_id || field&.idfallback). G2 (PRD #15).
0.3.2 - 2026-05-25
Documentation-only release. No code or behavior changes.
Fixed
- README §”Architecture” — Per-record reads/writes subsection erroneously
listed
typed_eav_changesas a publicInstanceMethodsAPI (added in 0.3.1). That method does not exist onInstanceMethods. Replaced with the actual existing methods (typed_eav_definitionsand noting thetyped_eav=alias). Dirty tracking for typed-EAV writes is tracked as a feature request — not implemented in this release.
0.3.1 - 2026-05-25
Documentation-only release. No code or behavior changes.
Added
- README §”Architecture” — full overview of the post-0.3.0 internal
module layout: macro entry (
HasTypedEav), the two-altitude query pattern (EntityQuery→FilterQuery→QueryBuilder),BulkReadandBulkWritesiblings,InstanceMethods,Field::TypedStorageconcern, family intermediate bases (ValidatedString,RangeBounded,Optionable),ScopeTuple,Partition,EventDispatcher, and the Phase-6 modules (SchemaPortability,CSVMapper). Anchored to ADRs 0001–0005 throughout.
Removed
TEST_PLAN.md— pre-0.3.0 test-sweep planning artifact (2026-04-08). Described specs for modules deleted in #9. Git history preserves it.typed_eav-enhancement-plan.md— pre-0.3.0 phased roadmap. References v0.1.0 line numbers and Phase-1 work that has since shipped. Git history preserves it.
0.3.0 - 2026-05-25
Pre-1.0 architecture cleanup arc (issues #9–#13). No public-API breakage for host AR models or registered custom field types; behavior changes are limited to two latent-bug fixes (now raised at field-save) and one internal helper relocation (see “Changed” below). Anchored by ADRs 0001–0005.
Added
- New
TypedEAV::Field::TypedStorageconcern (auto-included onField::Base) collapses the prior storage stack into three paired override points:read_value(record),write_value(record, casted),apply_default(record). Custom multi-cell field types now extendField::Basedirectly and override only these methods. See README §”Multi-cell field types” and ADR-0001 (issue #9). - New top-level
TypedEAV::ScopeTuplemodule exposes the[scope, parent_scope]normalization surface:normalize_permissive,normalize_strict, andinvariant_satisfied?. Used byPartition,TypedEAV.with_scope,Config#resolve_scope, and the query path (issue #10). - New top-level query objects extracted from
HasTypedEAV:TypedEAV::EntityQuery(class-method orchestration on host AR models),TypedEAV::FilterQuery(multi-filter SQL composition forwhere_typed_eav/with_field), andTypedEAV::BulkRead(bulk per-record reads viaeav_values_for). See ADR-0002 (issue #11). - New field family intermediate bases collapse per-leaf duplication:
TypedEAV::Field::ValidatedString— min/max-length + regex-pattern validation surface forstring_value-backed types (parent ofEmailandUrl).TypedEAV::Field::RangeBounded— min/max-bound validation helpers for comparable single-value types (parent ofInteger,Decimal,Date,DateTime).TypedEAV::Field::Optionable— concern (not parent) for types that draw values from aField::Optionset; included bySelectandMultiSelect.
See README §”Family intermediate bases (extension points)” and ADR-0004 (issue #12).
Changed
- Internal helper move.
TypedEAV::HasTypedEAV.definitions_by_nameandTypedEAV::HasTypedEAV.definitions_multimap_by_namemoved toTypedEAV::Partition.definitions_by_name/TypedEAV::Partition.definitions_multimap_by_name. These helpers were technically callable from application code but not documented; partition-tuple precedence is a partition concept and the new home reflects that. External callers (if any) should update the call site. See ADR-0002 (issue #11). TypedEAV::HasTypedEAVis now a slim macro module (lib/typed_eav/has_typed_eav.rb) that delegates to a per-instance methods file (lib/typed_eav/has_typed_eav/instance_methods.rb) plus the newEntityQuery/FilterQuery/BulkReadobjects. Public class-method and instance-method signatures on host AR models are unchanged. See ADR-0002 (issue #11).- Field validation now runs paired-bound checks at field-save time, not
only at value-write time:
Field::Email/Field::Url(viaValidatedString) rejectmax_length < min_lengthwhen the field record is saved.Field::Date/Field::DateTime(viaRangeBoundedleaves) reject invertedmin_date/max_date(andmin_datetime/max_datetime) bounds when the field record is saved.
Both were latent bugs prior to v0.3.0 — the bound mismatch was only surfaced when a
Valuewas written. Authors of custom field types that store inverted bounds will now see the validation fail earlier. See ADR-0004 (issue #12).
Removed
TypedEAV::Field::FieldStorageContract,TypedEAV::Field::CurrencyStorageContract, andTypedEAV::Field::ColumnMappingare deleted; their surface lives onField::TypedStorage. ADR-0001 (issue #9).TypedEAV::Partition.validate_tuple!is deleted; callers useTypedEAV::ScopeTuple.normalize_strictdirectly. Issue #10.
Internal
-
Added the opt-in
Field#destroy_with_values_in_batches!API for exact-field, callback-preserving keyset deletion. It commits bounded Value destroy batches, retains the Field across failure, and performs locked bounded finalization; ordinary Field destruction and dependency policies remain unchanged. -
Atomic ValueVersion writes are installed from the Value callback chains at boot, with an identical-pool guard and idempotent recovery if a callback is removed. BulkWrite keeps caller context unchanged while correlating version groups through its pending marker; EventDispatcher remains a public and generic observer broker.
TypedEAV::EventDispatcheris retained as the synchronous broker betweenTypedEAV::HooksandActiveSupport::Notifications. The cleanup arc explicitly considered collapsing it and rejected that: the broker is the seam where event-name normalization and thenotifications: falseopt-out live. See ADR-0003.- The Phase-6 modules —
TypedEAV::BulkWrite,TypedEAV::Importers::CSVMapper, andTypedEAV::SchemaPortability::*— remain independent top-level modules. The cleanup arc explicitly considered consolidating them under a singleTypedEAV::Operationsnamespace and rejected that: the modules share no internal contract and the namespace would be cosmetic. See ADR-0005. - Cyclomatic-complexity rubocop disables that previously masked the
HasTypedEAVmega-module are gone — the split files clear the default complexity thresholds.
References
- Issue #9 —
Field::TypedStorageconcern. - Issue #10 —
ScopeTupleextraction. - Issue #11 —
EntityQuery/FilterQuery/BulkReadsplit. - Issue #12 — Field family intermediate bases.
- Issue #13 — release coordination.
- ADR-0001 — collapse field storage stack.
- ADR-0002 — split
HasTypedEAVinto query objects. - ADR-0003 — retain
EventDispatcheras broker. - ADR-0004 — field family intermediate bases.
- ADR-0005 — keep Phase-6 modules independent.
0.2.1 - 2026-05-08
Metadata-only release.
Changed
- Updated the RubyGems package author metadata to
dchuk.
0.2.0 - 2026-04-29
Two-level scope partitioning. Field and section definitions now partition on
the tuple (entity_type, scope, parent_scope), so an app can scope custom
fields per workspace inside a tenant (or any second axis your domain needs)
without giving up the existing single-scope ergonomics.
Added
parent_scope_method:kwarg onhas_typed_eavfor two-level partition keys. Requiresscope_method:— declaringparent_scope_method:without it raises at macro-expansion time.parent_scope:kwarg onwhere_typed_eav,with_field, andtyped_eav_definitionsfor explicit per-query overrides.TypedEAV.with_scopeaccepts a[scope, parent_scope]tuple form. The scalar formwith_scope(value)is preserved (treated as[value, nil]).idx_te_sections_lookupindex for parity withidx_te_fields_lookup.
Changed
- BREAKING
Config.scope_resolvercallables MUST return a 2-element Array[scope, parent_scope]. v0.1.x callables returning a bare scalar will raiseArgumentErrorat the next ambient query — there is no silent fallback. If you don’t use parent_scope, return[scope, nil]. TypedEAV.current_scopenow returns[scope, parent_scope](ornil); was a String/nil scalar.Config::DEFAULT_SCOPE_RESOLVER(theacts_as_tenantauto-detect) returns[ActsAsTenant.current_tenant, nil]. The parent_scope slot isnilbecause the tenant gem has no parent-scope analog.Field::Base.for_entityandSection.for_entityaccept aparent_scope:kwarg (defaults tonil).- AR uniqueness validators on
Field(on:name) andSection(on:code) includeparent_scopein their scope key. - Three-way collision precedence in
definitions_by_name: full-triple wins, then scope-only, then global. - Paired partial unique indexes now cover the new tuple. Old
idx_te_fields_unique_scoped/idx_te_fields_unique_global(and the Section equivalents) are replaced by_uniq_scoped_full/_uniq_scoped_only/_uniq_globalper table. idx_te_fields_lookuprecreated withparent_scopebetweenscopeandsort_order.
Validation
Field::Base#validate_parent_scope_invariantrejects rows whereparent_scope.present?andscope.blank?(no orphan-parent rows).Section#validate_parent_scope_invariantis the symmetric guard.Value#validate_field_scope_matches_entityextended to the parent_scope axis: aValuewhose host’styped_eav_parent_scopedoesn’t match the field’sparent_scopeis rejected.
Migration steps
- Run
bin/rails typed_eav:install:migrationsto copyAddParentScopeToTypedEavPartitionsinto your app. - Run
bin/rails db:migrate. The migration usesCREATE INDEX CONCURRENTLYfor all index changes and is safe on production tables — existing rows are not rewritten. - Update any custom
TypedEAV.config.scope_resolverlambda to return[scope, parent_scope]. If you don’t use parent_scope, return[scope, nil]. A bare scalar return surfaces asArgumentErrorat runtime — there is no silent fallback. - Optional: declare
parent_scope_method:on hosts that have an in-tenant partition. Existing single-scope models continue to work without changes.
See the README “Migrating from v0.1.x” section for the full guidance, including the orphan-parent invariant and worked examples.
References
5ff7c30— migration scaffolding (column + paired partials + lookup index).52014a3— resolver tuple contract onwith_scope/Config.6c3afb5—Fieldpartition tuple + orphan-parent guard.9c7e916—Sectionpartition tuple + orphan-parent guard (symmetric).c628372—parent_scope_method:macro, query path wiring,Valuecross-axis guard.e5e78a4— spec coverage (440 examples, 0 failures).
0.1.0 - 2026-04-25
Initial release.