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
@@ -12,6 +12,9 @@ class CreateArMaterializedViewMetadata < ActiveRecord::Migration[<%= ActiveRecor
12
12
  t.integer :refresh_duration_ms
13
13
  t.text :last_error
14
14
  t.text :maintenance_payload
15
+ t.datetime :last_reconciled_at
16
+ t.integer :reconciled_partition_count
17
+ t.integer :fresh_set_generation, null: false, default: 0
15
18
 
16
19
  t.timestamps
17
20
  end
@@ -21,10 +24,21 @@ class CreateArMaterializedViewMetadata < ActiveRecord::Migration[<%= ActiveRecor
21
24
  create_table :ar_materialized_view_partitions do |t|
22
25
  t.string :view_name, null: false
23
26
  t.string :partition_key, null: false
27
+ t.integer :generation, null: false, default: 0
24
28
 
25
29
  t.datetime :created_at, null: false
26
30
  end
27
31
 
28
32
  add_index :ar_materialized_view_partitions, %i[view_name partition_key], unique: true
33
+
34
+ create_table :ar_materialized_view_source_watermarks do |t|
35
+ t.string :view_name, null: false
36
+ t.string :partition_key, null: false
37
+ t.bigint :source_ts, null: false
38
+
39
+ t.timestamps
40
+ end
41
+
42
+ add_index :ar_materialized_view_source_watermarks, %i[view_name partition_key], unique: true
29
43
  end
30
44
  end
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "rails/generators"
@@ -7,24 +6,20 @@ require "rails/generators/migration"
7
6
  module ActiverecordMaterialized
8
7
  # `rails generate activerecord_materialized:install` — installs the metadata-table migration.
9
8
  class InstallGenerator < ::Rails::Generators::Base
10
- extend T::Sig
11
9
  include Rails::Generators::Migration
12
10
 
13
11
  source_root File.expand_path("install", __dir__)
14
12
 
15
- sig { params(dirname: String).returns(String) }
16
13
  def self.next_migration_number(dirname)
17
- next_migration_number = T.unsafe(self).current_migration_number(dirname) + 1
14
+ next_migration_number = current_migration_number(dirname) + 1
18
15
  ::ActiveRecord::Migration.next_migration_number(next_migration_number)
19
16
  end
20
17
 
21
- sig { void }
22
18
  def copy_migration
23
19
  migration_template "create_ar_materialized_view_metadata.rb.erb",
24
20
  "db/migrate/create_ar_materialized_view_metadata.rb"
25
21
  end
26
22
 
27
- sig { void }
28
23
  def show_readme
29
24
  readme "README" if behavior == :invoke
30
25
  end
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "rails/generators"
@@ -12,17 +11,14 @@ module ActiverecordMaterialized
12
11
  #
13
12
  # Run after the view's `materialized_from` is defined, then `db:migrate`.
14
13
  class MigrationGenerator < ::Rails::Generators::NamedBase
15
- extend T::Sig
16
14
  include ::Rails::Generators::Migration
17
15
 
18
16
  source_root File.expand_path("templates", __dir__)
19
17
 
20
- sig { params(dirname: String).returns(String) }
21
18
  def self.next_migration_number(dirname)
22
- ::ActiveRecord::Migration.next_migration_number(T.unsafe(self).current_migration_number(dirname) + 1)
19
+ ::ActiveRecord::Migration.next_migration_number(current_migration_number(dirname) + 1)
23
20
  end
24
21
 
25
- sig { void }
26
22
  def create_migration_file
27
23
  migration_template "materialized_view_migration.rb.erb",
28
24
  File.join("db", "migrate", "create_#{builder.table_name}.rb")
@@ -30,15 +26,13 @@ module ActiverecordMaterialized
30
26
 
31
27
  private
32
28
 
33
- sig { returns(::ActiveRecord::Materialized::MigrationBuilder) }
34
29
  def builder
35
- @builder ||= T.let(build_builder, T.nilable(::ActiveRecord::Materialized::MigrationBuilder))
30
+ @builder ||= build_builder
36
31
  end
37
32
 
38
33
  # Resolve via the registry (after eager-load) rather than constantize.
39
- sig { returns(::ActiveRecord::Materialized::MigrationBuilder) }
40
34
  def build_builder
41
- T.unsafe(::Rails).application.eager_load!
35
+ ::Rails.application.eager_load!
42
36
  view_class = ::ActiveRecord::Materialized::Registry.for_class_name(class_name)
43
37
  if view_class.nil?
44
38
  Kernel.raise ::Thor::Error,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module ActiverecordMaterialized
7
+ # Generates a migration that installs write-outbox triggers on a dependency table, so writes that
8
+ # bypass ActiveRecord (raw SQL, another service, a backfill) are still captured and can be relayed
9
+ # into view maintenance via +ActiveRecord::Materialized.drain_write_outbox!+:
10
+ #
11
+ # bin/rails generate activerecord_materialized:outbox line_items category region
12
+ #
13
+ # The trailing arguments are the GROUP BY key columns to capture (empty for an un-grouped view).
14
+ # The migration emits the correct trigger DDL for the connection's adapter at migrate time.
15
+ # See +docs/out-of-band-writes.md+.
16
+ class OutboxGenerator < ::Rails::Generators::NamedBase
17
+ include ::Rails::Generators::Migration
18
+
19
+ source_root File.expand_path("templates", __dir__)
20
+
21
+ argument :columns, type: :array, default: [],
22
+ banner: "key_column key_column",
23
+ desc: "GROUP BY key columns to capture (the partition keys)"
24
+
25
+ def self.next_migration_number(dirname)
26
+ ::ActiveRecord::Migration.next_migration_number(current_migration_number(dirname) + 1)
27
+ end
28
+
29
+ def create_migration_file
30
+ # +file_name+, not NamedBase#table_name: NAME is a raw dependency table, and table_name would
31
+ # inflect it (pluralize a singular/irregular table — person -> people), targeting the wrong table.
32
+ migration_template "write_outbox_migration.rb.erb",
33
+ File.join("db", "migrate", "install_write_outbox_triggers_on_#{file_name}.rb")
34
+ end
35
+ end
36
+ end
@@ -4,7 +4,11 @@ class <%= builder.migration_class_name %> < ActiveRecord::Migration[<%= builder.
4
4
  def change
5
5
  create_table :<%= builder.table_name %> do |t|
6
6
  <% builder.column_definitions.each do |column| -%>
7
- t.<%= column.type %> :<%= column.name %>
7
+ t.<%= column.type %> :<%= column.name %><%= column.migration_arguments %>
8
+ <% end -%>
9
+ <% if (index = builder.index_definition) -%>
10
+ # Incremental maintenance filters and deletes by the GROUP BY key; this index keeps it partition-local.
11
+ t.index <%= index.columns.map(&:to_sym).inspect %><%= ", unique: true" if index.unique %>
8
12
  <% end -%>
9
13
  end
10
14
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Installs activerecord-materialized write-outbox triggers on <%= table_name %>. The triggers append
4
+ # every INSERT/UPDATE/DELETE — including raw SQL and writes from other services — to the outbox table,
5
+ # so out-of-band writes still reach view maintenance. Relay them with
6
+ # ActiveRecord::Materialized.drain_write_outbox! from a poller, cron, or background job.
7
+ #
8
+ # The correct trigger DDL for this database's adapter (SQLite/MySQL/Postgres) is emitted at migrate
9
+ # time; the outbox table is provisioned lazily on first install. See docs/out-of-band-writes.md.
10
+ class InstallWriteOutboxTriggersOn<%= file_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
11
+ def up
12
+ ActiveRecord::Materialized::WriteOutbox.install_triggers!(
13
+ :<%= file_name %>,
14
+ key_columns: %i[<%= columns.join(" ") %>]
15
+ )
16
+ end
17
+
18
+ def down
19
+ ActiveRecord::Materialized::WriteOutbox.uninstall_triggers!(:<%= file_name %>)
20
+ end
21
+ end
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "rails/generators"
@@ -6,11 +5,8 @@ require "rails/generators"
6
5
  module ActiverecordMaterialized
7
6
  # `rails generate activerecord_materialized:view NAME` — scaffolds a materialized view class.
8
7
  class ViewGenerator < ::Rails::Generators::NamedBase
9
- extend T::Sig
10
-
11
8
  source_root File.expand_path("templates", __dir__)
12
9
 
13
- sig { void }
14
10
  def create_model
15
11
  template "materialized_view.rb.erb", File.join("app/models", class_path, "#{file_name}.rb")
16
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-materialized
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Avrukin
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '8.0'
54
- - !ruby/object:Gem::Dependency
55
- name: sorbet-runtime
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0.5'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0.5'
68
54
  description: |
69
55
  Provides transparent materialized view semantics for Rails applications
70
56
  on databases that do not support native materialized views. Precomputes
@@ -82,26 +68,43 @@ files:
82
68
  - lib/activerecord/materialized/aggregate_analysis.rb
83
69
  - lib/activerecord/materialized/async_refresher.rb
84
70
  - lib/activerecord/materialized/cache_table_schema.rb
71
+ - lib/activerecord/materialized/change_source.rb
85
72
  - lib/activerecord/materialized/cold_read.rb
73
+ - lib/activerecord/materialized/column_type_inference.rb
74
+ - lib/activerecord/materialized/configurable_table_name.rb
86
75
  - lib/activerecord/materialized/configuration.rb
76
+ - lib/activerecord/materialized/connection_routing.rb
77
+ - lib/activerecord/materialized/data_verification_result.rb
78
+ - lib/activerecord/materialized/data_verifier.rb
79
+ - lib/activerecord/materialized/debezium_envelope.rb
87
80
  - lib/activerecord/materialized/delta_maintainer.rb
88
81
  - lib/activerecord/materialized/dependency_registry.rb
89
82
  - lib/activerecord/materialized/dependency_trackable.rb
90
83
  - lib/activerecord/materialized/incremental_maintainer.rb
84
+ - lib/activerecord/materialized/incremental_refresh.rb
85
+ - lib/activerecord/materialized/instrumentation.rb
91
86
  - lib/activerecord/materialized/maintenance_delta.rb
92
87
  - lib/activerecord/materialized/maintenance_delta_builder.rb
93
88
  - lib/activerecord/materialized/maintenance_store.rb
94
89
  - lib/activerecord/materialized/metadata.rb
95
90
  - lib/activerecord/materialized/metadata/maintenance_payload.rb
91
+ - lib/activerecord/materialized/metadata/reconciliation.rb
96
92
  - lib/activerecord/materialized/metadata/schema.rb
97
93
  - lib/activerecord/materialized/metadata/timestamps.rb
98
94
  - lib/activerecord/materialized/metadata_record.rb
99
95
  - lib/activerecord/materialized/migration_builder.rb
100
96
  - lib/activerecord/materialized/module_api.rb
97
+ - lib/activerecord/materialized/partition_filter.rb
98
+ - lib/activerecord/materialized/partition_keyed_store.rb
101
99
  - lib/activerecord/materialized/partition_record.rb
100
+ - lib/activerecord/materialized/partition_snapshot.rb
102
101
  - lib/activerecord/materialized/partition_state.rb
103
102
  - lib/activerecord/materialized/query_expressions.rb
104
103
  - lib/activerecord/materialized/railtie.rb
104
+ - lib/activerecord/materialized/read_router.rb
105
+ - lib/activerecord/materialized/reconcile_job.rb
106
+ - lib/activerecord/materialized/reconcile_result.rb
107
+ - lib/activerecord/materialized/reconciler.rb
105
108
  - lib/activerecord/materialized/refresh_callbacks.rb
106
109
  - lib/activerecord/materialized/refresh_job.rb
107
110
  - lib/activerecord/materialized/refresh_result.rb
@@ -110,27 +113,33 @@ files:
110
113
  - lib/activerecord/materialized/registry.rb
111
114
  - lib/activerecord/materialized/relation_cache_writer.rb
112
115
  - lib/activerecord/materialized/schema_verifier.rb
116
+ - lib/activerecord/materialized/source_watermark.rb
117
+ - lib/activerecord/materialized/source_watermark_record.rb
113
118
  - lib/activerecord/materialized/summary_delta.rb
114
119
  - lib/activerecord/materialized/summary_delta_builder.rb
115
120
  - lib/activerecord/materialized/table_model_registry.rb
121
+ - lib/activerecord/materialized/table_swap.rb
116
122
  - lib/activerecord/materialized/tasks.rb
117
- - lib/activerecord/materialized/type_reexports.rb
118
123
  - lib/activerecord/materialized/version.rb
119
124
  - lib/activerecord/materialized/view.rb
120
- - lib/activerecord/materialized/view_class.rb
121
125
  - lib/activerecord/materialized/view_configuration_class_methods.rb
122
126
  - lib/activerecord/materialized/view_definition.rb
123
127
  - lib/activerecord/materialized/view_incremental_class_methods.rb
128
+ - lib/activerecord/materialized/view_loader.rb
124
129
  - lib/activerecord/materialized/view_query_access_class_methods.rb
125
130
  - lib/activerecord/materialized/view_refresh_policy_class_methods.rb
126
131
  - lib/activerecord/materialized/write_change.rb
127
- - lib/activerecord_materialized_types.rb
132
+ - lib/activerecord/materialized/write_maintenance.rb
133
+ - lib/activerecord/materialized/write_outbox.rb
134
+ - lib/activerecord/materialized/write_outbox_record.rb
128
135
  - lib/generators/activerecord_materialized/install/templates/README
129
136
  - lib/generators/activerecord_materialized/install/templates/create_ar_materialized_view_metadata.rb.erb
130
137
  - lib/generators/activerecord_materialized/install_generator.rb
131
138
  - lib/generators/activerecord_materialized/migration_generator.rb
139
+ - lib/generators/activerecord_materialized/outbox_generator.rb
132
140
  - lib/generators/activerecord_materialized/templates/materialized_view.rb.erb
133
141
  - lib/generators/activerecord_materialized/templates/materialized_view_migration.rb.erb
142
+ - lib/generators/activerecord_materialized/templates/write_outbox_migration.rb.erb
134
143
  - lib/generators/activerecord_materialized/view_generator.rb
135
144
  homepage: https://github.com/mavrukin/activerecord-materialized
136
145
  licenses:
@@ -1,14 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module ActiveRecord
5
- module Materialized
6
- DebounceInterval = T.type_alias { ::ActiveRecordMaterializedTypes::DebounceInterval }
7
- StalenessDuration = T.type_alias { ::ActiveRecordMaterializedTypes::StalenessDuration }
8
- SourceDefinition = T.type_alias { ::ActiveRecordMaterializedTypes::SourceDefinition }
9
- RefreshMode = T.type_alias { ::ActiveRecordMaterializedTypes::RefreshMode }
10
- RefreshCallbackName = T.type_alias { ::ActiveRecordMaterializedTypes::RefreshCallbackName }
11
- Connection = T.type_alias { ::ActiveRecordMaterializedTypes::Connection }
12
- Timestamp = T.type_alias { ::ActiveRecordMaterializedTypes::Timestamp }
13
- end
14
- end
@@ -1,8 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module ActiveRecord
5
- module Materialized
6
- ViewClass = T.type_alias { T.class_of(View) }
7
- end
8
- end
@@ -1,18 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module ActiveRecordMaterializedTypes
5
- DebounceInterval = T.type_alias { T.any(Integer, Float, ::ActiveSupport::Duration) }
6
- StalenessDuration = T.type_alias { T.any(Integer, ::ActiveSupport::Duration) }
7
- SourceDefinition = T.type_alias do
8
- T.any(
9
- ::ActiveRecord::Relation,
10
- Proc,
11
- T.nilable(T.proc.returns(::ActiveRecord::Relation))
12
- )
13
- end
14
- RefreshMode = T.type_alias { Symbol }
15
- RefreshCallbackName = T.type_alias { T.any(Symbol, Proc) }
16
- Connection = T.type_alias { ::ActiveRecord::ConnectionAdapters::AbstractAdapter }
17
- Timestamp = T.type_alias { T.any(::Time, ::ActiveSupport::TimeWithZone) }
18
- end