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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -2
- data/README.md +79 -1710
- data/RELEASING.md +81 -0
- data/docs/adr/0001-collapse-column-mapping-stack.md +28 -0
- data/docs/adr/0002-entity-query-orchestration.md +33 -0
- data/docs/adr/0003-keep-event-dispatcher-broker.md +41 -0
- data/docs/adr/0004-field-family-intermediate-bases.md +49 -0
- data/docs/adr/0005-keep-phase-six-modules-independent.md +48 -0
- data/docs/adr/0006-include-missing-via-set-complement.md +77 -0
- data/docs/adr/0007-visibility-versus-mutation-relations.md +33 -0
- data/docs/adr/0008-partial-covering-scalar-indexes.md +83 -0
- data/docs/adr/0009-string-search-indexing.md +110 -0
- data/docs/adr/0010-planner-statistics-policy.md +109 -0
- data/docs/adr/0011-multi-filter-query-strategy.md +111 -0
- data/docs/adr/0012-cross-scope-administrative-query-policy.md +76 -0
- data/docs/adr/0013-durable-versioning-and-field-deletion.md +125 -0
- data/docs/adr/index.md +101 -0
- data/docs/getting-started.md +79 -0
- data/docs/guides/architecture.md +125 -0
- data/docs/guides/bulk-operations.md +205 -0
- data/docs/guides/csv-import.md +88 -0
- data/docs/guides/development.md +73 -0
- data/docs/guides/events-and-versioning.md +360 -0
- data/docs/guides/fields.md +342 -0
- data/docs/guides/performance.md +107 -0
- data/docs/guides/queries.md +188 -0
- data/docs/guides/schema.md +134 -0
- data/docs/guides/scoping.md +254 -0
- data/docs/guides/upgrading.md +26 -0
- data/docs/guides/usage.md +259 -0
- data/docs/index.md +44 -0
- data/docs/maintaining.md +82 -0
- data/docs/reference/api.md +133 -0
- data/docs/reference/configuration.md +64 -0
- data/docs/reference/index.md +16 -0
- data/lib/typed_eav/version.rb +1 -1
- 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
|
+
|