mat_views 0.1.2 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +10 -10
- data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
- data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
- data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
- data/app/assets/images/mat_views/favicon-16x16.png +0 -0
- data/app/assets/images/mat_views/favicon-32x32.png +0 -0
- data/app/assets/images/mat_views/favicon-48x48.png +0 -0
- data/app/assets/images/mat_views/favicon.ico +0 -0
- data/app/assets/images/mat_views/favicon.svg +18 -0
- data/app/assets/images/mat_views/logo.svg +18 -0
- data/app/assets/images/mat_views/mask-icon.svg +5 -0
- data/app/assets/stylesheets/mat_views/application.css +323 -12
- data/app/controllers/mat_views/admin/application_controller.rb +135 -0
- data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
- data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
- data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
- data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
- data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
- data/app/javascript/mat_views/application.js +8 -0
- data/app/javascript/mat_views/controllers/application.js +10 -0
- data/app/javascript/mat_views/controllers/details_controller.js +122 -0
- data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
- data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
- data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
- data/app/javascript/mat_views/controllers/index.js +10 -0
- data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
- data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
- data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
- data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
- data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
- data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
- data/app/jobs/mat_views/application_job.rb +107 -2
- data/app/jobs/mat_views/create_view_job.rb +21 -122
- data/app/jobs/mat_views/delete_view_job.rb +22 -129
- data/app/jobs/mat_views/refresh_view_job.rb +12 -133
- data/app/models/concerns/mat_views_i18n.rb +139 -0
- data/app/models/mat_views/application_record.rb +1 -0
- data/app/models/mat_views/mat_view_definition.rb +12 -7
- data/app/models/mat_views/mat_view_run.rb +34 -16
- data/app/views/layouts/mat_views/_footer.html.erb +41 -0
- data/app/views/layouts/mat_views/_header.html.erb +25 -0
- data/app/views/layouts/mat_views/admin.html.erb +47 -0
- data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
- data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
- data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
- data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
- data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
- data/app/views/mat_views/admin/runs/index.html.erb +38 -0
- data/app/views/mat_views/admin/runs/show.html.erb +64 -0
- data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
- data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
- data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
- data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/en-AU-ocker.yml +187 -0
- data/config/locales/en-AU.yml +187 -0
- data/config/locales/en-BB.yml +187 -0
- data/config/locales/en-BD.yml +187 -0
- data/config/locales/en-BE.yml +187 -0
- data/config/locales/en-BORK.yml +187 -0
- data/config/locales/en-BS.yml +187 -0
- data/config/locales/en-BZ.yml +187 -0
- data/config/locales/en-CA.yml +187 -0
- data/config/locales/en-CM.yml +187 -0
- data/config/locales/en-CY.yml +187 -0
- data/config/locales/en-EG.yml +187 -0
- data/config/locales/en-FJ.yml +187 -0
- data/config/locales/en-GB.yml +187 -0
- data/config/locales/en-GH.yml +187 -0
- data/config/locales/en-GI.yml +187 -0
- data/config/locales/en-GM.yml +187 -0
- data/config/locales/en-GY.yml +187 -0
- data/config/locales/en-HK.yml +187 -0
- data/config/locales/en-IE.yml +187 -0
- data/config/locales/en-IN.yml +187 -0
- data/config/locales/en-JM.yml +187 -0
- data/config/locales/en-KE.yml +187 -0
- data/config/locales/en-LK.yml +187 -0
- data/config/locales/en-LOL.yml +187 -0
- data/config/locales/en-LR.yml +187 -0
- data/config/locales/en-MS.yml +187 -0
- data/config/locales/en-MT.yml +187 -0
- data/config/locales/en-MW.yml +187 -0
- data/config/locales/en-MY.yml +187 -0
- data/config/locales/en-NG.yml +187 -0
- data/config/locales/en-NP.yml +187 -0
- data/config/locales/en-NZ.yml +187 -0
- data/config/locales/en-PG.yml +187 -0
- data/config/locales/en-PH.yml +187 -0
- data/config/locales/en-PK.yml +187 -0
- data/config/locales/en-RW.yml +187 -0
- data/config/locales/en-SCOT.yml +187 -0
- data/config/locales/en-SG.yml +187 -0
- data/config/locales/en-SHAKESPEARE.yml +187 -0
- data/config/locales/en-SL.yml +187 -0
- data/config/locales/en-SS.yml +187 -0
- data/config/locales/en-TH.yml +187 -0
- data/config/locales/en-TT.yml +187 -0
- data/config/locales/en-TZ.yml +187 -0
- data/config/locales/en-UG.yml +187 -0
- data/config/locales/en-US-pirate.yml +187 -0
- data/config/locales/en-US.yml +187 -0
- data/config/locales/en-YODA.yml +187 -0
- data/config/locales/en-ZA.yml +187 -0
- data/config/locales/en-ZW.yml +187 -0
- data/config/locales/en.yml +187 -0
- data/config/routes.rb +27 -3
- data/lib/ext/exception.rb +20 -0
- data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +7 -7
- data/lib/mat_views/admin/auth_bridge.rb +93 -0
- data/lib/mat_views/admin/default_auth.rb +61 -0
- data/lib/mat_views/configuration.rb +9 -0
- data/lib/mat_views/engine.rb +50 -2
- data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
- data/lib/mat_views/jobs/adapter.rb +8 -5
- data/lib/mat_views/service_response.rb +30 -15
- data/lib/mat_views/services/base_service.rb +204 -41
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +38 -121
- data/lib/mat_views/services/create_view.rb +72 -55
- data/lib/mat_views/services/delete_view.rb +46 -95
- data/lib/mat_views/services/regular_refresh.rb +38 -94
- data/lib/mat_views/services/swap_refresh.rb +83 -123
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +13 -6
- data/lib/tasks/helpers.rb +27 -27
- data/lib/tasks/mat_views_tasks.rake +48 -42
- metadata +131 -5
@@ -10,10 +10,10 @@ require 'securerandom'
|
|
10
10
|
module MatViews
|
11
11
|
module Services
|
12
12
|
##
|
13
|
-
# Service that performs a **swap-style refresh** of a
|
13
|
+
# Service that performs a **swap-style refresh** of a materialised view.
|
14
14
|
#
|
15
15
|
# Instead of locking the existing view, this strategy builds a new
|
16
|
-
# temporary
|
16
|
+
# temporary materialised view and atomically swaps it in. This approach
|
17
17
|
# minimizes downtime and allows for safer rebuilds of large views.
|
18
18
|
#
|
19
19
|
# Steps:
|
@@ -21,106 +21,112 @@ module MatViews
|
|
21
21
|
# 2. In a transaction: rename original → old, tmp → original, drop old.
|
22
22
|
# 3. Recreate declared unique indexes (if any).
|
23
23
|
#
|
24
|
-
#
|
25
|
-
# - `:estimated`
|
26
|
-
# - `:exact` → accurate, using `COUNT(*)`
|
27
|
-
# - `nil` → skip row count
|
24
|
+
# Options:
|
25
|
+
# - `row_count_strategy:` (Symbol, default: :none) → one of `:estimated`, `:exact`, or `:none or nil` to control row count reporting
|
28
26
|
#
|
29
|
-
#
|
27
|
+
# Returns a {MatViews::ServiceResponse}
|
30
28
|
#
|
31
|
-
# @
|
32
|
-
#
|
33
|
-
#
|
29
|
+
# @see MatViews::Services::ConcurrentRefresh
|
30
|
+
# @see MatViews::Services::RegularRefresh
|
31
|
+
#
|
32
|
+
# @example Direct usage
|
33
|
+
# svc = MatViews::Services::SwapRefresh.new(definition, **options)
|
34
|
+
# response = svc.call
|
35
|
+
# response.success? # => true/false
|
36
|
+
#
|
37
|
+
# @example via job, this is the typical usage and will create a run record in the DB
|
38
|
+
# When definition.refresh_strategy == "concurrent"
|
39
|
+
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::SwapRefresh, definition.id, **options)
|
34
40
|
#
|
35
41
|
class SwapRefresh < BaseService
|
36
|
-
|
37
|
-
# Row count strategy (`:estimated`, `:exact`, `nil`).
|
38
|
-
#
|
39
|
-
# @return [Symbol, nil]
|
40
|
-
attr_reader :row_count_strategy
|
41
|
-
|
42
|
-
##
|
43
|
-
# @param definition [MatViews::MatViewDefinition]
|
44
|
-
# @param row_count_strategy [Symbol, nil]
|
45
|
-
def initialize(definition, row_count_strategy: :estimated)
|
46
|
-
super(definition)
|
47
|
-
@row_count_strategy = row_count_strategy
|
48
|
-
end
|
42
|
+
private
|
49
43
|
|
50
44
|
##
|
51
45
|
# Execute the swap refresh.
|
52
46
|
#
|
53
47
|
# @return [MatViews::ServiceResponse]
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
payload = { view: "#{schema}.#{rel}" }
|
65
|
-
payload[:row_count] = fetch_rows_count if row_count_strategy.present?
|
66
|
-
|
67
|
-
ok(:updated, payload: payload, meta: { steps: steps, row_count_strategy: row_count_strategy, swap: true })
|
68
|
-
rescue StandardError => e
|
69
|
-
error_response(e,
|
70
|
-
meta: {
|
71
|
-
steps: steps,
|
72
|
-
backtrace: Array(e.backtrace),
|
73
|
-
row_count_strategy: row_count_strategy,
|
74
|
-
swap: true
|
75
|
-
},
|
76
|
-
payload: { view: "#{schema}.#{rel}" })
|
77
|
-
end
|
48
|
+
# - `status: :updated` on success, with `response` containing:
|
49
|
+
# - `view` - the qualified view name
|
50
|
+
# - `row_count_before` - if requested and available
|
51
|
+
# - `row_count_after` - if requested and available
|
52
|
+
# - `status: :error` with `error` on failure, with `error` containing:
|
53
|
+
# - serlialized exception class, message, and backtrace in a hash
|
54
|
+
#
|
55
|
+
def _run
|
56
|
+
self.response = { view: "#{schema}.#{rel}" }
|
78
57
|
|
79
|
-
|
58
|
+
response[:row_count_before] = fetch_rows_count
|
59
|
+
response[:sql] = swap_view
|
60
|
+
response[:row_count_after] = fetch_rows_count
|
80
61
|
|
81
|
-
|
82
|
-
|
83
|
-
# ────────────────────────────────────────────────────────────────
|
62
|
+
ok(:updated)
|
63
|
+
end
|
84
64
|
|
85
65
|
##
|
86
|
-
#
|
66
|
+
# Validation step (invoked by BaseService#call before execution).
|
67
|
+
# Ensures view exists.
|
87
68
|
#
|
88
69
|
# @api private
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
#
|
73
|
+
def prepare
|
74
|
+
raise_err "Materialized view #{schema}.#{rel} does not exist" unless view_exists?
|
93
75
|
|
94
76
|
nil
|
95
77
|
end
|
96
78
|
|
79
|
+
##
|
80
|
+
# Assign the request parameters.
|
81
|
+
# Called by {#call} before {#prepare}.
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
# @return [void]
|
85
|
+
#
|
86
|
+
def assign_request
|
87
|
+
self.request = { row_count_strategy: row_count_strategy, swap: true }
|
88
|
+
end
|
89
|
+
|
97
90
|
##
|
98
91
|
# Perform rename/drop/index recreation in a transaction.
|
99
92
|
#
|
100
93
|
# @api private
|
101
94
|
# @return [Array<String>] SQL steps executed
|
102
|
-
def
|
103
|
-
|
95
|
+
def swap_view
|
96
|
+
conn.execute(create_temp_view_sql)
|
97
|
+
steps = [
|
98
|
+
move_current_to_old_sql,
|
99
|
+
move_temp_to_current_sql,
|
100
|
+
drop_old_view_sql,
|
101
|
+
recreate_declared_unique_indexes_sql
|
102
|
+
].compact
|
104
103
|
conn.transaction do
|
105
|
-
|
106
|
-
|
107
|
-
conn.execute(rename_orig_sql)
|
104
|
+
steps.each { |step| conn.execute(step) }
|
105
|
+
end
|
108
106
|
|
109
|
-
|
110
|
-
|
111
|
-
|
107
|
+
# prepend the create step
|
108
|
+
steps.unshift(create_temp_view_sql)
|
109
|
+
steps
|
110
|
+
end
|
112
111
|
|
113
|
-
|
114
|
-
|
115
|
-
|
112
|
+
def create_temp_view_sql
|
113
|
+
@create_temp_view_sql ||= %(CREATE MATERIALIZED VIEW #{q_tmp} AS #{definition.sql} WITH DATA)
|
114
|
+
end
|
116
115
|
|
117
|
-
|
118
|
-
|
119
|
-
|
116
|
+
def move_current_to_old_sql
|
117
|
+
%(ALTER MATERIALIZED VIEW #{qualified_rel} RENAME TO #{conn.quote_column_name(old_rel)})
|
118
|
+
end
|
119
|
+
|
120
|
+
def move_temp_to_current_sql
|
121
|
+
%(ALTER MATERIALIZED VIEW #{q_tmp} RENAME TO #{conn.quote_column_name(rel)})
|
122
|
+
end
|
123
|
+
|
124
|
+
def drop_old_view_sql
|
125
|
+
%(DROP MATERIALIZED VIEW #{q_old})
|
120
126
|
end
|
121
127
|
|
122
128
|
##
|
123
|
-
# Quote the temporary
|
129
|
+
# Quote the temporary materialised view name.
|
124
130
|
#
|
125
131
|
# @api private
|
126
132
|
# @return [String] quoted temporary view name
|
@@ -129,7 +135,7 @@ module MatViews
|
|
129
135
|
end
|
130
136
|
|
131
137
|
##
|
132
|
-
# Quote the original
|
138
|
+
# Quote the original materialised view name.
|
133
139
|
#
|
134
140
|
# @api private
|
135
141
|
# @return [String] quoted original view name
|
@@ -159,62 +165,16 @@ module MatViews
|
|
159
165
|
# Recreate declared unique indexes on the swapped-in view.
|
160
166
|
#
|
161
167
|
# @api private
|
162
|
-
# @
|
163
|
-
|
164
|
-
# @param steps [Array<String>] collected SQL
|
165
|
-
def recreate_declared_unique_indexes!(schema:, rel:, steps:)
|
168
|
+
# @return [String] SQL statements to execute
|
169
|
+
def recreate_declared_unique_indexes_sql
|
166
170
|
cols = Array(definition.unique_index_columns).map(&:to_s).reject(&:empty?)
|
167
|
-
return if cols.empty?
|
171
|
+
return nil if cols.empty?
|
168
172
|
|
169
|
-
quoted_cols = cols.map { |
|
173
|
+
quoted_cols = cols.map { |col| conn.quote_column_name(col) }.join(', ')
|
170
174
|
idx_name = conn.quote_column_name("#{rel}_uniq_#{cols.join('_')}")
|
171
175
|
q_rel = conn.quote_table_name("#{schema}.#{rel}")
|
172
176
|
|
173
|
-
|
174
|
-
steps << sql
|
175
|
-
conn.execute(sql)
|
176
|
-
end
|
177
|
-
|
178
|
-
# ────────────────────────────────────────────────────────────────
|
179
|
-
# rows counting
|
180
|
-
# ────────────────────────────────────────────────────────────────
|
181
|
-
|
182
|
-
##
|
183
|
-
# Fetch the row count based on the configured strategy.
|
184
|
-
#
|
185
|
-
# @api private
|
186
|
-
# @return [Integer, nil]
|
187
|
-
def fetch_rows_count
|
188
|
-
case row_count_strategy
|
189
|
-
when :estimated then estimated_rows_count
|
190
|
-
when :exact then exact_rows_count
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
##
|
195
|
-
# Approximate row count via `pg_class.reltuples`.
|
196
|
-
#
|
197
|
-
# @api private
|
198
|
-
# @return [Integer]
|
199
|
-
def estimated_rows_count
|
200
|
-
conn.select_value(<<~SQL).to_i
|
201
|
-
SELECT COALESCE(c.reltuples::bigint, 0)
|
202
|
-
FROM pg_class c
|
203
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
204
|
-
WHERE c.relkind IN ('m','r','p')
|
205
|
-
AND n.nspname = #{conn.quote(schema)}
|
206
|
-
AND c.relname = #{conn.quote(rel)}
|
207
|
-
LIMIT 1
|
208
|
-
SQL
|
209
|
-
end
|
210
|
-
|
211
|
-
##
|
212
|
-
# Accurate row count via `COUNT(*)`.
|
213
|
-
#
|
214
|
-
# @api private
|
215
|
-
# @return [Integer]
|
216
|
-
def exact_rows_count
|
217
|
-
conn.select_value("SELECT COUNT(*) FROM #{qualified_rel}").to_i
|
177
|
+
%(CREATE UNIQUE INDEX #{idx_name} ON #{q_rel} (#{quoted_cols}))
|
218
178
|
end
|
219
179
|
end
|
220
180
|
end
|
data/lib/mat_views/version.rb
CHANGED
data/lib/mat_views.rb
CHANGED
@@ -5,8 +5,10 @@
|
|
5
5
|
# This source code is licensed under the MIT license found in the
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
7
7
|
|
8
|
+
require 'ext/exception'
|
8
9
|
require 'mat_views/version'
|
9
10
|
require 'mat_views/engine'
|
11
|
+
require 'mat_views/helpers/ui_test_ids'
|
10
12
|
require 'mat_views/configuration'
|
11
13
|
require 'mat_views/jobs/adapter'
|
12
14
|
require 'mat_views/service_response'
|
@@ -16,13 +18,16 @@ require 'mat_views/services/regular_refresh'
|
|
16
18
|
require 'mat_views/services/concurrent_refresh'
|
17
19
|
require 'mat_views/services/swap_refresh'
|
18
20
|
require 'mat_views/services/delete_view'
|
21
|
+
require 'mat_views/services/check_matview_exists'
|
22
|
+
require 'mat_views/admin/auth_bridge'
|
23
|
+
require 'mat_views/admin/default_auth'
|
19
24
|
|
20
25
|
##
|
21
26
|
# MatViews is a Rails engine that provides first-class support for
|
22
|
-
# PostgreSQL
|
27
|
+
# PostgreSQL materialised views in Rails applications.
|
23
28
|
#
|
24
29
|
# Features include:
|
25
|
-
# - Declarative definitions for
|
30
|
+
# - Declarative definitions for materialised views
|
26
31
|
# - Safe creation, refresh (regular, concurrent, swap), and deletion
|
27
32
|
# - Background job integration (ActiveJob, Sidekiq, Resque)
|
28
33
|
# - Tracking of run history and metrics
|
@@ -35,23 +40,25 @@ require 'mat_views/services/delete_view'
|
|
35
40
|
# end
|
36
41
|
#
|
37
42
|
# Once mounted, Rails apps can leverage MatViews services and jobs
|
38
|
-
# to manage
|
43
|
+
# to manage materialised views consistently.
|
39
44
|
module MatViews
|
40
45
|
class << self
|
41
46
|
# Global configuration for MatViews
|
42
47
|
# @return [MatViews::Configuration]
|
43
|
-
|
48
|
+
attr_reader :configuration
|
44
49
|
|
45
50
|
# Configure MatViews via block.
|
46
51
|
#
|
47
52
|
# Example:
|
48
53
|
# MatViews.configure do |config|
|
49
54
|
# config.job_adapter = :sidekiq
|
50
|
-
# config.job_queue = :
|
55
|
+
# config.job_queue = :materialised
|
51
56
|
# end
|
52
57
|
def configure
|
53
|
-
|
58
|
+
@configuration ||= Configuration.new
|
54
59
|
yield(configuration)
|
60
|
+
|
61
|
+
configuration.admin_ui[:row_count_strategy] ||= :none
|
55
62
|
end
|
56
63
|
end
|
57
64
|
end
|
data/lib/tasks/helpers.rb
CHANGED
@@ -13,7 +13,7 @@ module MatViews
|
|
13
13
|
# - Parsing boolean/flag-like arguments
|
14
14
|
# - Confirmation prompts
|
15
15
|
# - Enqueueing background jobs for create, refresh, and delete operations
|
16
|
-
# - Looking up
|
16
|
+
# - Looking up materialised view definitions
|
17
17
|
#
|
18
18
|
# By extracting this logic, Rake tasks can remain clean and declarative.
|
19
19
|
module Helpers
|
@@ -51,15 +51,15 @@ module MatViews
|
|
51
51
|
end
|
52
52
|
|
53
53
|
# Parse row count strategy from arg or ROW_COUNT_STRATEGY env.
|
54
|
-
# Defaults to :
|
54
|
+
# Defaults to :none if blank.
|
55
55
|
def parse_row_count_strategy(arg)
|
56
|
-
|
57
|
-
return :
|
56
|
+
str = (arg || ENV.fetch('ROW_COUNT_STRATEGY', nil)).to_s.strip
|
57
|
+
return :none if str.empty?
|
58
58
|
|
59
|
-
|
59
|
+
str.to_sym
|
60
60
|
end
|
61
61
|
|
62
|
-
# Check if a
|
62
|
+
# Check if a materialised view exists in schema.
|
63
63
|
# @param rel [String] relation name
|
64
64
|
# @param schema [String] schema name
|
65
65
|
# @return [Boolean]
|
@@ -78,14 +78,15 @@ module MatViews
|
|
78
78
|
# @return [MatViews::MatViewDefinition]
|
79
79
|
# @raise [RuntimeError] if no definition found or mismatch with DB
|
80
80
|
def find_definition_by_name!(raw_name)
|
81
|
-
|
81
|
+
raw_name_string = raw_name&.to_s&.strip
|
82
|
+
raise 'view_name is required' unless raw_name_string && !raw_name_string.empty?
|
82
83
|
|
83
84
|
schema, rel =
|
84
|
-
if
|
85
|
-
parts =
|
85
|
+
if raw_name_string.include?('.')
|
86
|
+
parts = raw_name_string.split('.', 2)
|
86
87
|
[parts[0], parts[1]]
|
87
88
|
else
|
88
|
-
[nil,
|
89
|
+
[nil, raw_name_string]
|
89
90
|
end
|
90
91
|
|
91
92
|
defn = MatViews::MatViewDefinition.find_by(name: rel)
|
@@ -109,7 +110,7 @@ module MatViews
|
|
109
110
|
# Otherwise, prompts user for confirmation and raises if declined.
|
110
111
|
def confirm!(message, skip: false)
|
111
112
|
if skip
|
112
|
-
logger.info("[mat_views] #{message}
|
113
|
+
logger.info("[mat_views] #{message} - confirmation skipped.")
|
113
114
|
return
|
114
115
|
end
|
115
116
|
|
@@ -124,32 +125,31 @@ module MatViews
|
|
124
125
|
|
125
126
|
# Enqueue a CreateView job for given definition.
|
126
127
|
#
|
127
|
-
# @param
|
128
|
+
# @param mat_view_definition_id [Integer] MatViewDefinition ID
|
128
129
|
# @param force [Boolean] whether to force creation
|
130
|
+
# @param row_count_strategy [Symbol] :estimated or :exact or :none
|
129
131
|
# @return [void]
|
130
|
-
def enqueue_create
|
131
|
-
q = MatViews.configuration.job_queue || :default
|
132
|
+
def enqueue_create(mat_view_definition_id, force, row_count_strategy)
|
132
133
|
MatViews::Jobs::Adapter.enqueue(
|
133
134
|
MatViews::CreateViewJob,
|
134
|
-
queue:
|
135
|
-
args: [
|
135
|
+
queue: MatViews.configuration.job_queue || :default,
|
136
|
+
args: [mat_view_definition_id, force, row_count_strategy]
|
136
137
|
)
|
137
138
|
end
|
138
139
|
|
139
140
|
# Enqueue a RefreshView job for given definition.
|
140
141
|
#
|
141
|
-
# @param
|
142
|
+
# @param mat_view_definition_id [Integer] MatViewDefinition ID
|
142
143
|
# @param row_count_strategy [Symbol] :estimated or :exact
|
143
144
|
# @return [void]
|
144
145
|
#
|
145
146
|
# This method allows scheduling a refresh operation with the specified row count strategy.
|
146
147
|
# It uses the configured job adapter to enqueue the job.
|
147
|
-
def enqueue_refresh
|
148
|
-
q = MatViews.configuration.job_queue || :default
|
148
|
+
def enqueue_refresh(mat_view_definition_id, row_count_strategy)
|
149
149
|
MatViews::Jobs::Adapter.enqueue(
|
150
150
|
MatViews::RefreshViewJob,
|
151
|
-
queue:
|
152
|
-
args: [
|
151
|
+
queue: MatViews.configuration.job_queue || :default,
|
152
|
+
args: [mat_view_definition_id, row_count_strategy]
|
153
153
|
)
|
154
154
|
end
|
155
155
|
|
@@ -166,18 +166,18 @@ module MatViews
|
|
166
166
|
|
167
167
|
# Enqueue a DeleteView job for given definition.
|
168
168
|
#
|
169
|
-
# @param
|
169
|
+
# @param mat_view_definition_id [Integer] MatViewDefinition ID
|
170
170
|
# @param cascade [Boolean] whether to drop with CASCADE
|
171
|
+
# @param row_count_strategy [Symbol] :estimated or :exact or :none
|
171
172
|
# @return [void]
|
172
173
|
#
|
173
|
-
# This method schedules a job to delete the
|
174
|
+
# This method schedules a job to delete the materialised view, optionally with CASCADE.
|
174
175
|
# It uses the configured job adapter to enqueue the job.
|
175
|
-
def enqueue_delete
|
176
|
-
q = MatViews.configuration.job_queue || :default
|
176
|
+
def enqueue_delete(mat_view_definition_id, cascade, row_count_strategy)
|
177
177
|
MatViews::Jobs::Adapter.enqueue(
|
178
178
|
MatViews::DeleteViewJob,
|
179
|
-
queue:
|
180
|
-
args: [
|
179
|
+
queue: MatViews.configuration.job_queue || :default,
|
180
|
+
args: [mat_view_definition_id, cascade, row_count_strategy]
|
181
181
|
)
|
182
182
|
end
|
183
183
|
end
|
@@ -10,33 +10,36 @@ namespace :mat_views do
|
|
10
10
|
# ───────────── CREATE ─────────────
|
11
11
|
|
12
12
|
desc 'Enqueue a CREATE for a specific view by its name (optionally schema-qualified)'
|
13
|
-
task :create_by_name, %i[view_name force yes] => :environment do |_t, args|
|
13
|
+
task :create_by_name, %i[view_name force row_count_strategy yes] => :environment do |_t, args|
|
14
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
14
15
|
force = helpers.parse_force?(args[:force])
|
15
16
|
skip = helpers.skip_confirm?(args[:yes])
|
16
17
|
defn = helpers.find_definition_by_name!(args[:view_name])
|
17
18
|
|
18
|
-
helpers.confirm!("Enqueue CREATE for view=#{defn.name} (id=#{defn.id}), force=#{force}", skip: skip)
|
19
|
-
helpers.enqueue_create
|
20
|
-
helpers.logger.info("[mat_views] Enqueued CreateViewJob for definition ##{defn.id} (#{defn.name}), force=#{force}")
|
19
|
+
helpers.confirm!("Enqueue CREATE for view=#{defn.name} (id=#{defn.id}), force=#{force}, row_count_strategy=#{rcs}", skip: skip)
|
20
|
+
helpers.enqueue_create(defn.id, force, rcs)
|
21
|
+
helpers.logger.info("[mat_views] Enqueued CreateViewJob for definition ##{defn.id} (#{defn.name}), force=#{force}, row_count_strategy=#{rcs}.")
|
21
22
|
end
|
22
23
|
|
23
24
|
desc 'Enqueue a CREATE for a specific view by its definition ID'
|
24
|
-
task :create_by_id, %i[
|
25
|
-
raise 'mat_views:create_by_id requires a
|
25
|
+
task :create_by_id, %i[mat_view_definition_id force row_count_strategy yes] => :environment do |_t, args|
|
26
|
+
raise 'mat_views:create_by_id requires a mat_view_definition_id parameter' if args[:mat_view_definition_id].to_s.strip.empty?
|
26
27
|
|
28
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
27
29
|
force = helpers.parse_force?(args[:force])
|
28
30
|
skip = helpers.skip_confirm?(args[:yes])
|
29
31
|
|
30
|
-
defn = MatViews::MatViewDefinition.find_by(id: args[:
|
31
|
-
raise "No MatViews::MatViewDefinition found for id=#{args[:
|
32
|
+
defn = MatViews::MatViewDefinition.find_by(id: args[:mat_view_definition_id])
|
33
|
+
raise "No MatViews::MatViewDefinition found for id=#{args[:mat_view_definition_id]}" unless defn
|
32
34
|
|
33
|
-
helpers.confirm!("Enqueue CREATE for id=#{defn.id} (#{defn.name}), force=#{force}", skip: skip)
|
34
|
-
helpers.enqueue_create
|
35
|
-
helpers.logger.info("[mat_views] Enqueued CreateViewJob for definition ##{defn.id} (#{defn.name}), force=#{force}")
|
35
|
+
helpers.confirm!("Enqueue CREATE for id=#{defn.id} (#{defn.name}), force=#{force}, row_count_strategy=#{rcs}", skip: skip)
|
36
|
+
helpers.enqueue_create(defn.id, force, rcs)
|
37
|
+
helpers.logger.info("[mat_views] Enqueued CreateViewJob for definition ##{defn.id} (#{defn.name}), force=#{force}, row_count_strategy=#{rcs}.")
|
36
38
|
end
|
37
39
|
|
38
|
-
desc 'Enqueue CREATE jobs for ALL defined
|
39
|
-
task :create_all, %i[force yes] => :environment do |_t, args|
|
40
|
+
desc 'Enqueue CREATE jobs for ALL defined materialised views'
|
41
|
+
task :create_all, %i[force row_count_strategy yes] => :environment do |_t, args|
|
42
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
40
43
|
force = helpers.parse_force?(args[:force])
|
41
44
|
skip = helpers.skip_confirm?(args[:yes])
|
42
45
|
|
@@ -47,9 +50,9 @@ namespace :mat_views do
|
|
47
50
|
next
|
48
51
|
end
|
49
52
|
|
50
|
-
helpers.confirm!("Enqueue CREATE for ALL (#{count}) views, force=#{force}", skip: skip)
|
51
|
-
scope.find_each { |defn| helpers.enqueue_create
|
52
|
-
helpers.logger.info("[mat_views] Enqueued #{count} CreateViewJob(s), force=#{force}.")
|
53
|
+
helpers.confirm!("Enqueue CREATE for ALL (#{count}) views, force=#{force}, row_count_strategy=#{rcs}", skip: skip)
|
54
|
+
scope.find_each { |defn| helpers.enqueue_create(defn.id, force, rcs) }
|
55
|
+
helpers.logger.info("[mat_views] Enqueued #{count} CreateViewJob(s), force=#{force}, row_count_strategy=#{rcs}.")
|
53
56
|
end
|
54
57
|
|
55
58
|
# ───────────── REFRESH ─────────────
|
@@ -61,26 +64,26 @@ namespace :mat_views do
|
|
61
64
|
defn = helpers.find_definition_by_name!(args[:view_name])
|
62
65
|
|
63
66
|
helpers.confirm!("Enqueue REFRESH for view=#{defn.name} (id=#{defn.id}), row_count_strategy=#{rcs}", skip: skip)
|
64
|
-
helpers.enqueue_refresh
|
65
|
-
helpers.logger.info("[mat_views] Enqueued RefreshViewJob for definition ##{defn.id} (#{defn.name}), row_count_strategy=#{rcs}")
|
67
|
+
helpers.enqueue_refresh(defn.id, rcs)
|
68
|
+
helpers.logger.info("[mat_views] Enqueued RefreshViewJob for definition ##{defn.id} (#{defn.name}), row_count_strategy=#{rcs}.")
|
66
69
|
end
|
67
70
|
|
68
71
|
desc 'Enqueue a REFRESH for a specific view by its definition ID'
|
69
|
-
task :refresh_by_id, %i[
|
70
|
-
raise 'mat_views:refresh_by_id requires a
|
72
|
+
task :refresh_by_id, %i[mat_view_definition_id row_count_strategy yes] => :environment do |_t, args|
|
73
|
+
raise 'mat_views:refresh_by_id requires a mat_view_definition_id parameter' if args[:mat_view_definition_id].to_s.strip.empty?
|
71
74
|
|
72
75
|
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
73
76
|
skip = helpers.skip_confirm?(args[:yes])
|
74
77
|
|
75
|
-
defn = MatViews::MatViewDefinition.find_by(id: args[:
|
76
|
-
raise "No MatViews::MatViewDefinition found for id=#{args[:
|
78
|
+
defn = MatViews::MatViewDefinition.find_by(id: args[:mat_view_definition_id])
|
79
|
+
raise "No MatViews::MatViewDefinition found for id=#{args[:mat_view_definition_id]}" unless defn
|
77
80
|
|
78
81
|
helpers.confirm!("Enqueue REFRESH for id=#{defn.id} (#{defn.name}), row_count_strategy=#{rcs}", skip: skip)
|
79
|
-
helpers.enqueue_refresh
|
80
|
-
helpers.logger.info("[mat_views] Enqueued RefreshViewJob for definition ##{defn.id} (#{defn.name}), row_count_strategy=#{rcs}")
|
82
|
+
helpers.enqueue_refresh(defn.id, rcs)
|
83
|
+
helpers.logger.info("[mat_views] Enqueued RefreshViewJob for definition ##{defn.id} (#{defn.name}), row_count_strategy=#{rcs}.")
|
81
84
|
end
|
82
85
|
|
83
|
-
desc 'Enqueue REFRESH jobs for ALL defined
|
86
|
+
desc 'Enqueue REFRESH jobs for ALL defined materialised views'
|
84
87
|
task :refresh_all, %i[row_count_strategy yes] => :environment do |_t, args|
|
85
88
|
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
86
89
|
skip = helpers.skip_confirm?(args[:yes])
|
@@ -93,40 +96,43 @@ namespace :mat_views do
|
|
93
96
|
end
|
94
97
|
|
95
98
|
helpers.confirm!("Enqueue REFRESH for ALL (#{count}) views, row_count_strategy=#{rcs}", skip: skip)
|
96
|
-
scope.find_each { |defn| helpers.enqueue_refresh
|
99
|
+
scope.find_each { |defn| helpers.enqueue_refresh(defn.id, rcs) }
|
97
100
|
helpers.logger.info("[mat_views] Enqueued #{count} RefreshViewJob(s), row_count_strategy=#{rcs}.")
|
98
101
|
end
|
99
102
|
|
100
103
|
# ───────────── DELETE ─────────────
|
101
104
|
|
102
105
|
desc 'Enqueue a DELETE (DROP MATERIALIZED VIEW) for a specific view by its name (optionally schema-qualified)'
|
103
|
-
task :delete_by_name, %i[view_name cascade yes] => :environment do |_t, args|
|
106
|
+
task :delete_by_name, %i[view_name cascade row_count_strategy yes] => :environment do |_t, args|
|
104
107
|
cascade = helpers.parse_cascade?(args[:cascade])
|
108
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
105
109
|
skip = helpers.skip_confirm?(args[:yes])
|
106
110
|
defn = helpers.find_definition_by_name!(args[:view_name])
|
107
111
|
|
108
|
-
helpers.confirm!("Enqueue DELETE for view=#{defn.name} (id=#{defn.id}), cascade=#{cascade}", skip: skip)
|
109
|
-
helpers.enqueue_delete
|
110
|
-
helpers.logger.info("[mat_views] Enqueued DeleteViewJob for definition ##{defn.id} (#{defn.name}), cascade=#{cascade}")
|
112
|
+
helpers.confirm!("Enqueue DELETE for view=#{defn.name} (id=#{defn.id}), cascade=#{cascade}, row_count_strategy=#{rcs}", skip: skip)
|
113
|
+
helpers.enqueue_delete(defn.id, cascade, rcs)
|
114
|
+
helpers.logger.info("[mat_views] Enqueued DeleteViewJob for definition ##{defn.id} (#{defn.name}), cascade=#{cascade}, row_count_strategy=#{rcs}.")
|
111
115
|
end
|
112
116
|
|
113
117
|
desc 'Enqueue a DELETE (DROP MATERIALIZED VIEW) for a specific view by its definition ID'
|
114
|
-
task :delete_by_id, %i[
|
115
|
-
raise 'mat_views:delete_by_id requires a
|
118
|
+
task :delete_by_id, %i[mat_view_definition_id cascade row_count_strategy yes] => :environment do |_t, args|
|
119
|
+
raise 'mat_views:delete_by_id requires a mat_view_definition_id parameter' if args[:mat_view_definition_id].to_s.strip.empty?
|
116
120
|
|
121
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
117
122
|
cascade = helpers.parse_cascade?(args[:cascade])
|
118
123
|
skip = helpers.skip_confirm?(args[:yes])
|
119
124
|
|
120
|
-
defn = MatViews::MatViewDefinition.find_by(id: args[:
|
121
|
-
raise "No MatViews::MatViewDefinition found for id=#{args[:
|
125
|
+
defn = MatViews::MatViewDefinition.find_by(id: args[:mat_view_definition_id])
|
126
|
+
raise "No MatViews::MatViewDefinition found for id=#{args[:mat_view_definition_id]}" unless defn
|
122
127
|
|
123
|
-
helpers.confirm!("Enqueue DELETE for id=#{defn.id} (#{defn.name}), cascade=#{cascade}", skip: skip)
|
124
|
-
helpers.enqueue_delete
|
125
|
-
helpers.logger.info("[mat_views] Enqueued DeleteViewJob for definition ##{defn.id} (#{defn.name}), cascade=#{cascade}")
|
128
|
+
helpers.confirm!("Enqueue DELETE for id=#{defn.id} (#{defn.name}), cascade=#{cascade}, row_count_strategy=#{rcs}", skip: skip)
|
129
|
+
helpers.enqueue_delete(defn.id, cascade, rcs)
|
130
|
+
helpers.logger.info("[mat_views] Enqueued DeleteViewJob for definition ##{defn.id} (#{defn.name}), cascade=#{cascade}, row_count_strategy=#{rcs}.")
|
126
131
|
end
|
127
132
|
|
128
|
-
desc 'Enqueue DELETE jobs for ALL defined
|
129
|
-
task :delete_all, %i[cascade yes] => :environment do |_t, args|
|
133
|
+
desc 'Enqueue DELETE jobs for ALL defined materialised views'
|
134
|
+
task :delete_all, %i[cascade row_count_strategy yes] => :environment do |_t, args|
|
135
|
+
rcs = helpers.parse_row_count_strategy(args[:row_count_strategy])
|
130
136
|
cascade = helpers.parse_cascade?(args[:cascade])
|
131
137
|
skip = helpers.skip_confirm?(args[:yes])
|
132
138
|
|
@@ -137,9 +143,9 @@ namespace :mat_views do
|
|
137
143
|
next
|
138
144
|
end
|
139
145
|
|
140
|
-
helpers.confirm!("Enqueue DELETE for ALL (#{count}) views, cascade=#{cascade}", skip: skip)
|
141
|
-
scope.find_each { |defn| helpers.enqueue_delete
|
142
|
-
helpers.logger.info("[mat_views] Enqueued #{count} DeleteViewJob(s), cascade=#{cascade}.")
|
146
|
+
helpers.confirm!("Enqueue DELETE for ALL (#{count}) views, cascade=#{cascade}, row_count_strategy=#{rcs}", skip: skip)
|
147
|
+
scope.find_each { |defn| helpers.enqueue_delete(defn.id, cascade, rcs) }
|
148
|
+
helpers.logger.info("[mat_views] Enqueued #{count} DeleteViewJob(s), cascade=#{cascade}, row_count_strategy=#{rcs}.")
|
143
149
|
end
|
144
150
|
end
|
145
151
|
# rubocop:enable Metrics/BlockLength
|