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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ed05279bf4f2812ebeb8cb882f38772636f28c273b0992a878e13fb06b95d17f
|
4
|
+
data.tar.gz: 8fa0d71481c042d1b763e012b91dfe397373161decf8a68ef13c27bd3349fb33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3233a39ea2a44171bc95d15be3bfccf53f507f49efb946787888f3aeae95b94273126331ac607a83e3d9e23c1f8770a4b0362ee2825b6c7ae1c60aab6f9aff92
|
7
|
+
data.tar.gz: 05566cd4d75f7ef05465387a39794ca1cae6351375e8b298b7deec0f1503e517d09429f5d1b605042dd6f739c0fa6f868fc839ce600bf201228f206c01dbaec9
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 The mat_views Authors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# mat_views (engine)
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/mat_views)
|
4
|
+
[](https://github.com/Code-Vedas/rails_materialized_views/actions/workflows/ci.yml)
|
5
|
+

|
6
|
+
|
7
|
+
Rails engine that manages **PostgreSQL materialized views** with definitions, services, background jobs, and Rake tasks.
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
## Quickstart (diagram)
|
12
|
+
|
13
|
+
```mermaid
|
14
|
+
flowchart LR
|
15
|
+
A[Define MatViewDefinition] --> B[Create]
|
16
|
+
B --> C[Unique Index for CONCURRENT]
|
17
|
+
C --> D[Refresh: regular or concurrent or swap]
|
18
|
+
D --> E[Read MV]
|
19
|
+
D --> F[Track runs]
|
20
|
+
```
|
21
|
+
|
22
|
+
---
|
23
|
+
|
24
|
+
## Install
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle add mat_views
|
28
|
+
bin/rails g mat_views:install
|
29
|
+
bin/rails db:migrate
|
30
|
+
```
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# config/initializers/mat_views.rb
|
34
|
+
MatViews.configure do |c|
|
35
|
+
c.job_queue = :default
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
---
|
40
|
+
|
41
|
+
## Define a view
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
defn = MatViews::MatViewDefinition.create!(
|
45
|
+
name: 'mv_user_activity',
|
46
|
+
sql: <<~SQL,
|
47
|
+
SELECT u.id AS user_id,
|
48
|
+
COUNT(a.*) AS accounts_count,
|
49
|
+
COUNT(e.*) AS events_count,
|
50
|
+
COUNT(s.*) AS sessions_count
|
51
|
+
FROM users u
|
52
|
+
LEFT JOIN accounts a ON a.user_id = u.id
|
53
|
+
LEFT JOIN events e ON e.user_id = u.id
|
54
|
+
LEFT JOIN sessions s ON s.user_id = u.id
|
55
|
+
GROUP BY u.id
|
56
|
+
SQL
|
57
|
+
refresh_strategy: :concurrent,
|
58
|
+
unique_index_columns: ['user_id']
|
59
|
+
)
|
60
|
+
```
|
61
|
+
|
62
|
+
---
|
63
|
+
|
64
|
+
## Services & Jobs
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# Create
|
68
|
+
MatViews::Services::CreateView.new(defn, force: true).run
|
69
|
+
MatViews::CreateViewJob.perform_later(defn.id, force: true)
|
70
|
+
|
71
|
+
# Refresh
|
72
|
+
MatViews::Services::RegularRefresh.new(defn, row_count_strategy: :estimated).run
|
73
|
+
MatViews::RefreshViewJob.perform_later(defn.id, row_count_strategy: :exact)
|
74
|
+
|
75
|
+
# Delete
|
76
|
+
MatViews::Services::DeleteView.new(defn, cascade: false, if_exists: true).run
|
77
|
+
MatViews::DeleteViewJob.perform_later(defn.id, cascade: true)
|
78
|
+
```
|
79
|
+
|
80
|
+
**Uniform response**: `status`, `payload`, `meta`, `success?` / `error?`.
|
81
|
+
|
82
|
+
---
|
83
|
+
|
84
|
+
## Enqueue adapter
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
MatViews::Jobs::Adapter.enqueue(job_class, queue: :default, args: [...])
|
88
|
+
```
|
89
|
+
|
90
|
+
- Uses your configured backend; **no guessing**.
|
91
|
+
- Supports **ActiveJob**, **Sidekiq**, **Resque**.
|
92
|
+
|
93
|
+
---
|
94
|
+
|
95
|
+
## Rake tasks
|
96
|
+
|
97
|
+
```bash
|
98
|
+
# Create
|
99
|
+
bundle exec rake mat_views:create_by_name\[VIEW_NAME,force,--yes]
|
100
|
+
bundle exec rake mat_views:create_by_id\[ID,force,--yes]
|
101
|
+
bundle exec rake mat_views:create_all\[force,--yes]
|
102
|
+
|
103
|
+
# Refresh
|
104
|
+
bundle exec rake mat_views:refresh_by_name\[VIEW_NAME,row_count_strategy,--yes]
|
105
|
+
bundle exec rake mat_views:refresh_by_id\[ID,row_count_strategy,--yes]
|
106
|
+
bundle exec rake mat_views:refresh_all\[row_count_strategy,--yes]
|
107
|
+
|
108
|
+
# Delete
|
109
|
+
bundle exec rake mat_views:delete_by_name\[VIEW_NAME,cascade,--yes]
|
110
|
+
bundle exec rake mat_views:delete_by_id\[ID,cascade,--yes]
|
111
|
+
bundle exec rake mat_views:delete_all\[cascade,--yes]
|
112
|
+
```
|
113
|
+
|
114
|
+
---
|
115
|
+
|
116
|
+
## Docs & policies
|
117
|
+
|
118
|
+
- Root README: [../README.md](../README.md)
|
119
|
+
- **Contributing:** [../CONTRIBUTING.md](../CONTRIBUTING.md)
|
120
|
+
- **Security policy:** [../SECURITY.md](../SECURITY.md)
|
121
|
+
- **Code of Conduct:** [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
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 'bundler/setup'
|
9
|
+
|
10
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
11
|
+
load 'rails/tasks/engine.rake'
|
12
|
+
|
13
|
+
load 'rails/tasks/statistics.rake'
|
14
|
+
|
15
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,39 @@
|
|
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
|
+
#
|
11
|
+
# All classes, modules, and services for materialized view management
|
12
|
+
# are defined under this namespace.
|
13
|
+
#
|
14
|
+
# @example Accessing a job
|
15
|
+
# MatViews::ApplicationJob
|
16
|
+
#
|
17
|
+
module MatViews
|
18
|
+
##
|
19
|
+
# Base class for all background jobs in the mat_views engine.
|
20
|
+
#
|
21
|
+
# Inherits from {ActiveJob::Base} and provides a common superclass
|
22
|
+
# for engine jobs such as {MatViews::CreateViewJob} and {MatViews::RefreshViewJob}.
|
23
|
+
#
|
24
|
+
# @abstract
|
25
|
+
#
|
26
|
+
# @see MatViews::CreateViewJob
|
27
|
+
# @see MatViews::RefreshViewJob
|
28
|
+
# @see MatViews::DeleteViewJob
|
29
|
+
#
|
30
|
+
# @example Defining a custom job
|
31
|
+
# class MyCustomJob < MatViews::ApplicationJob
|
32
|
+
# def perform(definition_id)
|
33
|
+
# # custom logic here
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
class ApplicationJob < ActiveJob::Base
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,188 @@
|
|
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 *creation* of PostgreSQL materialized views for a
|
13
|
+
# given {MatViews::MatViewDefinition}.
|
14
|
+
#
|
15
|
+
# The job:
|
16
|
+
# 1. Normalizes the `force` argument.
|
17
|
+
# 2. Looks up the target {MatViews::MatViewDefinition}.
|
18
|
+
# 3. Starts a {MatViews::MatViewRun} row to track lifecycle/timing, with `operation: :create`.
|
19
|
+
# 4. Executes {MatViews::Services::CreateView}.
|
20
|
+
# 5. Finalizes the run with success/failure, duration, and payload meta.
|
21
|
+
#
|
22
|
+
# @see MatViews::Services::CreateView
|
23
|
+
# @see MatViews::MatViewDefinition
|
24
|
+
# @see MatViews::MatViewRun
|
25
|
+
#
|
26
|
+
# @example Enqueue a create job
|
27
|
+
# MatViews::CreateViewJob.perform_later(definition.id, force: true)
|
28
|
+
#
|
29
|
+
# @example Inline run (test/dev)
|
30
|
+
# MatViews::CreateViewJob.new.perform(definition.id, false)
|
31
|
+
#
|
32
|
+
class CreateViewJob < ::ActiveJob::Base
|
33
|
+
##
|
34
|
+
# Queue name for the job.
|
35
|
+
#
|
36
|
+
# Uses `MatViews.configuration.job_queue` when configured, otherwise `:default`.
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
#
|
40
|
+
queue_as { MatViews.configuration.job_queue || :default }
|
41
|
+
|
42
|
+
##
|
43
|
+
# Perform the job for the given materialized view definition.
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
#
|
47
|
+
# @param definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
48
|
+
# @param force_arg [Boolean, Hash, nil] Optional flag or hash (`{ force: true }`)
|
49
|
+
# to force creation (drop/recreate) when supported by the service.
|
50
|
+
#
|
51
|
+
# @return [Hash] A serialized {MatViews::ServiceResponse#to_h}:
|
52
|
+
# - `:status` [Symbol] one of `:ok, :created, :updated, :noop, :error`
|
53
|
+
# - `:payload` [Hash] service-specific payload (also stored in run.meta)
|
54
|
+
# - `:error` [String, nil] error message if any
|
55
|
+
# - `:duration_ms` [Integer, nil]
|
56
|
+
# - `:meta` [Hash]
|
57
|
+
#
|
58
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
59
|
+
#
|
60
|
+
# @see MatViews::Services::CreateView
|
61
|
+
#
|
62
|
+
def perform(definition_id, force_arg = nil)
|
63
|
+
force = normalize_force(force_arg)
|
64
|
+
|
65
|
+
definition = MatViews::MatViewDefinition.find(definition_id)
|
66
|
+
run = start_run(definition)
|
67
|
+
|
68
|
+
response, duration_ms = execute(definition, force: force)
|
69
|
+
finalize_run!(run, response, duration_ms)
|
70
|
+
response.to_h
|
71
|
+
rescue StandardError => e
|
72
|
+
fail_run!(run, e) if run
|
73
|
+
raise e
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
##
|
79
|
+
# Normalize the `force` argument into a boolean.
|
80
|
+
#
|
81
|
+
# Accepts either a boolean-ish value or a Hash (e.g., `{ force: true }` or `{ "force" => true }`).
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
#
|
85
|
+
# @param arg [Object] Raw argument; commonly `true/false`, `nil`, or `Hash`.
|
86
|
+
# @return [Boolean] Coerced force flag.
|
87
|
+
#
|
88
|
+
def normalize_force(arg)
|
89
|
+
case arg
|
90
|
+
when Hash
|
91
|
+
arg[:force] || arg['force'] || false
|
92
|
+
else
|
93
|
+
!!arg
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Execute the create service and measure duration.
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
#
|
102
|
+
# @param definition [MatViews::MatViewDefinition]
|
103
|
+
# @param force [Boolean]
|
104
|
+
# @return [Array(MatViews::ServiceResponse, Integer)] response and elapsed ms.
|
105
|
+
#
|
106
|
+
def execute(definition, force:)
|
107
|
+
started = monotime
|
108
|
+
response = MatViews::Services::CreateView.new(definition, force: force).run
|
109
|
+
[response, elapsed_ms(started)]
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Begin a {MatViews::MatViewRun} row for lifecycle tracking.
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
#
|
117
|
+
# @param definition [MatViews::MatViewDefinition]
|
118
|
+
# @return [MatViews::MatViewRun] newly created run with `status: :running`
|
119
|
+
#
|
120
|
+
def start_run(definition)
|
121
|
+
MatViews::MatViewRun.create!(
|
122
|
+
mat_view_definition: definition,
|
123
|
+
status: :running,
|
124
|
+
started_at: Time.current,
|
125
|
+
operation: :create
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Finalize the run with success/failure, timing, and meta from the response payload.
|
131
|
+
#
|
132
|
+
# @api private
|
133
|
+
#
|
134
|
+
# @param run [MatViews::MatViewRun]
|
135
|
+
# @param response [MatViews::ServiceResponse]
|
136
|
+
# @param duration_ms [Integer]
|
137
|
+
# @return [void]
|
138
|
+
#
|
139
|
+
def finalize_run!(run, response, duration_ms)
|
140
|
+
base_attrs = {
|
141
|
+
finished_at: Time.current,
|
142
|
+
duration_ms: duration_ms,
|
143
|
+
meta: response.payload || {}
|
144
|
+
}
|
145
|
+
|
146
|
+
if response.success?
|
147
|
+
run.update!(base_attrs.merge(status: :success, error: nil))
|
148
|
+
else
|
149
|
+
run.update!(base_attrs.merge(status: :failed, error: response.error.to_s.presence))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Mark the run failed due to an exception.
|
155
|
+
#
|
156
|
+
# @api private
|
157
|
+
#
|
158
|
+
# @param run [MatViews::MatViewRun]
|
159
|
+
# @param exception [Exception]
|
160
|
+
# @return [void]
|
161
|
+
#
|
162
|
+
def fail_run!(run, exception)
|
163
|
+
run.update!(
|
164
|
+
finished_at: Time.current,
|
165
|
+
duration_ms: run.duration_ms || 0,
|
166
|
+
error: "#{exception.class}: #{exception.message}",
|
167
|
+
status: :failed
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Monotonic clock getter (for elapsed-time measurement).
|
173
|
+
#
|
174
|
+
# @api private
|
175
|
+
# @return [Float] seconds from a monotonic source.
|
176
|
+
#
|
177
|
+
def monotime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
178
|
+
|
179
|
+
##
|
180
|
+
# Convert a monotonic start time to elapsed milliseconds.
|
181
|
+
#
|
182
|
+
# @api private
|
183
|
+
# @param start [Float] monotonic seconds.
|
184
|
+
# @return [Integer] elapsed milliseconds.
|
185
|
+
#
|
186
|
+
def elapsed_ms(start) = ((monotime - start) * 1000).round
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,196 @@
|
|
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 *deletion* of PostgreSQL materialized views via
|
13
|
+
# {MatViews::Services::DeleteView}.
|
14
|
+
#
|
15
|
+
# This job mirrors {MatViews::CreateViewJob} and {MatViews::RefreshViewJob}:
|
16
|
+
# it times the run and persists lifecycle state in {MatViews::MatViewRun}.
|
17
|
+
#
|
18
|
+
# @see MatViews::Services::DeleteView
|
19
|
+
# @see MatViews::MatViewDefinition
|
20
|
+
# @see MatViews::MatViewRun
|
21
|
+
#
|
22
|
+
# @example Enqueue a delete job
|
23
|
+
# MatViews::DeleteViewJob.perform_later(definition.id, cascade: true)
|
24
|
+
#
|
25
|
+
# @example Inline run (test/dev)
|
26
|
+
# MatViews::DeleteViewJob.new.perform(definition.id, false)
|
27
|
+
#
|
28
|
+
class DeleteViewJob < ::ActiveJob::Base
|
29
|
+
##
|
30
|
+
# Queue name for the job.
|
31
|
+
#
|
32
|
+
# Uses `MatViews.configuration.job_queue` when configured, otherwise `:default`.
|
33
|
+
#
|
34
|
+
queue_as { MatViews.configuration.job_queue || :default }
|
35
|
+
|
36
|
+
##
|
37
|
+
# Perform the job for the given materialized view definition.
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
#
|
41
|
+
# @param definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
42
|
+
# @param cascade_arg [Boolean, String, Integer, Hash, nil] Cascade option.
|
43
|
+
# Accepts:
|
44
|
+
# - `true/false`
|
45
|
+
# - `1` (treated as true)
|
46
|
+
# - `"true"`, `"1"`, `"yes"` (case-insensitive)
|
47
|
+
# - `{ cascade: true }` or `{ "cascade" => true }`
|
48
|
+
#
|
49
|
+
# @return [Hash] A serialized {MatViews::ServiceResponse#to_h}:
|
50
|
+
# - `:status` [Symbol] `:success`, `:failed`, etc.
|
51
|
+
# - `:payload` [Hash] response payload (also stored in run.meta)
|
52
|
+
# - `:error` [String, nil]
|
53
|
+
# - `:duration_ms` [Integer]
|
54
|
+
# - `:meta` [Hash]
|
55
|
+
#
|
56
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
57
|
+
#
|
58
|
+
def perform(definition_id, cascade_arg = nil)
|
59
|
+
cascade = normalize_cascade?(cascade_arg)
|
60
|
+
definition = MatViews::MatViewDefinition.find(definition_id)
|
61
|
+
run = start_run(definition)
|
62
|
+
|
63
|
+
response, duration_ms = execute(definition, cascade: cascade)
|
64
|
+
finalize_run!(run, response, duration_ms)
|
65
|
+
response.to_h
|
66
|
+
rescue StandardError => e
|
67
|
+
fail_run!(run, e) if run
|
68
|
+
raise e
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
##
|
74
|
+
# Normalize cascade argument into a boolean.
|
75
|
+
#
|
76
|
+
# @api private
|
77
|
+
# @param arg [Object] Raw cascade argument.
|
78
|
+
# @return [Boolean] Whether to cascade drop.
|
79
|
+
#
|
80
|
+
def normalize_cascade?(arg)
|
81
|
+
value = if arg.is_a?(Hash)
|
82
|
+
arg[:cascade] || arg['cascade']
|
83
|
+
else
|
84
|
+
arg
|
85
|
+
end
|
86
|
+
cascade_value_trueish?(value)
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Evaluate if a value is "truthy" for cascade purposes.
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
# @param value [Object]
|
94
|
+
# @return [Boolean]
|
95
|
+
#
|
96
|
+
def cascade_value_trueish?(value)
|
97
|
+
case value
|
98
|
+
when true
|
99
|
+
true
|
100
|
+
when String
|
101
|
+
%w[true 1 yes].include?(value.strip.downcase)
|
102
|
+
when Integer
|
103
|
+
value == 1
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Execute the delete service and measure duration.
|
111
|
+
#
|
112
|
+
# @api private
|
113
|
+
# @param definition [MatViews::MatViewDefinition]
|
114
|
+
# @param cascade [Boolean]
|
115
|
+
# @return [Array(MatViews::ServiceResponse, Integer)]
|
116
|
+
#
|
117
|
+
def execute(definition, cascade:)
|
118
|
+
started = monotime
|
119
|
+
response = MatViews::Services::DeleteView.new(definition, cascade: cascade, if_exists: true).run
|
120
|
+
[response, elapsed_ms(started)]
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Begin a {MatViews::MatViewRun} row for lifecycle tracking.
|
125
|
+
#
|
126
|
+
# @api private
|
127
|
+
# @param definition [MatViews::MatViewDefinition]
|
128
|
+
# @return [MatViews::MatViewRun]
|
129
|
+
#
|
130
|
+
def start_run(definition)
|
131
|
+
MatViews::MatViewRun.create!(
|
132
|
+
mat_view_definition: definition,
|
133
|
+
status: :running,
|
134
|
+
started_at: Time.current,
|
135
|
+
operation: :drop
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Finalize the run with success/failure, timing, and meta from the response.
|
141
|
+
#
|
142
|
+
# @api private
|
143
|
+
# @param run [MatViews::MatViewRun]
|
144
|
+
# @param response [MatViews::ServiceResponse]
|
145
|
+
# @param duration_ms [Integer]
|
146
|
+
# @return [void]
|
147
|
+
#
|
148
|
+
def finalize_run!(run, response, duration_ms)
|
149
|
+
base_attrs = {
|
150
|
+
finished_at: Time.current,
|
151
|
+
duration_ms: duration_ms,
|
152
|
+
meta: response.payload || {}
|
153
|
+
}
|
154
|
+
|
155
|
+
if response.success?
|
156
|
+
run.update!(base_attrs.merge(status: :success, error: nil))
|
157
|
+
else
|
158
|
+
run.update!(base_attrs.merge(status: :failed, error: response.error.to_s.presence))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Mark the run failed due to an exception.
|
164
|
+
#
|
165
|
+
# @api private
|
166
|
+
# @param run [MatViews::MatViewRun]
|
167
|
+
# @param exception [Exception]
|
168
|
+
# @return [void]
|
169
|
+
#
|
170
|
+
def fail_run!(run, exception)
|
171
|
+
run.update!(
|
172
|
+
finished_at: Time.current,
|
173
|
+
duration_ms: run.duration_ms || 0,
|
174
|
+
error: "#{exception.class}: #{exception.message}",
|
175
|
+
status: :failed
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Monotonic clock getter (for elapsed-time measurement).
|
181
|
+
#
|
182
|
+
# @api private
|
183
|
+
# @return [Float] seconds
|
184
|
+
#
|
185
|
+
def monotime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
186
|
+
|
187
|
+
##
|
188
|
+
# Convert monotonic start time to elapsed milliseconds.
|
189
|
+
#
|
190
|
+
# @api private
|
191
|
+
# @param start [Float]
|
192
|
+
# @return [Integer] elapsed ms
|
193
|
+
#
|
194
|
+
def elapsed_ms(start) = ((monotime - start) * 1000).round
|
195
|
+
end
|
196
|
+
end
|