activerecord-materialized 0.1.1 → 0.2.0
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 +93 -0
- data/README.md +138 -323
- data/lib/activerecord/materialized/aggregate_analysis.rb +10 -31
- data/lib/activerecord/materialized/async_refresher.rb +18 -27
- data/lib/activerecord/materialized/cache_table_schema.rb +53 -34
- data/lib/activerecord/materialized/change_source.rb +25 -0
- data/lib/activerecord/materialized/cold_read.rb +1 -10
- data/lib/activerecord/materialized/column_type_inference.rb +167 -0
- data/lib/activerecord/materialized/configurable_table_name.rb +45 -0
- data/lib/activerecord/materialized/configuration.rb +104 -29
- data/lib/activerecord/materialized/connection_routing.rb +29 -0
- data/lib/activerecord/materialized/data_verification_result.rb +41 -0
- data/lib/activerecord/materialized/data_verifier.rb +162 -0
- data/lib/activerecord/materialized/debezium_envelope.rb +105 -0
- data/lib/activerecord/materialized/delta_maintainer.rb +4 -15
- data/lib/activerecord/materialized/dependency_registry.rb +43 -35
- data/lib/activerecord/materialized/dependency_trackable.rb +7 -14
- data/lib/activerecord/materialized/incremental_maintainer.rb +45 -28
- data/lib/activerecord/materialized/incremental_refresh.rb +63 -0
- data/lib/activerecord/materialized/instrumentation.rb +102 -0
- data/lib/activerecord/materialized/maintenance_delta.rb +4 -19
- data/lib/activerecord/materialized/maintenance_delta_builder.rb +46 -14
- data/lib/activerecord/materialized/maintenance_store.rb +58 -21
- data/lib/activerecord/materialized/metadata/maintenance_payload.rb +1 -7
- data/lib/activerecord/materialized/metadata/reconciliation.rb +25 -0
- data/lib/activerecord/materialized/metadata/schema.rb +19 -41
- data/lib/activerecord/materialized/metadata/timestamps.rb +0 -5
- data/lib/activerecord/materialized/metadata.rb +41 -25
- data/lib/activerecord/materialized/metadata_record.rb +2 -16
- data/lib/activerecord/materialized/migration_builder.rb +8 -8
- data/lib/activerecord/materialized/module_api.rb +219 -22
- data/lib/activerecord/materialized/partition_filter.rb +45 -0
- data/lib/activerecord/materialized/partition_keyed_store.rb +72 -0
- data/lib/activerecord/materialized/partition_record.rb +2 -16
- data/lib/activerecord/materialized/partition_snapshot.rb +79 -0
- data/lib/activerecord/materialized/partition_state.rb +47 -46
- data/lib/activerecord/materialized/query_expressions.rb +0 -12
- data/lib/activerecord/materialized/railtie.rb +15 -3
- data/lib/activerecord/materialized/read_router.rb +51 -0
- data/lib/activerecord/materialized/reconcile_job.rb +22 -0
- data/lib/activerecord/materialized/reconcile_result.rb +36 -0
- data/lib/activerecord/materialized/reconciler.rb +79 -0
- data/lib/activerecord/materialized/refresh_callbacks.rb +4 -18
- data/lib/activerecord/materialized/refresh_job.rb +0 -4
- data/lib/activerecord/materialized/refresh_result.rb +2 -10
- data/lib/activerecord/materialized/refresh_scheduler.rb +5 -7
- data/lib/activerecord/materialized/refresher.rb +35 -50
- data/lib/activerecord/materialized/registry.rb +26 -16
- data/lib/activerecord/materialized/relation_cache_writer.rb +12 -46
- data/lib/activerecord/materialized/schema_verifier.rb +0 -9
- data/lib/activerecord/materialized/source_watermark.rb +93 -0
- data/lib/activerecord/materialized/source_watermark_record.rb +16 -0
- data/lib/activerecord/materialized/summary_delta.rb +1 -17
- data/lib/activerecord/materialized/summary_delta_builder.rb +2 -10
- data/lib/activerecord/materialized/table_model_registry.rb +1 -8
- data/lib/activerecord/materialized/table_swap.rb +106 -0
- data/lib/activerecord/materialized/tasks.rb +46 -38
- data/lib/activerecord/materialized/version.rb +1 -2
- data/lib/activerecord/materialized/view.rb +11 -22
- data/lib/activerecord/materialized/view_configuration_class_methods.rb +23 -35
- data/lib/activerecord/materialized/view_definition.rb +76 -65
- data/lib/activerecord/materialized/view_incremental_class_methods.rb +57 -60
- data/lib/activerecord/materialized/view_loader.rb +49 -0
- data/lib/activerecord/materialized/view_query_access_class_methods.rb +100 -57
- data/lib/activerecord/materialized/view_refresh_policy_class_methods.rb +70 -37
- data/lib/activerecord/materialized/write_change.rb +75 -26
- data/lib/activerecord/materialized/write_maintenance.rb +73 -0
- data/lib/activerecord/materialized/write_outbox.rb +345 -0
- data/lib/activerecord/materialized/write_outbox_record.rb +17 -0
- data/lib/activerecord/materialized.rb +27 -6
- data/lib/generators/activerecord_materialized/install/templates/create_ar_materialized_view_metadata.rb.erb +14 -0
- data/lib/generators/activerecord_materialized/install_generator.rb +1 -6
- data/lib/generators/activerecord_materialized/migration_generator.rb +3 -9
- data/lib/generators/activerecord_materialized/outbox_generator.rb +36 -0
- data/lib/generators/activerecord_materialized/templates/materialized_view_migration.rb.erb +5 -1
- data/lib/generators/activerecord_materialized/templates/write_outbox_migration.rb.erb +21 -0
- data/lib/generators/activerecord_materialized/view_generator.rb +0 -4
- metadata +27 -18
- data/lib/activerecord/materialized/type_reexports.rb +0 -14
- data/lib/activerecord/materialized/view_class.rb +0 -8
- data/lib/activerecord_materialized_types.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20172f4721a9fec7ab702812329c1f217524f2b03b1a9334f9645b8439b584c4
|
|
4
|
+
data.tar.gz: d2c308b700d98bdfa9c3703e02acd9351ccaa4a8ea9655236878bdfe3288dd72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 631f0b027ab071ca180ea19f0643b9f7917a508d734f87f06b26ef5a16c871b5103010b63f8ae020ac20cb7d0394d2e297c0f1d7c0b2e4b207eef37323611044
|
|
7
|
+
data.tar.gz: 8ca8d97f0c6b2fa76fac92c546429373d5d2e4348ac446e18bb5bfe9820653713bb9433d66a423c33e287c8cbf94ecf20cfec0db3fda44d5459425bd2209294a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Pluggable change sources & CDC ingestion.** Change *detection* is now decoupled from view
|
|
12
|
+
*maintenance* behind a public ingestion API — `publish_write_change!` and `mark_dirty_for_tables!`
|
|
13
|
+
(with the lower-level `ingest_change` for a normalized change descriptor) — so a view can be fed
|
|
14
|
+
from ActiveRecord callbacks (still the default), raw SQL, another service, or a CDC stream. Choose
|
|
15
|
+
the source globally with `config.default_change_source` or per view with `change_source :none`;
|
|
16
|
+
each view is fed by exactly one source. A dependency-free Debezium envelope adapter
|
|
17
|
+
(`ingest_debezium_change`) maps a decoded change event straight into the pipeline, and optional
|
|
18
|
+
monotonic **source watermarks** (`source_ts:`, exposed via `View.source_watermark`) suppress
|
|
19
|
+
provably-stale out-of-order changes and surface per-view freshness/lag; Debezium's `source.ts_ms`
|
|
20
|
+
is forwarded as that watermark automatically. Delivery is at-least-once and order-tolerant —
|
|
21
|
+
duplicates and reordering converge. Validated end-to-end against real MySQL binlog and Postgres
|
|
22
|
+
logical-replication decoding. See the [Change sources](docs/change-sources.md) docs.
|
|
23
|
+
(#80, #105, #106, #113, #117, #118)
|
|
24
|
+
- **Out-of-band write capture via triggers/outbox.** A database-native outbox change source
|
|
25
|
+
(`WriteOutbox.install_triggers!` + `drain_write_outbox!`, with an `activerecord_materialized:outbox`
|
|
26
|
+
generator) captures writes that bypass both ActiveRecord callbacks and the ingestion API — raw SQL,
|
|
27
|
+
bulk backfills, or another service on the shared database. Triggers record the changed `GROUP BY`
|
|
28
|
+
key columns, scoped per operation so a partition-moving update maintains both the old and new
|
|
29
|
+
partition, and draining is batched, at-least-once, and per-row isolated. Portable across Postgres,
|
|
30
|
+
MySQL, and SQLite; opt-in. See the [detecting out-of-band writes](docs/out-of-band-writes.md) guide.
|
|
31
|
+
(#68)
|
|
32
|
+
- **Self-healing reconciliation & data-drift detection.** A `DataVerifier`
|
|
33
|
+
(`verify_data` / `verify_data!`, `materialized:audit`) recomputes the source per partition and
|
|
34
|
+
reports `missing` / `extra` / `mismatched` keys, with `:row_count`, `:checksum`, and `:full` modes
|
|
35
|
+
plus cheap sampling for large views. Built on it, self-healing reconciliation
|
|
36
|
+
(`reconcile!` / `reconcile_stale!`, `materialized:reconcile`) verifies views on a schedule and
|
|
37
|
+
**scoped-repairs** whatever the change source missed — never a full `rebuild!` — bounding staleness
|
|
38
|
+
in time and composing with `max_staleness`. Reconciliation is safe alongside normal refresh: it
|
|
39
|
+
drains pending maintenance first, defers rather than double-maintains when a refresh already holds
|
|
40
|
+
the cycle, and isolates per-view failures. See
|
|
41
|
+
[Data integrity: drift detection & self-healing](docs/reconciliation.md). (#62)
|
|
42
|
+
- **Distributed / HA maintenance.** New `enqueue_stale_refreshes!` / `enqueue_stale_reconciles!`
|
|
43
|
+
(and matching rake tasks) fan the periodic bounded-staleness backstop out across the job fleet as
|
|
44
|
+
one job per stale view, backed by a new `ReconcileJob` that re-checks staleness before working.
|
|
45
|
+
Writer/replica topology support routes maintenance to the primary and verification reads to a
|
|
46
|
+
replica (`config.maintenance_role` / `config.verification_role`), reads cache and source in a
|
|
47
|
+
single snapshot to avoid false drift, and folds a `config.replica_lag` budget into `stale?`.
|
|
48
|
+
Cross-process cycles are serialized by a metadata-row lock so concurrent servers apply additive
|
|
49
|
+
deltas exactly once and recompute a partition once. Run the periodic tick from a single owner — the
|
|
50
|
+
gem ships no scheduler or leader election. See the
|
|
51
|
+
[distributed deployment](docs/distributed-deployment.md) docs. (#92, #93, #94, #95)
|
|
52
|
+
- **Observability via `ActiveSupport::Notifications`.** The read, refresh, and maintenance lifecycle
|
|
53
|
+
points emit documented events (`read.active_record_materialized`,
|
|
54
|
+
`refresh.active_record_materialized`, `maintenance.active_record_materialized`) with stable
|
|
55
|
+
payloads — cache-hit vs read-through and staleness on reads; duration, mode, partitions recomputed,
|
|
56
|
+
and outcome on refreshes; and a widen-to-full signal on writes. No telemetry-vendor dependency;
|
|
57
|
+
read events fire only when a subscriber is attached. See the [Observability](docs/observability.md)
|
|
58
|
+
docs.
|
|
59
|
+
- **Scoped maintenance for joined-table keys.** `partition_key_for(table) { |change| ... }` maps a
|
|
60
|
+
write on a joined/leaf dependency table — whose own payload lacks the `GROUP BY` key — to the
|
|
61
|
+
affected partition key(s), keeping maintenance scoped to those partitions instead of widening to a
|
|
62
|
+
full recompute. See the [joined-table docs](docs/architecture.md#views-whose-group-key-lives-on-a-joined-table).
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **`config.refresh_dispatcher` now auto-resolves to `:active_job` when ActiveJob is loaded**
|
|
67
|
+
(previously the in-process `:async` thread), so a typical multi-server Rails deployment coordinates
|
|
68
|
+
refresh across servers by default; an explicit `config.refresh_dispatcher` still wins.
|
|
69
|
+
**Upgrade note:** ensure a worker drains `config.refresh_queue_name` (default `:materialized_views`)
|
|
70
|
+
or set `config.refresh_dispatcher = :async` explicitly — otherwise refresh-on-write jobs enqueue
|
|
71
|
+
with nothing to run them and views serve stale reads. A boot warning fires when the in-process
|
|
72
|
+
refresher is active despite ActiveJob being available. `config.reconcile_queue_name` defaults to the
|
|
73
|
+
refresh queue. (#93)
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **Cold-view widen correctness.** A full-partition (widen) recompute can't be applied to a never-built
|
|
78
|
+
(cold) view; letting one through previously killed populate-on-read until a manual `rebuild!`. Every
|
|
79
|
+
widen producer now funnels through a single `MaintenanceStore#merge!` chokepoint that drops the
|
|
80
|
+
cold-view recompute and resets the fresh set, and a lock-free per-view **epoch**
|
|
81
|
+
(`fresh_set_generation`) closes a populate-vs-widen race that could otherwise serve stale data on a
|
|
82
|
+
cold view. Reads fall through to the always-correct source and repopulate on the next miss;
|
|
83
|
+
warm/materialized views are unaffected. (#110, #120)
|
|
84
|
+
- **Partition-moving update under-scoping.** A CDC/ingestion `:update` with a partial `before`-image
|
|
85
|
+
(no `GROUP BY` key — the common non-FULL image: Postgres default `REPLICA IDENTITY`, MySQL
|
|
86
|
+
`binlog-row-image=minimal`) previously left the *old* partition stale. The delta builder now
|
|
87
|
+
recognizes it cannot identify every affected partition from a partial image and widens to a full
|
|
88
|
+
recompute, honoring the ingestion API's documented always-correct guarantee. Scoped updates with
|
|
89
|
+
full before/after images (or `key_attributes`) are unchanged. (#110)
|
|
90
|
+
|
|
91
|
+
### Internal
|
|
92
|
+
|
|
93
|
+
- Shared per-partition-store base (`PartitionKeyedStore`) factored out of `PartitionState` and
|
|
94
|
+
`SourceWatermark`, and documentation/tooling cleanup. (#115, #124)
|
|
95
|
+
|
|
3
96
|
## 0.1.0 (2026-06-18)
|
|
4
97
|
|
|
5
98
|
Initial release.
|