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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -0
  3. data/README.md +138 -323
  4. data/lib/activerecord/materialized/aggregate_analysis.rb +10 -31
  5. data/lib/activerecord/materialized/async_refresher.rb +18 -27
  6. data/lib/activerecord/materialized/cache_table_schema.rb +53 -34
  7. data/lib/activerecord/materialized/change_source.rb +25 -0
  8. data/lib/activerecord/materialized/cold_read.rb +1 -10
  9. data/lib/activerecord/materialized/column_type_inference.rb +167 -0
  10. data/lib/activerecord/materialized/configurable_table_name.rb +45 -0
  11. data/lib/activerecord/materialized/configuration.rb +104 -29
  12. data/lib/activerecord/materialized/connection_routing.rb +29 -0
  13. data/lib/activerecord/materialized/data_verification_result.rb +41 -0
  14. data/lib/activerecord/materialized/data_verifier.rb +162 -0
  15. data/lib/activerecord/materialized/debezium_envelope.rb +105 -0
  16. data/lib/activerecord/materialized/delta_maintainer.rb +4 -15
  17. data/lib/activerecord/materialized/dependency_registry.rb +43 -35
  18. data/lib/activerecord/materialized/dependency_trackable.rb +7 -14
  19. data/lib/activerecord/materialized/incremental_maintainer.rb +45 -28
  20. data/lib/activerecord/materialized/incremental_refresh.rb +63 -0
  21. data/lib/activerecord/materialized/instrumentation.rb +102 -0
  22. data/lib/activerecord/materialized/maintenance_delta.rb +4 -19
  23. data/lib/activerecord/materialized/maintenance_delta_builder.rb +46 -14
  24. data/lib/activerecord/materialized/maintenance_store.rb +58 -21
  25. data/lib/activerecord/materialized/metadata/maintenance_payload.rb +1 -7
  26. data/lib/activerecord/materialized/metadata/reconciliation.rb +25 -0
  27. data/lib/activerecord/materialized/metadata/schema.rb +19 -41
  28. data/lib/activerecord/materialized/metadata/timestamps.rb +0 -5
  29. data/lib/activerecord/materialized/metadata.rb +41 -25
  30. data/lib/activerecord/materialized/metadata_record.rb +2 -16
  31. data/lib/activerecord/materialized/migration_builder.rb +8 -8
  32. data/lib/activerecord/materialized/module_api.rb +219 -22
  33. data/lib/activerecord/materialized/partition_filter.rb +45 -0
  34. data/lib/activerecord/materialized/partition_keyed_store.rb +72 -0
  35. data/lib/activerecord/materialized/partition_record.rb +2 -16
  36. data/lib/activerecord/materialized/partition_snapshot.rb +79 -0
  37. data/lib/activerecord/materialized/partition_state.rb +47 -46
  38. data/lib/activerecord/materialized/query_expressions.rb +0 -12
  39. data/lib/activerecord/materialized/railtie.rb +15 -3
  40. data/lib/activerecord/materialized/read_router.rb +51 -0
  41. data/lib/activerecord/materialized/reconcile_job.rb +22 -0
  42. data/lib/activerecord/materialized/reconcile_result.rb +36 -0
  43. data/lib/activerecord/materialized/reconciler.rb +79 -0
  44. data/lib/activerecord/materialized/refresh_callbacks.rb +4 -18
  45. data/lib/activerecord/materialized/refresh_job.rb +0 -4
  46. data/lib/activerecord/materialized/refresh_result.rb +2 -10
  47. data/lib/activerecord/materialized/refresh_scheduler.rb +5 -7
  48. data/lib/activerecord/materialized/refresher.rb +35 -50
  49. data/lib/activerecord/materialized/registry.rb +26 -16
  50. data/lib/activerecord/materialized/relation_cache_writer.rb +12 -46
  51. data/lib/activerecord/materialized/schema_verifier.rb +0 -9
  52. data/lib/activerecord/materialized/source_watermark.rb +93 -0
  53. data/lib/activerecord/materialized/source_watermark_record.rb +16 -0
  54. data/lib/activerecord/materialized/summary_delta.rb +1 -17
  55. data/lib/activerecord/materialized/summary_delta_builder.rb +2 -10
  56. data/lib/activerecord/materialized/table_model_registry.rb +1 -8
  57. data/lib/activerecord/materialized/table_swap.rb +106 -0
  58. data/lib/activerecord/materialized/tasks.rb +46 -38
  59. data/lib/activerecord/materialized/version.rb +1 -2
  60. data/lib/activerecord/materialized/view.rb +11 -22
  61. data/lib/activerecord/materialized/view_configuration_class_methods.rb +23 -35
  62. data/lib/activerecord/materialized/view_definition.rb +76 -65
  63. data/lib/activerecord/materialized/view_incremental_class_methods.rb +57 -60
  64. data/lib/activerecord/materialized/view_loader.rb +49 -0
  65. data/lib/activerecord/materialized/view_query_access_class_methods.rb +100 -57
  66. data/lib/activerecord/materialized/view_refresh_policy_class_methods.rb +70 -37
  67. data/lib/activerecord/materialized/write_change.rb +75 -26
  68. data/lib/activerecord/materialized/write_maintenance.rb +73 -0
  69. data/lib/activerecord/materialized/write_outbox.rb +345 -0
  70. data/lib/activerecord/materialized/write_outbox_record.rb +17 -0
  71. data/lib/activerecord/materialized.rb +27 -6
  72. data/lib/generators/activerecord_materialized/install/templates/create_ar_materialized_view_metadata.rb.erb +14 -0
  73. data/lib/generators/activerecord_materialized/install_generator.rb +1 -6
  74. data/lib/generators/activerecord_materialized/migration_generator.rb +3 -9
  75. data/lib/generators/activerecord_materialized/outbox_generator.rb +36 -0
  76. data/lib/generators/activerecord_materialized/templates/materialized_view_migration.rb.erb +5 -1
  77. data/lib/generators/activerecord_materialized/templates/write_outbox_migration.rb.erb +21 -0
  78. data/lib/generators/activerecord_materialized/view_generator.rb +0 -4
  79. metadata +27 -18
  80. data/lib/activerecord/materialized/type_reexports.rb +0 -14
  81. data/lib/activerecord/materialized/view_class.rb +0 -8
  82. data/lib/activerecord_materialized_types.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3b0e7bdccf7c3354e443d0c824e1ac372392e66517c3422df35d6ccc393adb0
4
- data.tar.gz: a405e79709760fa5f7cadcfedae9ccd132f98081e9278cf127f01fdff0c8bbf7
3
+ metadata.gz: 20172f4721a9fec7ab702812329c1f217524f2b03b1a9334f9645b8439b584c4
4
+ data.tar.gz: d2c308b700d98bdfa9c3703e02acd9351ccaa4a8ea9655236878bdfe3288dd72
5
5
  SHA512:
6
- metadata.gz: e92ca73b2ad1c34dd4ba7952293a15ba12c87b357dd1d33efe2c10dbc26580e2e8eef73cc9b4c107e1c568f751482e506ee01acd17dcb7f7db45a0bf098f2779
7
- data.tar.gz: d8086c2df447684c4076577c8fd00982d67d4c0e773eef2680b2585f810ab4d83a3a046170dcdc5027793580b2ad50c6565f9e69247938bfc8b23f3b25c9e6cb
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.