mat_views 0.1.2
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +15 -0
- data/app/assets/stylesheets/mat_views/application.css +15 -0
- data/app/jobs/mat_views/application_job.rb +39 -0
- data/app/jobs/mat_views/create_view_job.rb +188 -0
- data/app/jobs/mat_views/delete_view_job.rb +196 -0
- data/app/jobs/mat_views/refresh_view_job.rb +215 -0
- data/app/models/mat_views/application_record.rb +34 -0
- data/app/models/mat_views/mat_view_definition.rb +98 -0
- data/app/models/mat_views/mat_view_run.rb +89 -0
- data/config/routes.rb +12 -0
- data/lib/generators/mat_views/install/install_generator.rb +86 -0
- data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +29 -0
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +32 -0
- data/lib/generators/mat_views/install/templates/mat_views_initializer.rb +23 -0
- data/lib/mat_views/configuration.rb +49 -0
- data/lib/mat_views/engine.rb +34 -0
- data/lib/mat_views/jobs/adapter.rb +78 -0
- data/lib/mat_views/service_response.rb +60 -0
- data/lib/mat_views/services/base_service.rb +308 -0
- data/lib/mat_views/services/concurrent_refresh.rb +177 -0
- data/lib/mat_views/services/create_view.rb +156 -0
- data/lib/mat_views/services/delete_view.rb +160 -0
- data/lib/mat_views/services/regular_refresh.rb +146 -0
- data/lib/mat_views/services/swap_refresh.rb +221 -0
- data/lib/mat_views/version.rb +21 -0
- data/lib/mat_views.rb +57 -0
- data/lib/tasks/helpers.rb +185 -0
- data/lib/tasks/mat_views_tasks.rake +145 -0
- metadata +95 -0
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
##
|
9
|
+
# Top-level namespace for the mat_views engine.
|
10
|
+
module MatViews
|
11
|
+
##
|
12
|
+
# ActiveJob that handles `REFRESH MATERIALIZED VIEW` for a given
|
13
|
+
# {MatViews::MatViewDefinition}.
|
14
|
+
#
|
15
|
+
# The job mirrors {MatViews::CreateViewJob}'s lifecycle:
|
16
|
+
# it measures duration and persists state in {MatViews::MatViewRun}.
|
17
|
+
#
|
18
|
+
# The actual refresh implementation is delegated based on
|
19
|
+
# `definition.refresh_strategy`:
|
20
|
+
#
|
21
|
+
# - `"concurrent"` → {MatViews::Services::ConcurrentRefresh}
|
22
|
+
# - `"swap"` → {MatViews::Services::SwapRefresh}
|
23
|
+
# - otherwise → {MatViews::Services::RegularRefresh}
|
24
|
+
#
|
25
|
+
# Row count reporting can be controlled via `row_count_strategy`:
|
26
|
+
# - `:estimated` (default) — fast, approximate via reltuples
|
27
|
+
# - `:exact` — accurate `COUNT(*)`
|
28
|
+
# - `nil` — skip counting
|
29
|
+
#
|
30
|
+
# @see MatViews::MatViewDefinition
|
31
|
+
# @see MatViews::MatViewRun
|
32
|
+
# @see MatViews::Services::RegularRefresh
|
33
|
+
# @see MatViews::Services::ConcurrentRefresh
|
34
|
+
# @see MatViews::Services::SwapRefresh
|
35
|
+
#
|
36
|
+
# @example Enqueue a refresh with exact row count
|
37
|
+
# MatViews::RefreshViewJob.perform_later(definition.id, :exact)
|
38
|
+
#
|
39
|
+
# @example Enqueue using keyword-hash form
|
40
|
+
# MatViews::RefreshViewJob.perform_later(definition.id, row_count_strategy: :estimated)
|
41
|
+
#
|
42
|
+
class RefreshViewJob < ::ActiveJob::Base
|
43
|
+
##
|
44
|
+
# Queue name for the job.
|
45
|
+
#
|
46
|
+
# Uses `MatViews.configuration.job_queue` when configured, otherwise `:default`.
|
47
|
+
#
|
48
|
+
queue_as { MatViews.configuration.job_queue || :default }
|
49
|
+
|
50
|
+
##
|
51
|
+
# Perform the job for the given materialized view definition.
|
52
|
+
#
|
53
|
+
# Accepts either a symbol/string (`:estimated`, `:exact`) or a hash
|
54
|
+
# (`{ row_count_strategy: :exact }`) for `strategy_arg`.
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
#
|
58
|
+
# @param definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
59
|
+
# @param strategy_arg [Symbol, String, Hash, nil] Row count strategy override.
|
60
|
+
# When a Hash, looks for `:row_count_strategy` / `"row_count_strategy"`.
|
61
|
+
#
|
62
|
+
# @return [Hash] Serialized {MatViews::ServiceResponse#to_h}:
|
63
|
+
# - `:status` [Symbol]
|
64
|
+
# - `:payload` [Hash]
|
65
|
+
# - `:error` [String, nil]
|
66
|
+
# - `:duration_ms` [Integer]
|
67
|
+
# - `:meta` [Hash]
|
68
|
+
#
|
69
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
70
|
+
#
|
71
|
+
def perform(definition_id, strategy_arg = nil)
|
72
|
+
row_count_strategy = normalize_strategy(strategy_arg)
|
73
|
+
definition = MatViews::MatViewDefinition.find(definition_id)
|
74
|
+
run = start_run(definition)
|
75
|
+
|
76
|
+
response, duration_ms = execute(definition, row_count_strategy: row_count_strategy)
|
77
|
+
finalize_run!(run, response, duration_ms)
|
78
|
+
response.to_h
|
79
|
+
rescue StandardError => e
|
80
|
+
fail_run!(run, e) if run
|
81
|
+
raise e
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
##
|
87
|
+
# Normalize the strategy argument into a symbol or default.
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
#
|
91
|
+
# @param arg [Symbol, String, Hash, nil]
|
92
|
+
# @return [Symbol] One of `:estimated`, `:exact`, or `:estimated` by default.
|
93
|
+
#
|
94
|
+
def normalize_strategy(arg)
|
95
|
+
case arg
|
96
|
+
when Hash
|
97
|
+
(arg[:row_count_strategy] || arg['row_count_strategy'] || :estimated).to_sym
|
98
|
+
when String, Symbol
|
99
|
+
arg.to_sym
|
100
|
+
else
|
101
|
+
:estimated
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Execute the appropriate refresh service and measure duration.
|
107
|
+
#
|
108
|
+
# @api private
|
109
|
+
#
|
110
|
+
# @param definition [MatViews::MatViewDefinition]
|
111
|
+
# @param row_count_strategy [Symbol, nil]
|
112
|
+
# @return [Array(MatViews::ServiceResponse, Integer)] response and elapsed ms.
|
113
|
+
#
|
114
|
+
def execute(definition, row_count_strategy:)
|
115
|
+
started = monotime
|
116
|
+
response = service(definition).new(definition, row_count_strategy: row_count_strategy).run
|
117
|
+
[response, elapsed_ms(started)]
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Select the refresh service class based on the definition's strategy.
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
#
|
125
|
+
# @param definition [MatViews::MatViewDefinition]
|
126
|
+
# @return [Class] One of the refresh service classes.
|
127
|
+
#
|
128
|
+
def service(definition)
|
129
|
+
case definition.refresh_strategy
|
130
|
+
when 'concurrent'
|
131
|
+
MatViews::Services::ConcurrentRefresh
|
132
|
+
when 'swap'
|
133
|
+
MatViews::Services::SwapRefresh
|
134
|
+
else
|
135
|
+
MatViews::Services::RegularRefresh
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Begin a {MatViews::MatViewRun} row for lifecycle tracking.
|
141
|
+
#
|
142
|
+
# @api private
|
143
|
+
#
|
144
|
+
# @param definition [MatViews::MatViewDefinition]
|
145
|
+
# @return [MatViews::MatViewRun]
|
146
|
+
#
|
147
|
+
def start_run(definition)
|
148
|
+
MatViews::MatViewRun.create!(
|
149
|
+
mat_view_definition: definition,
|
150
|
+
status: :running,
|
151
|
+
started_at: Time.current,
|
152
|
+
operation: :refresh
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Finalize the run with success/failure, timing, and meta from the response.
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
#
|
161
|
+
# @param run [MatViews::MatViewRun]
|
162
|
+
# @param response [MatViews::ServiceResponse]
|
163
|
+
# @param duration_ms [Integer]
|
164
|
+
# @return [void]
|
165
|
+
#
|
166
|
+
def finalize_run!(run, response, duration_ms)
|
167
|
+
base_attrs = {
|
168
|
+
finished_at: Time.current,
|
169
|
+
duration_ms: duration_ms,
|
170
|
+
meta: response.payload || {}
|
171
|
+
}
|
172
|
+
|
173
|
+
if response.success?
|
174
|
+
run.update!(base_attrs.merge(status: :success, error: nil))
|
175
|
+
else
|
176
|
+
run.update!(base_attrs.merge(status: :failed, error: response.error.to_s.presence))
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Mark the run failed due to an exception.
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
#
|
185
|
+
# @param run [MatViews::MatViewRun]
|
186
|
+
# @param exception [Exception]
|
187
|
+
# @return [void]
|
188
|
+
#
|
189
|
+
def fail_run!(run, exception)
|
190
|
+
run.update!(
|
191
|
+
finished_at: Time.current,
|
192
|
+
duration_ms: run.duration_ms || 0,
|
193
|
+
error: "#{exception.class}: #{exception.message}",
|
194
|
+
status: :failed
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Monotonic clock getter (for elapsed-time measurement).
|
200
|
+
#
|
201
|
+
# @api private
|
202
|
+
# @return [Float] seconds
|
203
|
+
#
|
204
|
+
def monotime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
205
|
+
|
206
|
+
##
|
207
|
+
# Convert monotonic start time to elapsed milliseconds.
|
208
|
+
#
|
209
|
+
# @api private
|
210
|
+
# @param start [Float]
|
211
|
+
# @return [Integer] elapsed ms
|
212
|
+
#
|
213
|
+
def elapsed_ms(start) = ((monotime - start) * 1000).round
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
##
|
9
|
+
# Top-level namespace for the mat_views engine.
|
10
|
+
module MatViews
|
11
|
+
##
|
12
|
+
# Base model class for all ActiveRecord models in the mat_views engine.
|
13
|
+
#
|
14
|
+
# Inherits from {ActiveRecord::Base} and marks itself as an abstract class.
|
15
|
+
# Other engine models should subclass this rather than inheriting directly
|
16
|
+
# from {ActiveRecord::Base}, so that shared behavior or configuration can be
|
17
|
+
# applied in one place.
|
18
|
+
#
|
19
|
+
# @abstract
|
20
|
+
#
|
21
|
+
# @example Define a new model under mat_views
|
22
|
+
# class MatViews::MatViewDefinition < MatViews::ApplicationRecord
|
23
|
+
# self.table_name = "mat_view_definitions"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
class ApplicationRecord < ActiveRecord::Base
|
27
|
+
##
|
28
|
+
# Marks this record class as abstract, so it won’t be persisted to a table.
|
29
|
+
#
|
30
|
+
# @return [void]
|
31
|
+
#
|
32
|
+
self.abstract_class = true
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
##
|
9
|
+
# Top-level namespace for the mat_views engine.
|
10
|
+
module MatViews
|
11
|
+
##
|
12
|
+
# Represents a **materialized view definition** managed by the engine.
|
13
|
+
#
|
14
|
+
# A definition stores the canonical name and SQL for a materialized view and
|
15
|
+
# drives lifecycle operations (create, refresh, delete) via background jobs
|
16
|
+
# and services. It also tracks operational history through associated
|
17
|
+
# run models.
|
18
|
+
#
|
19
|
+
# Validations ensure a sane PostgreSQL identifier for `name` and that `sql`
|
20
|
+
# begins with `SELECT` (case-insensitive).
|
21
|
+
#
|
22
|
+
# @see MatViews::CreateViewJob
|
23
|
+
# @see MatViews::RefreshViewJob
|
24
|
+
# @see MatViews::DeleteViewJob
|
25
|
+
# @see MatViews::Services::CreateView
|
26
|
+
# @see MatViews::Services::RegularRefresh
|
27
|
+
# @see MatViews::Services::ConcurrentRefresh
|
28
|
+
# @see MatViews::Services::SwapRefresh
|
29
|
+
#
|
30
|
+
# @example Creating a definition
|
31
|
+
# defn = MatViews::MatViewDefinition.create!(
|
32
|
+
# name: "mv_user_accounts",
|
33
|
+
# sql: "SELECT users.id, accounts.id AS account_id FROM users JOIN accounts ON ..."
|
34
|
+
# )
|
35
|
+
#
|
36
|
+
# @example Enqueue a refresh
|
37
|
+
# MatViews::RefreshViewJob.perform_later(defn.id, :estimated)
|
38
|
+
#
|
39
|
+
class MatViewDefinition < ApplicationRecord
|
40
|
+
##
|
41
|
+
# Underlying database table name.
|
42
|
+
self.table_name = 'mat_view_definitions'
|
43
|
+
|
44
|
+
# ────────────────────────────────────────────────────────────────
|
45
|
+
# Associations
|
46
|
+
# ────────────────────────────────────────────────────────────────
|
47
|
+
|
48
|
+
##
|
49
|
+
# Historical create runs linked to this definition.
|
50
|
+
#
|
51
|
+
# @return [ActiveRecord::Relation<MatViews::MatViewRun>]
|
52
|
+
#
|
53
|
+
has_many :mat_view_runs,
|
54
|
+
dependent: :destroy,
|
55
|
+
class_name: 'MatViews::MatViewRun'
|
56
|
+
|
57
|
+
# ────────────────────────────────────────────────────────────────
|
58
|
+
# Validations
|
59
|
+
# ────────────────────────────────────────────────────────────────
|
60
|
+
|
61
|
+
##
|
62
|
+
# @!attribute name
|
63
|
+
# @return [String] PostgreSQL identifier for the materialized view.
|
64
|
+
#
|
65
|
+
validates :name,
|
66
|
+
presence: true,
|
67
|
+
uniqueness: true,
|
68
|
+
format: { with: /\A[a-zA-Z_][a-zA-Z0-9_]*\z/ }
|
69
|
+
|
70
|
+
##
|
71
|
+
# @!attribute sql
|
72
|
+
# @return [String] SELECT statement used to materialize the view.
|
73
|
+
#
|
74
|
+
validates :sql,
|
75
|
+
presence: true,
|
76
|
+
format: { with: /\A\s*SELECT/i, message: 'must begin with a SELECT' }
|
77
|
+
|
78
|
+
# ────────────────────────────────────────────────────────────────
|
79
|
+
# Enums / configuration
|
80
|
+
# ────────────────────────────────────────────────────────────────
|
81
|
+
|
82
|
+
##
|
83
|
+
# Refresh strategy that governs which service is used by {RefreshViewJob}.
|
84
|
+
#
|
85
|
+
# - `:regular` → {MatViews::Services::RegularRefresh}
|
86
|
+
# - `:concurrent` → {MatViews::Services::ConcurrentRefresh}
|
87
|
+
# - `:swap` → {MatViews::Services::SwapRefresh}
|
88
|
+
#
|
89
|
+
# @!attribute [rw] refresh_strategy
|
90
|
+
# @return [String] one of `"regular"`, `"concurrent"`, `"swap"`
|
91
|
+
#
|
92
|
+
enum :refresh_strategy, { regular: 0, concurrent: 1, swap: 2 }
|
93
|
+
|
94
|
+
def last_run
|
95
|
+
mat_view_runs.order(created_at: :desc).first
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
##
|
9
|
+
# Top-level namespace for the mat_views engine.
|
10
|
+
module MatViews
|
11
|
+
##
|
12
|
+
# ActiveRecord model that tracks the lifecycle of *runs* for
|
13
|
+
# materialized views.
|
14
|
+
#
|
15
|
+
# Each record corresponds to a single attempt to mutate a materialized view
|
16
|
+
# from a {MatViews::MatViewDefinition}, storing its status, timing, and
|
17
|
+
# any associated error or metadata.
|
18
|
+
#
|
19
|
+
# This model provides an auditable history of view provisioning across
|
20
|
+
# environments, useful for telemetry, dashboards, and debugging.
|
21
|
+
#
|
22
|
+
# @see MatViews::MatViewDefinition
|
23
|
+
# @see MatViews::CreateViewJob
|
24
|
+
#
|
25
|
+
# @example Query recent successful runs
|
26
|
+
# MatViews::MatViewRun.status_success.order(created_at: :desc).limit(10)
|
27
|
+
#
|
28
|
+
# @example Check if a definition has any failed runs
|
29
|
+
# definition.mat_view_runs.status_failed.any?
|
30
|
+
#
|
31
|
+
class MatViewRun < ApplicationRecord
|
32
|
+
##
|
33
|
+
# Underlying database table name.
|
34
|
+
self.table_name = 'mat_view_runs'
|
35
|
+
|
36
|
+
##
|
37
|
+
# The definition this run belongs to.
|
38
|
+
#
|
39
|
+
# @return [MatViews::MatViewDefinition]
|
40
|
+
#
|
41
|
+
belongs_to :mat_view_definition, class_name: 'MatViews::MatViewDefinition'
|
42
|
+
|
43
|
+
##
|
44
|
+
# Status of the create run.
|
45
|
+
#
|
46
|
+
# @!attribute [r] status
|
47
|
+
# @return [Symbol] One of:
|
48
|
+
# - `:pending` — queued but not yet started
|
49
|
+
# - `:running` — currently executing
|
50
|
+
# - `:success` — completed successfully
|
51
|
+
# - `:failed` — encountered an error
|
52
|
+
#
|
53
|
+
enum :status, {
|
54
|
+
pending: 0,
|
55
|
+
running: 1,
|
56
|
+
success: 2,
|
57
|
+
failed: 3
|
58
|
+
}, prefix: :status
|
59
|
+
|
60
|
+
# Operation type of the run.
|
61
|
+
#
|
62
|
+
# @!attribute [r] operation
|
63
|
+
# @return [Symbol] One of:
|
64
|
+
# - `:create` — initial creation of the materialized view
|
65
|
+
# - `:refresh` — refreshing an existing view
|
66
|
+
# - `:drop` — dropping the materialized view
|
67
|
+
enum :operation, {
|
68
|
+
create: 0,
|
69
|
+
refresh: 1,
|
70
|
+
drop: 2
|
71
|
+
}, prefix: :operation
|
72
|
+
|
73
|
+
##
|
74
|
+
# Validations
|
75
|
+
#
|
76
|
+
# Ensures that a status is always present.
|
77
|
+
validates :status, presence: true
|
78
|
+
|
79
|
+
##
|
80
|
+
# Scopes
|
81
|
+
scope :create_runs, -> { where(operation: :create) }
|
82
|
+
scope :refresh_runs, -> { where(operation: :refresh) }
|
83
|
+
scope :drop_runs, -> { where(operation: :drop) }
|
84
|
+
|
85
|
+
def row_count
|
86
|
+
meta['row_count']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
MatViews::Engine.routes.draw do
|
9
|
+
# Routes for MatViews can be defined here.
|
10
|
+
# For example, you can add a root route or other resources as needed.
|
11
|
+
# root 'mat_views#home'
|
12
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
require 'rails/generators'
|
9
|
+
require 'rails/generators/migration'
|
10
|
+
require 'rails/generators/active_record'
|
11
|
+
|
12
|
+
##
|
13
|
+
# Top-level namespace for the mat_views engine.
|
14
|
+
module MatViews
|
15
|
+
##
|
16
|
+
# Namespace for Rails generators shipped with the mat_views engine.
|
17
|
+
module Generators
|
18
|
+
##
|
19
|
+
# Rails generator that installs MatViews into a host application by:
|
20
|
+
#
|
21
|
+
# 1. Copying migrations for definitions and run-tracking tables.
|
22
|
+
# 2. Creating an initializer at `config/initializers/mat_views.rb`.
|
23
|
+
# 3. Printing a success message with next steps.
|
24
|
+
#
|
25
|
+
# @example Run the installer
|
26
|
+
# bin/rails g mat_views:install
|
27
|
+
#
|
28
|
+
# @see MatViews::MatViewDefinition
|
29
|
+
# @see MatViews::MatViewRun
|
30
|
+
#
|
31
|
+
class InstallGenerator < Rails::Generators::Base
|
32
|
+
include Rails::Generators::Migration
|
33
|
+
|
34
|
+
##
|
35
|
+
# Directory containing template files for the generator.
|
36
|
+
#
|
37
|
+
# @return [String] absolute path to templates dir
|
38
|
+
#
|
39
|
+
source_root File.expand_path('templates', __dir__)
|
40
|
+
|
41
|
+
##
|
42
|
+
# Short description shown in `rails g --help`.
|
43
|
+
desc 'Installs MatViews: copies migrations and initializer.'
|
44
|
+
|
45
|
+
##
|
46
|
+
# Copies all required migrations into the host app.
|
47
|
+
#
|
48
|
+
# @return [void]
|
49
|
+
#
|
50
|
+
def copy_migrations
|
51
|
+
migration_template 'create_mat_view_definitions.rb', 'db/migrate/create_mat_view_definitions.rb'
|
52
|
+
migration_template 'create_mat_view_runs.rb', 'db/migrate/create_mat_view_runs.rb'
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Creates the engine initializer in the host app.
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
#
|
60
|
+
def create_initializer
|
61
|
+
copy_file 'mat_views_initializer.rb', 'config/initializers/mat_views.rb'
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Prints a success message after installation.
|
66
|
+
#
|
67
|
+
# @return [void]
|
68
|
+
#
|
69
|
+
def show_success_message
|
70
|
+
say "\n✅ MatViews installed! Don't forget to run: rails db:migrate\n", :green
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Computes the next migration number for copied migrations.
|
75
|
+
#
|
76
|
+
# Required by Rails to generate timestamped migration filenames.
|
77
|
+
#
|
78
|
+
# @param path [String] destination path for migrations
|
79
|
+
# @return [String] the next migration number (timestamp)
|
80
|
+
#
|
81
|
+
def self.next_migration_number(path)
|
82
|
+
ActiveRecord::Generators::Base.next_migration_number(path)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
# This migration creates the mat_view_definitions table, which stores definitions for materialized views.
|
9
|
+
# It includes fields for the view name, SQL definition, refresh strategy, schedule, unique index columns,
|
10
|
+
# dependencies, last refreshed timestamp, and timestamps for creation and updates.
|
11
|
+
class CreateMatViewDefinitions < ActiveRecord::Migration[7.1]
|
12
|
+
def change
|
13
|
+
create_table :mat_view_definitions do |t|
|
14
|
+
t.string :name, null: false, comment: 'The name of the materialized view'
|
15
|
+
t.text :sql, null: false, comment: 'The SQL query defining the materialized view'
|
16
|
+
# refresh_strategy can be
|
17
|
+
# regular: 0 - Default strategy, in-place refresh.
|
18
|
+
# concurrent: 1 - Concurrent refresh, requires at least one unique index.
|
19
|
+
# swap: 2 - Swap the materialized view with a new one, uses more memory.
|
20
|
+
t.integer :refresh_strategy, default: 0, null: false,
|
21
|
+
comment: 'Strategy for refreshing the materialized view. Options: regular, concurrent, swap'
|
22
|
+
t.string :schedule_cron, comment: 'Cron schedule for automatic refresh of the materialized view'
|
23
|
+
t.jsonb :unique_index_columns, default: [], comment: 'Columns used for unique indexing, if any'
|
24
|
+
t.jsonb :dependencies, default: [],
|
25
|
+
comment: 'Dependencies of the materialized view, such as other views or tables'
|
26
|
+
t.timestamps
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
# This migration creates the mat_view_runs table, which tracks the mutation runs(create,refresh,drop) of materialized views.
|
9
|
+
# It includes references to the materialized view definition, status, operation type, timestamps,
|
10
|
+
# duration, error messages, and additional metadata.
|
11
|
+
class CreateMatViewRuns < ActiveRecord::Migration[7.1]
|
12
|
+
def change
|
13
|
+
create_table :mat_view_runs do |t|
|
14
|
+
t.references :mat_view_definition,
|
15
|
+
null: false,
|
16
|
+
foreign_key: true,
|
17
|
+
comment: 'Reference to the materialized view definition'
|
18
|
+
|
19
|
+
# 0=pending, 1=running, 2=success, 3=failed
|
20
|
+
t.integer :status, null: false, default: 0, comment: '0=pending,1=running,2=success,3=failed'
|
21
|
+
# 0=create, 1=refresh, 2=drop
|
22
|
+
t.integer :operation, null: false, default: 0, comment: '0=create,1=refresh,2=drop'
|
23
|
+
t.datetime :started_at, comment: 'Timestamp when the operation started'
|
24
|
+
t.datetime :finished_at, comment: 'Timestamp when the operation finished'
|
25
|
+
t.integer :duration_ms, comment: 'Duration of the operation in milliseconds'
|
26
|
+
t.text :error, comment: 'Error message if the operation failed'
|
27
|
+
t.jsonb :meta, null: false, default: {}, comment: 'Additional metadata about the run, such as job ID, row count or parameters'
|
28
|
+
|
29
|
+
t.timestamps
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
MatViews.configure do |config|
|
9
|
+
### Job Adapter Configuration
|
10
|
+
# Set job adapter for MatViews
|
11
|
+
# This can be set to :active_job, :sidekiq, and :resque,
|
12
|
+
# defaults to :active_job.
|
13
|
+
#
|
14
|
+
# Depending on your application's job processing setup.
|
15
|
+
# Uncomment the line below to set the job adapter.
|
16
|
+
# config.job_adapter = :active_job
|
17
|
+
|
18
|
+
# job_queue is the queue name for the job adapter.
|
19
|
+
# Default is :default.
|
20
|
+
# This is used to specify the queue where MatViews jobs will be enqueued.
|
21
|
+
# Uncomment the line below to set the job queue.
|
22
|
+
# config.job_queue = :default
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
module MatViews
|
9
|
+
##
|
10
|
+
# Configuration for the MatViews engine.
|
11
|
+
#
|
12
|
+
# This class provides customization points for how MatViews integrates
|
13
|
+
# with background job systems and controls default behavior across
|
14
|
+
# the engine.
|
15
|
+
#
|
16
|
+
# @example Configure in an initializer
|
17
|
+
# MatViews.configure do |config|
|
18
|
+
# config.job_adapter = :sidekiq
|
19
|
+
# config.job_queue = :low_priority
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Supported job adapters:
|
23
|
+
# - `:active_job` (default)
|
24
|
+
# - `:sidekiq`
|
25
|
+
# - `:resque`
|
26
|
+
#
|
27
|
+
class Configuration
|
28
|
+
##
|
29
|
+
# The job adapter to use for enqueuing jobs.
|
30
|
+
#
|
31
|
+
# @return [Symbol] :active_job, :sidekiq, or :resque
|
32
|
+
attr_accessor :job_adapter
|
33
|
+
|
34
|
+
##
|
35
|
+
# The default queue name to use for jobs.
|
36
|
+
#
|
37
|
+
# @return [Symbol, String]
|
38
|
+
attr_accessor :job_queue
|
39
|
+
|
40
|
+
##
|
41
|
+
# Initialize with defaults.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def initialize
|
45
|
+
@job_adapter = :active_job
|
46
|
+
@job_queue = :default
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|