Module: ActiveRecord::Materialized
- Defined in:
- lib/activerecord/materialized/module_api.rb,
lib/activerecord/materialized/view.rb,
lib/activerecord/materialized/tasks.rb,
lib/activerecord/materialized/railtie.rb,
lib/activerecord/materialized/version.rb,
lib/activerecord/materialized/metadata.rb,
lib/activerecord/materialized/registry.rb,
lib/activerecord/materialized/cold_read.rb,
lib/activerecord/materialized/refresher.rb,
lib/activerecord/materialized/reconciler.rb,
lib/activerecord/materialized/table_swap.rb,
lib/activerecord/materialized/read_router.rb,
lib/activerecord/materialized/refresh_job.rb,
lib/activerecord/materialized/view_loader.rb,
lib/activerecord/materialized/write_change.rb,
lib/activerecord/materialized/write_outbox.rb,
lib/activerecord/materialized/change_source.rb,
lib/activerecord/materialized/configuration.rb,
lib/activerecord/materialized/data_verifier.rb,
lib/activerecord/materialized/reconcile_job.rb,
lib/activerecord/materialized/summary_delta.rb,
lib/activerecord/materialized/refresh_result.rb,
lib/activerecord/materialized/async_refresher.rb,
lib/activerecord/materialized/instrumentation.rb,
lib/activerecord/materialized/metadata/schema.rb,
lib/activerecord/materialized/metadata_record.rb,
lib/activerecord/materialized/partition_state.rb,
lib/activerecord/materialized/schema_verifier.rb,
lib/activerecord/materialized/view_definition.rb,
lib/activerecord/materialized/delta_maintainer.rb,
lib/activerecord/materialized/partition_filter.rb,
lib/activerecord/materialized/partition_record.rb,
lib/activerecord/materialized/reconcile_result.rb,
lib/activerecord/materialized/source_watermark.rb,
lib/activerecord/materialized/debezium_envelope.rb,
lib/activerecord/materialized/maintenance_delta.rb,
lib/activerecord/materialized/maintenance_store.rb,
lib/activerecord/materialized/migration_builder.rb,
lib/activerecord/materialized/query_expressions.rb,
lib/activerecord/materialized/refresh_callbacks.rb,
lib/activerecord/materialized/refresh_scheduler.rb,
lib/activerecord/materialized/write_maintenance.rb,
lib/activerecord/materialized/aggregate_analysis.rb,
lib/activerecord/materialized/cache_table_schema.rb,
lib/activerecord/materialized/connection_routing.rb,
lib/activerecord/materialized/partition_snapshot.rb,
lib/activerecord/materialized/dependency_registry.rb,
lib/activerecord/materialized/incremental_refresh.rb,
lib/activerecord/materialized/metadata/timestamps.rb,
lib/activerecord/materialized/write_outbox_record.rb,
lib/activerecord/materialized/dependency_trackable.rb,
lib/activerecord/materialized/table_model_registry.rb,
lib/activerecord/materialized/column_type_inference.rb,
lib/activerecord/materialized/partition_keyed_store.rb,
lib/activerecord/materialized/relation_cache_writer.rb,
lib/activerecord/materialized/summary_delta_builder.rb,
lib/activerecord/materialized/incremental_maintainer.rb,
lib/activerecord/materialized/configurable_table_name.rb,
lib/activerecord/materialized/metadata/reconciliation.rb,
lib/activerecord/materialized/source_watermark_record.rb,
lib/activerecord/materialized/data_verification_result.rb,
lib/activerecord/materialized/maintenance_delta_builder.rb,
lib/activerecord/materialized/metadata/maintenance_payload.rb,
lib/activerecord/materialized/view_incremental_class_methods.rb,
lib/activerecord/materialized/view_query_access_class_methods.rb,
lib/activerecord/materialized/view_configuration_class_methods.rb,
lib/activerecord/materialized/view_refresh_policy_class_methods.rb
Overview
Application-level materialized views for Rails/ActiveRecord on databases without native materialized-view support (MySQL, MariaDB, SQLite).
Define views by subclassing View. This module is the top-level entry point for global configuration and operational helpers such as Materialized.verify_schema!.
Defined Under Namespace
Modules: CacheTableSchema, ChangeSource, ColumnTypeInference, ConfigurableTableName, ConnectionRouting, DebeziumEnvelope, DependencyTrackable, Instrumentation, QueryExpressions, RefreshCallbacks, Tasks, ViewConfigurationClassMethods, ViewIncrementalClassMethods, ViewQueryAccessClassMethods, ViewRefreshPolicyClassMethods, WriteOutbox Classes: AggregateAnalysis, AsyncRefresher, ColdRead, Configuration, DataVerificationResult, DataVerifier, DeltaMaintainer, DependencyRegistry, IncrementalMaintainer, IncrementalRefresh, MaintenanceDelta, MaintenanceDeltaBuilder, MaintenanceStore, Metadata, MetadataRecord, MigrationBuilder, NotMaterializedError, PartitionFilter, PartitionKeyedStore, PartitionRecord, PartitionSnapshot, PartitionState, Railtie, ReadRouter, ReconcileJob, ReconcileResult, Reconciler, RefreshJob, RefreshResult, RefreshScheduler, Refresher, Registry, RelationCacheWriter, SchemaVerifier, SourceWatermark, SourceWatermarkRecord, SummaryDelta, SummaryDeltaBuilder, TableModelRegistry, TableSwap, View, ViewDefinition, ViewLoader, WriteChange, WriteMaintenance, WriteOutboxRecord
Constant Summary collapse
- VERSION =
The gem version.
"0.2.0"
Class Attribute Summary collapse
-
.configuration ⇒ Configuration
The global configuration object.
Class Method Summary collapse
- .atomic_swap_refresh? ⇒ Boolean
-
.configure {|config| ... } ⇒ void
Configure the gem, typically from an initializer.
-
.drain_write_outbox!(limit: nil) ⇒ Integer
Relay pending WriteOutbox rows (captured by database triggers for out-of-band writes) through #ingest_change, then delete them.
-
.enqueue_stale_reconciles!(mode: :checksum, sample: nil) ⇒ Array<String>
Fans the periodic reconcile backstop across an ActiveJob fleet: enqueues one ReconcileJob per stale, materialized view so many workers share the (expensive) drift verification instead of one process running it serially.
-
.enqueue_stale_refreshes! ⇒ Array<String>
The fan-out form of the serial Registry.refresh_stale!: enqueues one RefreshJob per stale, materialized view.
-
.ingest_change(table:, operation:, source_ts: nil, **images) ⇒ void
Ingests a change described as a normalized descriptor — the CDC-friendly entry point for a consumer that has the table, operation, and (optionally) the changed key columns or full before/after images as plain data rather than an ActiveRecord object.
-
.ingest_debezium_change(envelope, table = nil) ⇒ void
Relay one Debezium CDC change envelope through #ingest_change — mapping
op(c/r → create, u → update, d → destroy), the +before+/+after+ row images,source.table, and the envelope'ssource.ts_msas thesource_tswatermark for you (a nestedpayloadis unwrapped). -
.mark_dirty_for_tables!(tables) ⇒ void
Coarse ingestion signal for callers that cannot describe the individual write: enqueues a full recompute for every view depending on any of
tablesand schedules it per each view's refresh strategy (inline for:immediate, in the background for:async). - .metadata_table_name ⇒ Object
- .partition_table_name ⇒ Object
-
.publish_write_change!(change) ⇒ void
Publishes a committed dependency write from a custom change source (a CDC/replication stream, a bulk loader, another service).
-
.reconcile!(mode: :checksum, sample: nil) ⇒ Array<ReconcileResult>
Reconciles every registered view — verifies its contents against the source and repairs any drift with scoped maintenance (never a full rebuild), returning a ReconcileResult per view.
-
.reconcile_stale!(mode: :checksum, sample: nil) ⇒ Array<ReconcileResult>
Like Materialized.reconcile! but only for stale views — dirty, never refreshed, or past
max_staleness— the scheduled bounded-staleness backstop (fresh views are skipped). -
.verify_data(mode: :checksum, sample: nil) ⇒ Array<DataVerificationResult>
Checks every registered view's materialized contents against its source relation and returns a DataVerificationResult per view (never raises on drift), so callers can alert on or repair the divergent partition keys.
-
.verify_data!(mode: :checksum, sample: nil) ⇒ Array<DataVerificationResult>
Like Materialized.verify_data but raises DataVerifier::DataDriftError if any view has drifted — for a boot/CI/cron gate.
-
.verify_schema! ⇒ void
Verifies every registered view's cache table still matches the columns its source relation projects — run it at boot or in CI to catch a view whose definition changed without a migration.
-
.warn_if_in_process_dispatcher!(logger: default_logger) ⇒ void
Logged once at boot (see Railtie) when ActiveJob is available but the effective dispatcher is still the in-process refresher — it does not coordinate across processes, so it is a correctness/efficiency hazard in a multi-process deployment.
Class Attribute Details
.configuration ⇒ Configuration
The global configuration object. Prefer configure for setting values.
20 21 22 23 24 25 26 27 |
# File 'lib/activerecord/materialized/module_api.rb', line 20 def configuration config = @configuration if config.nil? config = Configuration.new @configuration = config end config end |
Class Method Details
.atomic_swap_refresh? ⇒ Boolean
235 |
# File 'lib/activerecord/materialized/module_api.rb', line 235 def atomic_swap_refresh? = configuration.atomic_swap_refresh |
.configure {|config| ... } ⇒ void
This method returns an undefined value.
Configure the gem, typically from an initializer.
40 41 42 |
# File 'lib/activerecord/materialized/module_api.rb', line 40 def configure(&block) yield(configuration) end |
.drain_write_outbox!(limit: nil) ⇒ Integer
Relay pending WriteOutbox rows (captured by database triggers for out-of-band writes)
through #ingest_change, then delete them. The operational entry point run from a poller,
cron, or background job when a table has trigger/outbox capture installed. Returns the number
of rows relayed so a caller can loop until drained. See docs/out-of-band-writes.md.
233 |
# File 'lib/activerecord/materialized/module_api.rb', line 233 def drain_write_outbox!(limit: nil) = WriteOutbox.drain!(limit: limit) |
.enqueue_stale_reconciles!(mode: :checksum, sample: nil) ⇒ Array<String>
Fans the periodic reconcile backstop across an ActiveJob fleet: enqueues one ReconcileJob per stale, materialized view so many workers share the (expensive) drift verification instead of one process running it serially. Run it from a SINGLE owner — a leader, a dedicated instance, or one recurring job — never as cron on every server (see the distributed-deployment guide). Requires ActiveJob; without it, use reconcile_stale!.
114 115 116 117 118 |
# File 'lib/activerecord/materialized/module_api.rb', line 114 def enqueue_stale_reconciles!(mode: :checksum, sample: nil) enqueue_for_each_stale_view(:enqueue_stale_reconciles!) do |view_class| ReconcileJob.perform_later(view_class.view_key, mode: mode, sample: sample) end end |
.enqueue_stale_refreshes! ⇒ Array<String>
The fan-out form of the serial ActiveRecord::Materialized::Registry.refresh_stale!: enqueues one RefreshJob per stale, materialized view. Same single-owner rule as enqueue_stale_reconciles!. Requires ActiveJob.
124 125 126 127 128 |
# File 'lib/activerecord/materialized/module_api.rb', line 124 def enqueue_stale_refreshes! enqueue_for_each_stale_view(:enqueue_stale_refreshes!) do |view_class| RefreshJob.perform_later(view_class.view_key) end end |
.ingest_change(table:, operation:, source_ts: nil, **images) ⇒ void
This method returns an undefined value.
Ingests a change described as a normalized descriptor — the CDC-friendly entry point for a consumer that has the table, operation, and (optionally) the changed key columns or full before/after images as plain data rather than an ActiveRecord object. Builds a WriteChange and publishes it via publish_write_change!, so it drives the externally-fed (+change_source :none+) views on the table; a callback-driven view is left to its own callbacks — recover one after a bulk load with mark_dirty_for_tables!.
Supply +before+/+after+ images when the stream carries them; otherwise
key_attributes scopes maintenance to the affected partition(s). With
neither — or a partial :update image that cannot identify both the old
and new partition — maintenance widens to a full recompute (always correct).
See ActiveRecord::Materialized::WriteChange.from_descriptor.
198 199 200 201 202 203 204 205 206 207 |
# File 'lib/activerecord/materialized/module_api.rb', line 198 def ingest_change(table:, operation:, source_ts: nil, **images) unless source_ts.nil? || source_ts.is_a?(Integer) raise ArgumentError, "source_ts must be an Integer (e.g. a Debezium ts_ms or a Kafka offset); got #{source_ts.class}" end change = WriteChange.from_descriptor(table_name: table, operation: operation, **images) change = change.with_source_ts(source_ts) if source_ts publish_write_change!(change) end |
.ingest_debezium_change(envelope, table = nil) ⇒ void
This method returns an undefined value.
Relay one Debezium CDC change envelope through #ingest_change — mapping op (c/r → create,
u → update, d → destroy), the +before+/+after+ row images, source.table, and the envelope's
source.ts_ms as the source_ts watermark for you (a nested
payload is unwrapped). A nil envelope (a Kafka tombstone) is a no-op; a non-tombstone with
no op, or an unsupported op, raises. Pass table to override the target when it differs
from source.table. table is positional (not a keyword) so an inline envelope literal is not
misparsed as keyword arguments. See DebeziumEnvelope and the CDC section of the README.
221 222 223 224 |
# File 'lib/activerecord/materialized/module_api.rb', line 221 def ingest_debezium_change(envelope, table = nil) descriptor = DebeziumEnvelope.to_change_descriptor(envelope, table) ingest_change(**descriptor) if descriptor end |
.mark_dirty_for_tables!(tables) ⇒ void
This method returns an undefined value.
Coarse ingestion signal for callers that cannot describe the individual
write: enqueues a full recompute for every view depending on any of
tables and schedules it per each view's refresh strategy (inline for
:immediate, in the background for :async). Idempotent, so it is safe to
call repeatedly and to recover a view after a callback-skipping bulk load.
166 167 168 |
# File 'lib/activerecord/materialized/module_api.rb', line 166 def mark_dirty_for_tables!(tables) DependencyRegistry.mark_dirty_for_tables!(tables) end |
.metadata_table_name ⇒ Object
44 |
# File 'lib/activerecord/materialized/module_api.rb', line 44 def = configuration. |
.partition_table_name ⇒ Object
46 |
# File 'lib/activerecord/materialized/module_api.rb', line 46 def partition_table_name = configuration.partition_table_name |
.publish_write_change!(change) ⇒ void
This method returns an undefined value.
Publishes a committed dependency write from a custom change source (a
CDC/replication stream, a bulk loader, another service). It drives the
externally-fed views (change_source :none) that depend on the table;
callback-driven views are left to their own commit callbacks so no view is
maintained twice. To recover a callback-driven view after a callback-skipping
bulk load, use mark_dirty_for_tables! instead.
154 155 156 |
# File 'lib/activerecord/materialized/module_api.rb', line 154 def publish_write_change!(change) DependencyRegistry.publish_write_change!(change) end |
.reconcile!(mode: :checksum, sample: nil) ⇒ Array<ReconcileResult>
Reconciles every registered view — verifies its contents against the source and repairs any drift with scoped maintenance (never a full rebuild), returning a ReconcileResult per view. See Reconciler.
90 91 92 |
# File 'lib/activerecord/materialized/module_api.rb', line 90 def reconcile!(mode: :checksum, sample: nil) Registry.reconcile_all!(mode: mode, sample: sample) end |
.reconcile_stale!(mode: :checksum, sample: nil) ⇒ Array<ReconcileResult>
Like reconcile! but only for stale views — dirty, never refreshed, or past
max_staleness — the scheduled bounded-staleness backstop (fresh views are
skipped). Drive it from cron or ActiveJob.
101 102 103 |
# File 'lib/activerecord/materialized/module_api.rb', line 101 def reconcile_stale!(mode: :checksum, sample: nil) Registry.reconcile_stale!(mode: mode, sample: sample) end |
.verify_data(mode: :checksum, sample: nil) ⇒ Array<DataVerificationResult>
Checks every registered view's materialized contents against its source relation and returns a DataVerificationResult per view (never raises on drift), so callers can alert on or repair the divergent partition keys.
66 67 68 |
# File 'lib/activerecord/materialized/module_api.rb', line 66 def verify_data(mode: :checksum, sample: nil) Registry.all.map { |view_class| DataVerifier.new(view_class, mode: mode, sample: sample).verify } end |
.verify_data!(mode: :checksum, sample: nil) ⇒ Array<DataVerificationResult>
Like verify_data but raises ActiveRecord::Materialized::DataVerifier::DataDriftError if any view has drifted — for a boot/CI/cron gate. Returns the results when all are clean.
75 76 77 78 79 80 81 |
# File 'lib/activerecord/materialized/module_api.rb', line 75 def verify_data!(mode: :checksum, sample: nil) results = verify_data(mode: mode, sample: sample) drifted = results.select(&:drifted?) return results if drifted.empty? raise DataVerifier::DataDriftError, (drifted) end |
.verify_schema! ⇒ void
This method returns an undefined value.
Verifies every registered view's cache table still matches the columns its source relation projects — run it at boot or in CI to catch a view whose definition changed without a migration. Never alters tables.
54 55 56 57 |
# File 'lib/activerecord/materialized/module_api.rb', line 54 def verify_schema! registered = Registry.all registered.each { |view_class| SchemaVerifier.new(view_class).verify! } end |
.warn_if_in_process_dispatcher!(logger: default_logger) ⇒ void
This method returns an undefined value.
Logged once at boot (see Railtie) when ActiveJob is available but the effective dispatcher is still the in-process refresher — it does not coordinate across processes, so it is a correctness/efficiency hazard in a multi-process deployment. Silent when dispatching via ActiveJob, and silent when ActiveJob isn't loaded at all (a genuinely single-process app, for which the in-process refresher is the right and only choice).
138 139 140 141 142 143 |
# File 'lib/activerecord/materialized/module_api.rb', line 138 def warn_if_in_process_dispatcher!(logger: default_logger) return unless configuration.active_job_available? return if configuration.refresh_dispatcher == :active_job logger&.warn(IN_PROCESS_DISPATCHER_WARNING) end |