typed_eav 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -2
  3. data/README.md +79 -1710
  4. data/RELEASING.md +81 -0
  5. data/docs/adr/0001-collapse-column-mapping-stack.md +28 -0
  6. data/docs/adr/0002-entity-query-orchestration.md +33 -0
  7. data/docs/adr/0003-keep-event-dispatcher-broker.md +41 -0
  8. data/docs/adr/0004-field-family-intermediate-bases.md +49 -0
  9. data/docs/adr/0005-keep-phase-six-modules-independent.md +48 -0
  10. data/docs/adr/0006-include-missing-via-set-complement.md +77 -0
  11. data/docs/adr/0007-visibility-versus-mutation-relations.md +33 -0
  12. data/docs/adr/0008-partial-covering-scalar-indexes.md +83 -0
  13. data/docs/adr/0009-string-search-indexing.md +110 -0
  14. data/docs/adr/0010-planner-statistics-policy.md +109 -0
  15. data/docs/adr/0011-multi-filter-query-strategy.md +111 -0
  16. data/docs/adr/0012-cross-scope-administrative-query-policy.md +76 -0
  17. data/docs/adr/0013-durable-versioning-and-field-deletion.md +125 -0
  18. data/docs/adr/index.md +101 -0
  19. data/docs/getting-started.md +79 -0
  20. data/docs/guides/architecture.md +125 -0
  21. data/docs/guides/bulk-operations.md +205 -0
  22. data/docs/guides/csv-import.md +88 -0
  23. data/docs/guides/development.md +73 -0
  24. data/docs/guides/events-and-versioning.md +360 -0
  25. data/docs/guides/fields.md +342 -0
  26. data/docs/guides/performance.md +107 -0
  27. data/docs/guides/queries.md +188 -0
  28. data/docs/guides/schema.md +134 -0
  29. data/docs/guides/scoping.md +254 -0
  30. data/docs/guides/upgrading.md +26 -0
  31. data/docs/guides/usage.md +259 -0
  32. data/docs/index.md +44 -0
  33. data/docs/maintaining.md +82 -0
  34. data/docs/reference/api.md +133 -0
  35. data/docs/reference/configuration.md +64 -0
  36. data/docs/reference/index.md +16 -0
  37. data/lib/typed_eav/version.rb +1 -1
  38. metadata +35 -1
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: "ADR 0010: Application-owned evaluation of planner statistics"
3
+ ---
4
+
5
+ # ADR 0010: Application-owned evaluation of planner statistics
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ TypedEAV stores many logical fields in one physical table. A predicate such as
14
+ `field_id = 42 AND integer_value = 7` can therefore violate the planner's
15
+ default assumption that the two columns are independent. PostgreSQL extended
16
+ statistics can describe a column group in three different ways:
17
+
18
+ - `dependencies` describes functional dependencies between columns. PostgreSQL
19
+ can use it for compatible equality and `IN` clauses, but not range clauses.
20
+ - `mcv` stores frequencies for common combinations of values. It can help when
21
+ a queried combination is represented in that list.
22
+ - `ndistinct` estimates the number of distinct column combinations. It is
23
+ principally relevant to grouping cardinality, not the field/value `WHERE`
24
+ predicates exercised here.
25
+
26
+ The Phase 4A benchmark created a separate object for each tested typed column,
27
+ paired with `field_id`, and compared `dependencies`, `mcv`, `ndistinct`, and a
28
+ combined object. Its deterministic 300,000-entity dataset ran ten trials and 50
29
+ paired blocks on PostgreSQL 17.11. The target of 100 controlled this experiment;
30
+ it is not a recommended target for every database.
31
+
32
+ Dependencies improved aggregate equality-estimate accuracy in this synthetic
33
+ workload, but did not apply to ranges. The combined object did not reproduce the
34
+ dependencies-only aggregate result: for the four probes whose estimates
35
+ changed, matching MCV groups supplied the estimate, so the combined candidate
36
+ mirrored MCV instead. `ndistinct` did not change these `WHERE` estimates. No
37
+ candidate changed a plan shape, introduced a sequential scan, or lost an
38
+ index-only plan, and the experiment does not demonstrate execution-time benefit.
39
+
40
+ One preregistered probe was mislabeled. `date_skewed_common_eq` queried
41
+ `2024-01-01`, while the generated common dates begin at `2024-01-02`; the probe
42
+ matched zero rows. It is absent-date estimation evidence, not common-date
43
+ evidence. The retained artifact remains mechanically valid and discloses the
44
+ query and row count, so it is not rewritten.
45
+
46
+ ## Decision
47
+
48
+ TypedEAV will not create extended-statistics objects automatically and will not
49
+ ship a migration, generator, or helper for them. Production schema, queries, and
50
+ installation behavior remain unchanged.
51
+
52
+ An application may evaluate application-owned `dependencies` statistics when
53
+ its own plans show persistent field/value equality misestimates. The application
54
+ must choose the typed columns and target from its workload; it must not copy the
55
+ benchmark's target of 100 as a universal setting. `mcv`, `ndistinct`, or a
56
+ combined object require their own workload evidence and must not be added merely
57
+ because PostgreSQL supports them.
58
+
59
+ For example, an application investigating integer equality could test an
60
+ application-specific object in a representative preproduction database:
61
+
62
+ ```sql
63
+ CREATE STATISTICS app_te_values_field_integer_dependencies (dependencies)
64
+ ON field_id, integer_value
65
+ FROM typed_eav_values;
66
+
67
+ ALTER STATISTICS app_te_values_field_integer_dependencies
68
+ SET STATISTICS <application-selected-target>;
69
+
70
+ ANALYZE typed_eav_values;
71
+ ```
72
+
73
+ Creation alone does not populate statistics; `ANALYZE` is required. Compare the
74
+ same representative queries before and after with `EXPLAIN (ANALYZE, BUFFERS,
75
+ WAL, SETTINGS)`, checking estimated versus actual rows, plan shape, planning and
76
+ execution time, and workload-level latency. Also measure `ANALYZE` duration,
77
+ catalog size, and maintenance effects. Repeat after realistic data churn and on
78
+ every PostgreSQL version the application operates. Keep an object only when the
79
+ observed benefit justifies those costs.
80
+
81
+ The consuming application owns the DDL, stable names, deployment timing, and
82
+ rollback. Before creating or dropping anything, inspect `pg_statistic_ext` and,
83
+ when the role is permitted to read it, `pg_statistic_ext_data`; verify the name,
84
+ table, columns, kinds, owner, and target. In a database shared by multiple
85
+ applications, coordinate a single owner and do not alter or drop a statistics
86
+ object merely because its name looks familiar. Removal is explicit and followed
87
+ by another measurement cycle:
88
+
89
+ ```sql
90
+ DROP STATISTICS IF EXISTS app_te_values_field_integer_dependencies;
91
+ ANALYZE typed_eav_values;
92
+ ```
93
+
94
+ Dropping the object removes the extended data; the follow-up `ANALYZE` refreshes
95
+ ordinary statistics for a comparable post-removal baseline. Schedule creation,
96
+ `ANALYZE`, and removal according to the application's table size and operational
97
+ constraints rather than assuming the benchmark's costs transfer.
98
+
99
+ ## Consequences
100
+
101
+ - TypedEAV adds no schema object or maintenance cost for applications that have
102
+ not demonstrated a planner-estimation problem.
103
+ - Applications can test the narrow `field_id`/typed-column correlation without
104
+ changing TypedEAV query semantics.
105
+ - Application owners must evaluate targets, PostgreSQL versions, data
106
+ distributions, and operational costs themselves.
107
+ - Phase 4A remains bounded PostgreSQL 17 synthetic evidence. It supports this
108
+ evaluation policy, not a claim of runtime improvement or general planner
109
+ behavior.
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: "ADR 0011: Retain chained-IN multi-filter queries"
3
+ ---
4
+
5
+ # ADR 0011: Retain chained-IN multi-filter queries
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ TypedEAV currently resolves every requested field before constructing SQL. It
14
+ then builds one typed value subquery per filter, selects distinct `entity_id`
15
+ values from each, and adds each result to the host relation as an `id IN (...)`
16
+ predicate. This chained-`IN` shape preserves the host relation as the universe
17
+ for complement and missing-value operations.
18
+
19
+ Phase 4B compared that implementation with `INTERSECT`, correlated `EXISTS`,
20
+ and direct grouped `HAVING`. `INTERSECT` and `EXISTS` reused the same resolved
21
+ per-filter subqueries. Grouped `HAVING` was eligible only for compatible
22
+ present-row predicates: without a host join or a separate complement it cannot
23
+ represent missing or host-universe complement semantics, and a direct grouped
24
+ value scan cannot return the unfiltered host relation for an empty filter set.
25
+
26
+ The seed-4502 representative run used 100,000 primary hosts, more than 50 field
27
+ definitions, 25 scenarios, three rotations, and ten attempts per eligible
28
+ strategy group. It retained 2,940 uniformly capped attempts, 294 sorted
29
+ host-identity oracles, and 75 scenario/trial summaries on PostgreSQL 17 under
30
+ resource-capped co-tenancy. Of the attempts, 622 reached the 1,000 ms timeout
31
+ and are right-censored lower bounds. Of the 294 non-retried 5,000 ms oracles,
32
+ 282 completed and 12 timed out; the timed-out identities are unknown, not equal
33
+ or unequal. No completed oracle mismatched or errored. The embedded 2,000-host
34
+ smoke completed all 98 eligible oracles with equal identities, but that does
35
+ not establish representative-scale equivalence.
36
+
37
+ The candidates did not produce a robust general winner. `INTERSECT`, `EXISTS`,
38
+ and grouped `HAVING` strongly improved the high-selectivity 10-predicate case,
39
+ but the mixed 10-predicate case regressed and many low-, mixed-, and
40
+ skewed-selectivity large cases were censored or otherwise inconclusive.
41
+
42
+ Two evidence defects further limit the result:
43
+
44
+ - The artifact's derived buffer totals are false zeros because the extractor
45
+ split each multiword EXPLAIN block key into individual words. The retained
46
+ raw plans contain nonzero block counters and can be reparsed, but the
47
+ published derived zero values are not valid buffer evidence.
48
+ - The 20-predicate scenarios repeat ten fields, while the skewed 10- and
49
+ 20-predicate scenarios repeat five fields. The run therefore does not measure
50
+ 20 distinct fields, and its skewed cases do not measure 10 distinct fields.
51
+
52
+ Absolute timings and dispersion from the continuously busy co-tenant host are
53
+ diagnostic. PostgreSQL 17 plan choices are not evidence for every supported
54
+ PostgreSQL version or application distribution.
55
+
56
+ ## Decision
57
+
58
+ Retain the current chained host `IN` query strategy. Production query code and
59
+ public APIs do not change.
60
+
61
+ `INTERSECT` and correlated `EXISTS` remain research-only alternatives. Direct
62
+ grouped `HAVING` is also research-only for compatible present-row predicates
63
+ and remains ineligible for missing-value, host-universe complement, and empty-
64
+ filter semantics. No adaptive selector or production replacement is available
65
+ or authorized.
66
+
67
+ ## Gates for future research
68
+
69
+ Future adaptive or replacement work must pass all of these gates before a
70
+ production change can be considered:
71
+
72
+ 1. **Semantic coverage.** Build every strategy from identical resolved fields,
73
+ typed operands, predicates, datasets, and host universes. Prove scope,
74
+ parent-scope, global/scoped shadowing, all-scope administration,
75
+ polymorphic host identity, arbitrary supported operators, explicit NULL,
76
+ missing and `include_missing` complements, duplicate internal matches,
77
+ empty filters, ordering where requested, and supported error behavior.
78
+ Grouped `HAVING` must stay ineligible where it cannot express the contract.
79
+ 2. **Representative equivalence.** Complete the sorted `(entity_type, id)`
80
+ count/checksum oracle for every eligible representative group. A timeout,
81
+ mismatch, or error cannot be treated as equality; no completed-only sample,
82
+ retry, imputation, or censored bound may authorize replacement.
83
+ 3. **Distinct-field scale.** Measure actual 10- and 20-distinct-field workloads
84
+ across high-, low-, mixed-, and skewed-selectivity families rather than
85
+ reaching those predicate counts by repeating five or ten fields.
86
+ 4. **Correct buffer evidence.** Re-extract separate shared/local/temp read,
87
+ hit, dirtied, and written block counters from every retained raw plan,
88
+ validate derived totals against the source EXPLAIN JSON, and obtain complete
89
+ comparable `ANALYZE` buffer evidence. The current derived zeros cannot be
90
+ used as a baseline.
91
+ 5. **Performance threshold.** Show uncensored improvement of at least 20% at
92
+ p95 for both 10- and 20-filter workloads in at least three workload families.
93
+ Preserve all scheduled repetitions and right-censored results; do not infer
94
+ a winner from completed attempts alone.
95
+ 6. **Planning and plan shape.** Pre-register material-regression bounds, then
96
+ show bounded planning-time and buffer behavior and no material plan-shape
97
+ regression, including index use and host-relation behavior, across the
98
+ eligible semantic matrix. Repeat planner evidence on the PostgreSQL versions
99
+ for which a production strategy would be claimed.
100
+ Cross-scope planning at high tenant cardinality remains separate required
101
+ Phase 4 evidence. Passing a favorable microbenchmark or repairing only one of
102
+ the defects above is insufficient for Gate 4 or production authorization.
103
+
104
+ ## Consequences
105
+
106
+ TypedEAV keeps the broadly exercised semantics and predictable public behavior
107
+ of its current composition path. It also forgoes the strong improvements seen
108
+ in one high-selectivity case until those gains survive the full semantic and
109
+ operational evidence gates. The representative artifact remains useful as
110
+ negative and retention evidence; it is not proof that alternatives are
111
+ equivalent, eligible, adaptive, or generally slower.
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: "ADR 0012: Bound cross-scope administrative queries in applications"
3
+ ---
4
+
5
+ # ADR 0012: Bound cross-scope administrative queries in applications
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ `TypedEAV.unscoped` is the deliberate escape hatch for administration,
14
+ analytics, migrations, and cross-tenant audits. It is not the ordinary tenant
15
+ request path. Within a tenant tuple, field visibility considers global,
16
+ scope-only, and full-tuple definitions and selects the most-specific definition
17
+ for each name. Under `unscoped`, the same-name definitions across every
18
+ partition remain visible and query construction unions their per-definition
19
+ relations for each filter.
20
+
21
+ Code-path analysis shows that the `ALL_SCOPES` branch eagerly materializes the
22
+ visible definitions, groups them by name, constructs one relation per matching
23
+ definition, and combines those relations with Arel OR nodes. Repeating that
24
+ work across filters makes construction depend on the definitions visible to
25
+ the administrative query as well as its filters. This behavior is distinct
26
+ from bounded tenant specificity and must not be generalized to normal tenant
27
+ traffic.
28
+
29
+ Phase 4C attempted representative cross-scope measurement in T066 and T069.
30
+ Neither execution produced an accepted representative artifact. T066 rejected
31
+ at artifact validation without retaining the decisive rejection detail. T069
32
+ then rejected at the required checkpoint gate, and runner ordering again lost
33
+ the exported diagnostic before local retention. Local smoke exercised the
34
+ intended semantics, but smoke evidence does not establish representative
35
+ scaling or candidate equivalence. The rejected runs therefore support no
36
+ latency, throughput, planner, cardinality-limit, or comparative-performance
37
+ claim.
38
+
39
+ ## Decision
40
+
41
+ Keep `TypedEAV.unscoped` as an explicit administrative and analytics surface.
42
+ Applications should keep the definition universe for each administrative job
43
+ as narrow as their use case permits and batch broad cross-partition work at an
44
+ application-owned boundary. The appropriate relation, partition selection,
45
+ batch size, scheduling, and operational limits depend on the consuming
46
+ application and must be measured there.
47
+
48
+ TypedEAV does not add a built-in threshold, warning, guardrail, batching API,
49
+ query rewrite, schema object, or dependency. The benchmark-only homogeneous
50
+ `field_id`-array prototype is not adopted. It lacked accepted representative
51
+ semantic and performance evidence and was ineligible for mixed field casts,
52
+ operators, and NULL/missing complement semantics.
53
+
54
+ No numeric definition limit is recommended. Applications that operate across
55
+ many partitions should inspect their own generated SQL, planning and execution
56
+ behavior, memory use, and workload interference before selecting an operational
57
+ boundary. Ordinary tenant requests should continue using scoped resolution,
58
+ not `unscoped` as a shortcut around scope configuration.
59
+
60
+ ## Consequences
61
+
62
+ - Tenant-scoped lookup retains most-specific global/scope/full-tuple behavior.
63
+ - Cross-scope queries retain their existing union semantics and public API.
64
+ - Administrative callers own bounding and batching for broad work.
65
+ - No rejected benchmark output is published as representative evidence.
66
+ - A future production optimization requires a new accepted protocol that
67
+ preserves polymorphism, field-owned casts, shadowing, supported operators,
68
+ explicit NULL, missing rows, and `include_missing` semantics.
69
+
70
+ ## Related decisions
71
+
72
+ - ADR 0002 separates entity-query orchestration from per-field predicates.
73
+ - ADR 0006 defines `include_missing` as host-level set complement, including
74
+ the `ALL_SCOPES` multimap branch.
75
+ - ADR 0011 retains the chained-`IN` multi-filter strategy and requires complete
76
+ representative equivalence before replacement.
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: "ADR 0013: Durable versioning and callback-preserving field deletion"
3
+ ---
4
+
5
+ # ADR 0013: Durable versioning and callback-preserving field deletion
6
+
7
+ ## Status
8
+
9
+ Accepted and implemented by the atomic versioning boundary work.
10
+
11
+ ## Context
12
+
13
+ The historical implementation wrote `ValueVersion` rows from an internal
14
+ `after_commit` subscriber. That boundary was intentionally after the source transaction: a
15
+ source `Value` create, update, or destroy can commit successfully and then the
16
+ version writer can raise. The same applies when a field's `field_dependent:
17
+ "destroy"` callback destroys its dependent Values. Real-commit regression
18
+ coverage records this observable divergence: the source rows and field cascade
19
+ remain committed while the attempted version rows are absent.
20
+
21
+ This is not a rollback guarantee. An `after_commit` exception can be surfaced
22
+ to the caller, but it cannot undo the already committed source mutation. Public
23
+ application callbacks remain a separate concern and should remain best-effort
24
+ after-commit hooks, rescued and logged rather than making source persistence
25
+ fail closed.
26
+
27
+ ## Options
28
+
29
+ | Option | Boundary | Strength | Cost and limitation |
30
+ | --- | --- | --- | --- |
31
+ | Historical after-commit delivery | `Value` committed, then subscriber wrote `ValueVersion` | Backward-compatible and simple | A subscriber failure left committed source state without a version row; retries and visibility were external concerns |
32
+ | Synchronous source-transaction write | Write `ValueVersion` before the `Value` transaction commits | Source and version row succeed or roll back together; smallest durable boundary | Requires `Value` and `ValueVersion` to share the same connection pool; changes callback ordering and needs explicit recursion/rollback tests |
33
+ | Generic outbox | Source transaction appends an event; a worker writes versions | Cross-process retry, replay, and operational observability | Adds schema, worker/queue, idempotency, retention, ordering, and deployment machinery before a cross-database or external-consumer requirement exists |
34
+
35
+ ## Decision
36
+
37
+ Characterization selects synchronous `ValueVersion` writes inside the source
38
+ transaction as the smallest follow-up boundary. Enablement remains
39
+ boot-latched: when versioning is disabled at boot, no subscriber is registered
40
+ and the disabled path adds no per-mutation hot-path predicate. When enabled,
41
+ activation must fail closed unless
42
+ `TypedEAV::Value.connection_pool.equal?(TypedEAV::ValueVersion.connection_pool)`
43
+ is true. A pool mismatch is a startup/configuration error, not a permitted
44
+ best-effort mode. This is the implemented production boundary documented by
45
+ this ADR.
46
+
47
+ The implementation uses actual Value callback-chain inspection as its
48
+ boot-latch truth, reinstalls missing callbacks idempotently, and rejects a
49
+ Value/ValueVersion pool mismatch before installation. BulkWrite preserves the
50
+ caller context and carries version-group correlation through an internal
51
+ pending marker. The implementation must preserve the existing write contract: registry opt-in,
52
+ exact entity/field tuple and tenant/partition semantics, before/after typed
53
+ snapshots, context, actor resolution, `changed_at`, version-group selection,
54
+ and pending-marker cleanup on both commit and rollback. A source rollback must
55
+ roll back its synchronous version rows; a successful source mutation produces
56
+ exactly one corresponding version row. Public application callbacks remain
57
+ best-effort, rescued and logged after commit, and must not be presented as
58
+ durable or fail-closed delivery.
59
+
60
+ The generic outbox is deferred. It becomes appropriate only if version events
61
+ must cross a database/process boundary or require an independently operated
62
+ consumer. Until then, its additional queue, retry, idempotency, ordering, and
63
+ retention surface is not justified.
64
+
65
+ ## Durability boundaries and historical limits
66
+
67
+ Idempotency is one version row per successful source mutation. A caller owns a
68
+ whole-source retry: retry the complete source transaction after a rollback or
69
+ connection failure, rather than replaying an individual version write. There is
70
+ no asynchronous replay cursor, checkpoint protocol, or durable event identity
71
+ in this boundary. Ordering is the order of mutations within one source
72
+ transaction; no total order is promised across concurrent transactions or
73
+ databases.
74
+
75
+ The synchronous boundary does not repair historical gaps created by the
76
+ historical after-commit path, and it does not infer or rewrite history after model,
77
+ registry, tenant, field, or snapshot semantics drift. Existing gaps remain
78
+ observable historical gaps and require a separately approved, application-owned
79
+ repair process if a consumer needs one. Version rows remain append-only;
80
+ retention, archival, and deletion are application-owned policies and are not
81
+ automated here.
82
+
83
+ ## Implemented bounded Field deletion contract
84
+
85
+ Field-dependent destruction must not use `delete_all`, broad unscoped deletes,
86
+ or early Field removal. The explicit
87
+ `Field#destroy_with_values_in_batches!(batch_size: 1_000)` API:
88
+
89
+ 1. identify the exact `field_id` and process dependent Values in bounded
90
+ primary-key keyset batches (`id > last_id`, ordered by `id`);
91
+ 2. destroy each Value through Active Record so Value callbacks and the selected
92
+ versioning boundary run, while retaining exact-field and source-transaction
93
+ isolation;
94
+ 3. commits each bounded batch independently; a retry re-derives the remaining
95
+ rows from the exact-field keyset without skipping or repeating committed
96
+ work;
97
+ 4. retry/resume until an exact-field query proves no dependent Values remain;
98
+ 5. destroy the Field only after the final drain proof, preserving its own
99
+ callbacks and policy semantics.
100
+
101
+ The operation is idempotent and preserves ordering by keyset position. It is
102
+ explicitly rejected inside an open transaction or when Field, Value, and
103
+ ValueVersion do not share one connection pool. The final drain locks the Field,
104
+ accepts at most one bounded residual batch, proves zero exact-field rows, and
105
+ only then invokes ordinary Field destruction. It does not claim that an
106
+ after-commit callback failure rolled back source data.
107
+
108
+ After the final Field deletion, the foreign keys intentionally null both
109
+ `ValueVersion.value_id` (when its Value is subsequently gone) and
110
+ `ValueVersion.field_id` (when the Field is gone). The durable `entity_type`,
111
+ `entity_id`, and payload can remain queryable, but direct Value/Field identity is
112
+ then lost. This is an accepted identity tradeoff, not something the ADR hides
113
+ or promises to reconstruct with a schema snapshot.
114
+
115
+ ## Evidence and limits
116
+
117
+ The real-commit regressions establish current failure semantics for Value
118
+ create/update/destroy and field-dependent cascades. They do not measure retry
119
+ cost, batch size, queue latency, or cross-database behavior. Those questions
120
+ require a separately approved implementation and protocol.
121
+
122
+ ## Related decisions
123
+
124
+ - ADR 0003 keeps the EventDispatcher as the internal callback broker.
125
+ - ADR 0012 keeps broad administrative work application-bounded.
data/docs/adr/index.md ADDED
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: "Design decisions"
3
+ nav_group: Project
4
+ nav_exclude: false
5
+ ---
6
+
7
+ # Design decisions
8
+
9
+ The choices below explain behavior you may encounter when integrating or
10
+ extending TypedEAV. Each summary covers the decision, why it exists, and what
11
+ it means for your application. Links lead to the full engineering rationale.
12
+
13
+ ## Fields and extension points
14
+
15
+ - **Fields own typed storage.** A custom field declares its columns and implements
16
+ logical reads, writes, and defaults. Change detection and snapshots derive from
17
+ those column declarations, avoiding duplicate storage mappings that can drift.
18
+ Multi-column types use the same extension pattern as built-in Currency.
19
+ [Rationale](0001-collapse-column-mapping-stack.md)
20
+ - **Related types share validation behavior.** String and range-bounded field
21
+ families, plus a shared option-handling concern, keep validation consistent.
22
+ Custom types can reuse these families instead of rebuilding length, range,
23
+ and option checks. [Rationale](0004-field-family-intermediate-bases.md)
24
+
25
+ For implementation examples, see [custom field types](../guides/fields.md#custom-field-types).
26
+
27
+ ## Scoping and queries
28
+
29
+ - **Field predicates and query composition are separate.** Per-field SQL handles
30
+ typed operands; higher-level queries handle filters and partition resolution.
31
+ This keeps type-specific behavior independent of how multiple filters combine.
32
+ Applications normally use the host model API rather than these internal classes.
33
+ [Rationale](0002-entity-query-orchestration.md)
34
+ - **Missing values are an explicit query choice.** Ordinary `is_null` matches an
35
+ existing NULL value row. With `include_missing: true`, the query also includes
36
+ hosts without a value row, using the caller's host set and excluding non-NULL
37
+ matches. This supports “is empty” searches without changing the default SQL-like
38
+ NULL contract. [Rationale](0006-include-missing-via-set-complement.md)
39
+ - **Visibility and mutation have different boundaries.** Reads may include global
40
+ and less-specific fallback definitions; ordering mutations target the exact
41
+ partition. Reordering a tenant's definitions therefore cannot renumber shared
42
+ fallback definitions or another partition's fields.
43
+ [Rationale](0007-visibility-versus-mutation-relations.md)
44
+ - **Multiple filters retain the existing subquery strategy.** TypedEAV combines
45
+ per-field matches through host `IN` predicates. Alternative query shapes have
46
+ not demonstrated a sufficiently reliable, semantically equivalent improvement
47
+ to justify a replacement. There is no adaptive query-strategy switch.
48
+ [Rationale](0011-multi-filter-query-strategy.md)
49
+ - **Cross-partition queries are an administrative tool.** `unscoped` considers
50
+ definitions across partitions, so broad queries can require substantially more
51
+ work. Applications own authorization, narrowing, and batching; the gem does not
52
+ claim a universal safe partition count. Normal tenant requests should use scoped
53
+ resolution. [Rationale](0012-cross-scope-administrative-query-policy.md)
54
+
55
+ See [query behavior](../guides/queries.md) and [scoping](../guides/scoping.md)
56
+ for the public contracts and examples.
57
+
58
+ ## Indexing and performance
59
+
60
+ - **Default scalar indexes omit NULL cells.** Values normally populate only their
61
+ applicable typed columns. Partial covering indexes avoid indexing unrelated NULL
62
+ cells while supporting non-NULL scalar queries. Separate NULL indexes are not
63
+ installed automatically because their storage and write costs depend on the
64
+ workload. [Rationale](0008-partial-covering-scalar-indexes.md)
65
+ - **Trigram indexes are application-owned.** The gem keeps its default string
66
+ B-tree and public `ILIKE` operators. Trigram indexing can help selected searches,
67
+ but short patterns and negative searches need different expectations. Applications
68
+ evaluate the extension, index, and write/storage costs on their own data.
69
+ [Rationale](0009-string-search-indexing.md)
70
+ - **Extended planner statistics are opt-in.** Correlated field/value predicates
71
+ can benefit from better estimates, but better estimates do not necessarily
72
+ improve runtime. TypedEAV does not install statistics objects or prescribe a
73
+ universal target; applications add them only when their query plans justify it.
74
+ [Rationale](0010-planner-statistics-policy.md)
75
+
76
+ These are workload choices, not universal speed guarantees. See
77
+ [storage and performance](../guides/performance.md) for evaluation guidance.
78
+
79
+ ## Imports, events, and audit history
80
+
81
+ - **Import utilities remain independently usable.** Schema portability moves
82
+ definitions, CSV mapping transforms rows, and bulk writing persists values.
83
+ Keeping them separate lets applications preview, validate, and save in the order
84
+ their workflow requires. Their different return values reflect different jobs;
85
+ there is no mandatory import pipeline.
86
+ [Rationale](0005-keep-phase-six-modules-independent.md)
87
+ - **Public event hooks have a separate error policy.** Public callbacks run after
88
+ commit, and their errors are logged rather than making a committed save appear
89
+ to have failed. The event dispatcher preserves that boundary. Applications
90
+ needing reliable external delivery must provide their own delivery mechanism.
91
+ [Rationale](0003-keep-event-dispatcher-broker.md)
92
+ - **Enabled audit history shares the value transaction.** Value changes and their
93
+ audit rows commit or roll back together, requiring a shared connection pool.
94
+ Public after-commit hooks remain separate. Large field deletions have an explicit
95
+ batched path that preserves value callbacks and versioning; committed batches
96
+ remain committed if a later batch fails, and the field is retained for retry.
97
+ [Rationale](0013-durable-versioning-and-field-deletion.md)
98
+
99
+ See [CSV mapping](../guides/csv-import.md), [schema portability](../guides/schema.md),
100
+ [bulk operations](../guides/bulk-operations.md), and
101
+ [events and versioning](../guides/events-and-versioning.md) for usage details.
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: "Getting started"
3
+ nav_group: Start here
4
+ ---
5
+
6
+ # Getting started
7
+
8
+ ## Compatibility
9
+
10
+ The canonical support contract lives in
11
+ [`.github/compatibility.json`](https://github.com/dchuk/typed_eav/blob/main/.github/compatibility.json). Typed EAV supports:
12
+
13
+ | Runtime | Supported versions |
14
+ |---|---|
15
+ | Ruby | 3.3 through 4.0 (`>= 3.3`, `< 4.1`) |
16
+ | Rails | 7.2 through 8.1 (`>= 7.2`, `< 8.2`) |
17
+ | PostgreSQL | 15 through 18 |
18
+
19
+ CI proves representative floor, middle, and ceiling combinations rather than
20
+ every Cartesian product. Versions outside these ranges and prerelease versions
21
+ are outside the support guarantee. PostgreSQL compatibility claims assume the
22
+ current minor release for each supported major version.
23
+
24
+ ## Installation
25
+
26
+ Add to your Gemfile:
27
+
28
+ ```ruby
29
+ gem "typed_eav"
30
+ ```
31
+
32
+ Run the install migration:
33
+
34
+ ```bash
35
+ bin/rails typed_eav:install:migrations
36
+ bin/rails db:migrate
37
+ ```
38
+
39
+ PostgreSQL is required; MySQL and SQLite are not supported. For existing
40
+ installations, see [Upgrading](guides/upgrading.md) and the
41
+ [Changelog](https://github.com/dchuk/typed_eav/blob/main/CHANGELOG.md).
42
+
43
+ ## Quick Start
44
+
45
+ Assuming your application already has a `Contact` model and table:
46
+
47
+ ```ruby
48
+ class Contact < ApplicationRecord
49
+ has_typed_eav
50
+ end
51
+
52
+ TypedEAV::Field::Integer.create!(
53
+ name: "age",
54
+ entity_type: Contact.polymorphic_name,
55
+ options: { min: 0, max: 150 }
56
+ )
57
+
58
+ contact = Contact.new
59
+ contact.set_typed_eav_value("age", "40")
60
+ contact.save! # Supply any other attributes your Contact model requires.
61
+
62
+ contact.typed_eav_value("age") # => 40 (Integer)
63
+ contact.typed_eav_hash # => { "age" => 40 }
64
+
65
+ Contact.with_field("age", :gteq, 21)
66
+ .order_typed_eav("age", direction: :desc)
67
+ .limit(25)
68
+ ```
69
+
70
+ Fields cast and validate both assigned values and query operands. Queries
71
+ return Active Record relations, so you can combine them with ordinary host
72
+ filters. Use `Contact.polymorphic_name` when creating definitions to respect
73
+ Rails' STI and namespaced-polymorphism settings.
74
+
75
+ See [Reading, writing, and forms](guides/usage.md) for bulk assignment,
76
+ nested attributes, form helpers, and the admin scaffold, or
77
+ [Querying typed fields](guides/queries.md) for operators, multi-field
78
+ filters, sorting, distinct values, counts, and numeric aggregates.
79
+