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
@@ -8,25 +8,32 @@
|
|
8
8
|
module MatViews
|
9
9
|
module Services
|
10
10
|
##
|
11
|
-
# Service responsible for creating PostgreSQL
|
11
|
+
# Service responsible for creating PostgreSQL materialised views.
|
12
12
|
#
|
13
13
|
# The service validates the view definition, handles existence checks,
|
14
14
|
# executes `CREATE MATERIALIZED VIEW ... WITH DATA`, and, when the
|
15
15
|
# refresh strategy is `:concurrent`, ensures a supporting UNIQUE index.
|
16
16
|
#
|
17
|
-
#
|
17
|
+
# Options:
|
18
|
+
# - `force:` (Boolean, default: false) → drop and recreate if the view already exists
|
19
|
+
# - `row_count_strategy:` (Symbol, default: :none) → one of `:estimated`, `:exact`, or `:none or nil` to control row count reporting
|
20
|
+
#
|
21
|
+
# Returns a {MatViews::ServiceResponse}
|
18
22
|
#
|
19
23
|
# @see MatViews::Services::RegularRefresh
|
20
24
|
# @see MatViews::Services::ConcurrentRefresh
|
21
25
|
#
|
22
26
|
# @example Create a new matview (no force)
|
23
|
-
# svc = MatViews::Services::CreateView.new(defn)
|
24
|
-
# response = svc.
|
25
|
-
# response.status # => :created or :
|
27
|
+
# svc = MatViews::Services::CreateView.new(defn, **options)
|
28
|
+
# response = svc.call
|
29
|
+
# response.status # => :created or :skipped
|
26
30
|
#
|
27
31
|
# @example Force recreate an existing matview
|
28
32
|
# svc = MatViews::Services::CreateView.new(defn, force: true)
|
29
|
-
# svc.
|
33
|
+
# svc.call
|
34
|
+
#
|
35
|
+
# @example via job, this is the typical usage and will create a run record in the DB
|
36
|
+
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::CreateViewJob, definition.id, **options)
|
30
37
|
#
|
31
38
|
class CreateView < BaseService
|
32
39
|
##
|
@@ -38,91 +45,101 @@ module MatViews
|
|
38
45
|
##
|
39
46
|
# @param definition [MatViews::MatViewDefinition]
|
40
47
|
# @param force [Boolean] Whether to drop+recreate an existing matview.
|
41
|
-
|
42
|
-
|
43
|
-
|
48
|
+
# @param row_count_strategy [Symbol, nil] one of `:estimated`, `:exact`, or `nil` (default: `:estimated`)
|
49
|
+
#
|
50
|
+
# Supports optional row count strategies:
|
51
|
+
# - `:estimated` → approximate, using `pg_class.reltuples`
|
52
|
+
# - `:exact` → accurate, using `COUNT(*)`
|
53
|
+
# - `nil` → skip row count
|
54
|
+
def initialize(definition, force: false, row_count_strategy: :estimated)
|
55
|
+
super(definition, row_count_strategy: row_count_strategy)
|
56
|
+
@force = force
|
57
|
+
# Transactions are disabled if unique_index_columns are present because
|
58
|
+
# PostgreSQL does not allow creating a UNIQUE INDEX CONCURRENTLY inside a transaction block.
|
59
|
+
# If a unique index is required (for concurrent refresh), we must avoid wrapping the operation in a transaction.
|
60
|
+
@use_transaction = definition.unique_index_columns.none?
|
44
61
|
end
|
45
62
|
|
63
|
+
private
|
64
|
+
|
46
65
|
##
|
47
66
|
# Execute the create operation.
|
48
67
|
#
|
49
68
|
# - Validates name, SQL, and concurrent-index requirements.
|
50
|
-
# - Handles existing view:
|
51
|
-
# - Creates the
|
69
|
+
# - Handles existing view: skipped (default) or drop+recreate (`force: true`).
|
70
|
+
# - Creates the materialised view WITH DATA.
|
52
71
|
# - Creates a UNIQUE index if refresh strategy is concurrent.
|
53
72
|
#
|
54
|
-
# @
|
55
|
-
# - `:created` on success (payload includes `view` and `created_indexes`)
|
56
|
-
# - `:noop` if the view already exists and `force: false`
|
57
|
-
# - `:error` if validation or execution fails
|
73
|
+
# @api private
|
58
74
|
#
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
75
|
+
# @return [MatViews::ServiceResponse]
|
76
|
+
# - `status: :created or :skipped` on success, with `response` containing:
|
77
|
+
# - `view` - the qualified view name
|
78
|
+
# - `row_count_before` - if requested and available
|
79
|
+
# - `row_count_after` - if requested and available
|
80
|
+
# - `status: :error` with `error` on failure, with `error` containing:
|
81
|
+
# - serlialized exception class, message, and backtrace in a hash
|
82
|
+
def _run
|
83
|
+
sql = create_with_data_sql
|
84
|
+
self.response = { view: "#{schema}.#{rel}", sql: [sql] }
|
85
|
+
# If exists, either skipped or drop+recreate
|
86
|
+
existed = handle_existing
|
65
87
|
return existed if existed.is_a?(MatViews::ServiceResponse)
|
66
88
|
|
67
|
-
|
68
|
-
|
89
|
+
response[:row_count_before] = UNKNOWN_ROW_COUNT
|
90
|
+
conn.execute(sql)
|
91
|
+
response[:row_count_after] = fetch_rows_count
|
69
92
|
|
70
93
|
# For concurrent strategy, ensure the unique index so future
|
71
94
|
# REFRESH MATERIALIZED VIEW CONCURRENTLY is allowed.
|
72
|
-
|
73
|
-
|
74
|
-
ok(:created, payload: { view: qualified_rel, **index_info })
|
75
|
-
rescue StandardError => e
|
76
|
-
error_response(
|
77
|
-
e,
|
78
|
-
payload: { view: qualified_rel },
|
79
|
-
meta: { sql: sql, force: force }
|
80
|
-
)
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
95
|
+
response.merge!(ensure_unique_index_if_needed)
|
84
96
|
|
85
|
-
|
86
|
-
|
87
|
-
# ────────────────────────────────────────────────────────────────
|
97
|
+
ok(:created)
|
98
|
+
end
|
88
99
|
|
89
100
|
##
|
90
|
-
#
|
101
|
+
# Validation step (invoked by BaseService#call before execution).
|
102
|
+
# Empty for this service as no other preparation is needed.
|
91
103
|
#
|
92
104
|
# @api private
|
93
|
-
# @return [MatViews::ServiceResponse, nil] error response or nil if OK
|
94
105
|
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
return err('refresh_strategy=concurrent requires unique_index_columns (non-empty)') if strategy == 'concurrent' && cols.empty?
|
106
|
+
# @return [void]
|
107
|
+
#
|
108
|
+
def prepare; end
|
99
109
|
|
100
|
-
|
110
|
+
##
|
111
|
+
# Assign the request parameters.
|
112
|
+
# Called by {#call} before {#prepare}.
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
# @return [void]
|
116
|
+
#
|
117
|
+
def assign_request
|
118
|
+
self.request = { row_count_strategy: row_count_strategy, force: }
|
101
119
|
end
|
102
120
|
|
103
121
|
##
|
104
|
-
# Handle existing matview: return
|
122
|
+
# Handle existing matview: return skipped if not forcing, or drop if forcing.
|
105
123
|
#
|
106
124
|
# @api private
|
107
125
|
# @return [MatViews::ServiceResponse, nil]
|
108
126
|
#
|
109
|
-
def handle_existing
|
127
|
+
def handle_existing
|
110
128
|
return nil unless view_exists?
|
111
129
|
|
112
|
-
return
|
130
|
+
return ok(:skipped) unless force
|
113
131
|
|
114
132
|
drop_view
|
115
133
|
nil
|
116
134
|
end
|
117
135
|
|
118
136
|
##
|
119
|
-
#
|
120
|
-
#
|
137
|
+
# SQL for `CREATE MATERIALIZED VIEW ... WITH DATA`.
|
121
138
|
# @api private
|
122
|
-
# @return [
|
139
|
+
# @return [String]
|
123
140
|
#
|
124
|
-
def
|
125
|
-
|
141
|
+
def create_with_data_sql
|
142
|
+
<<~SQL
|
126
143
|
CREATE MATERIALIZED VIEW #{qualified_rel} AS
|
127
144
|
#{sql}
|
128
145
|
WITH DATA
|
@@ -147,9 +164,9 @@ module MatViews
|
|
147
164
|
concurrently = pg_idle?
|
148
165
|
conn.execute(<<~SQL)
|
149
166
|
CREATE UNIQUE INDEX #{'CONCURRENTLY ' if concurrently}#{quote_table_name(idx_name)}
|
150
|
-
ON #{qualified_rel} (#{cols.map { |
|
167
|
+
ON #{qualified_rel} (#{cols.map { |col| quote_column_name(col) }.join(', ')})
|
151
168
|
SQL
|
152
|
-
{ created_indexes: [idx_name] }
|
169
|
+
{ created_indexes: [idx_name], row_count_before: UNKNOWN_ROW_COUNT, row_count_after: fetch_rows_count }
|
153
170
|
end
|
154
171
|
end
|
155
172
|
end
|
@@ -8,26 +8,26 @@
|
|
8
8
|
module MatViews
|
9
9
|
module Services
|
10
10
|
##
|
11
|
-
# Service that safely drops a PostgreSQL
|
11
|
+
# Service that safely drops a PostgreSQL materialised view.
|
12
12
|
#
|
13
13
|
# Options:
|
14
|
-
# - `
|
15
|
-
# - `
|
14
|
+
# - `cascade:` (Boolean, default: false) → drop with CASCADE instead of RESTRICT
|
15
|
+
# - `row_count_strategy:` (Symbol, default: :none) → one of `:estimated`, `:exact`, or `:none or nil` to control row count reporting
|
16
16
|
#
|
17
|
-
# Returns a {MatViews::ServiceResponse}
|
18
|
-
# - `ok(:deleted, ...)` when dropped successfully
|
19
|
-
# - `ok(:skipped, ...)` when absent and `if_exists: true`
|
20
|
-
# - `err("...")` or `error_response(...)` on validation or execution error
|
17
|
+
# Returns a {MatViews::ServiceResponse}
|
21
18
|
#
|
22
19
|
# @see MatViews::DeleteViewJob
|
23
20
|
# @see MatViews::MatViewRun
|
24
21
|
#
|
25
22
|
# @example Drop a view if it exists
|
26
|
-
# svc = MatViews::Services::DeleteView.new(defn)
|
27
|
-
# svc.
|
23
|
+
# svc = MatViews::Services::DeleteView.new(defn, **options)
|
24
|
+
# svc.call
|
28
25
|
#
|
29
26
|
# @example Force drop with CASCADE
|
30
|
-
# MatViews::Services::DeleteView.new(defn, cascade: true).
|
27
|
+
# MatViews::Services::DeleteView.new(defn, cascade: true).call
|
28
|
+
#
|
29
|
+
# @example via job, this is the typical usage and will create a run record in the DB
|
30
|
+
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::DeleteViewJob, definition.id, **options)
|
31
31
|
#
|
32
32
|
class DeleteView < BaseService
|
33
33
|
##
|
@@ -36,122 +36,73 @@ module MatViews
|
|
36
36
|
# @return [Boolean]
|
37
37
|
attr_reader :cascade
|
38
38
|
|
39
|
-
##
|
40
|
-
# Whether to allow idempotent skipping if view is absent (default: true).
|
41
|
-
#
|
42
|
-
# @return [Boolean]
|
43
|
-
attr_reader :if_exists
|
44
|
-
|
45
39
|
##
|
46
40
|
# @param definition [MatViews::MatViewDefinition]
|
47
41
|
# @param cascade [Boolean] drop with CASCADE instead of RESTRICT
|
48
|
-
# @param
|
49
|
-
def initialize(definition, cascade: false,
|
50
|
-
super(definition)
|
51
|
-
@cascade
|
52
|
-
@if_exists = if_exists ? true : false
|
42
|
+
# @param row_count_strategy [Symbol, nil] one of `:estimated`, `:exact`, or `nil` (default: `:estimated`)
|
43
|
+
def initialize(definition, cascade: false, row_count_strategy: :estimated)
|
44
|
+
super(definition, row_count_strategy: row_count_strategy)
|
45
|
+
@cascade = cascade ? true : false
|
53
46
|
end
|
54
47
|
|
48
|
+
private
|
49
|
+
|
55
50
|
##
|
56
51
|
# Run the drop operation.
|
57
52
|
#
|
58
53
|
# Steps:
|
59
|
-
# - Validate name format
|
60
|
-
# -
|
54
|
+
# - Validate name format
|
55
|
+
# - return :skipped if absent
|
61
56
|
# - Execute DROP MATERIALIZED VIEW.
|
62
57
|
#
|
63
|
-
# @return [MatViews::ServiceResponse]
|
64
|
-
#
|
65
|
-
def run
|
66
|
-
prep = prepare!
|
67
|
-
return prep if prep
|
68
|
-
|
69
|
-
res = skip_early_if_absent
|
70
|
-
return res if res
|
71
|
-
|
72
|
-
perform_drop
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
# ────────────────────────────────────────────────────────────────
|
78
|
-
# internal
|
79
|
-
# ────────────────────────────────────────────────────────────────
|
80
|
-
|
81
|
-
##
|
82
|
-
# Execute the DROP MATERIALIZED VIEW statement.
|
83
|
-
#
|
84
|
-
# @api private
|
85
|
-
# @return [MatViews::ServiceResponse]
|
86
|
-
#
|
87
|
-
def perform_drop
|
88
|
-
conn.execute(sql)
|
89
|
-
|
90
|
-
ok(:deleted,
|
91
|
-
payload: { view: "#{schema}.#{rel}" },
|
92
|
-
meta: { sql: sql, cascade: cascade, if_exists: if_exists })
|
93
|
-
rescue ActiveRecord::StatementInvalid => e
|
94
|
-
msg = "#{e.message} — dependencies exist. Use cascade: true to force drop."
|
95
|
-
error_response(
|
96
|
-
e.class.new(msg),
|
97
|
-
meta: { sql: sql, cascade: cascade, if_exists: if_exists },
|
98
|
-
payload: { view: "#{schema}.#{rel}" }
|
99
|
-
)
|
100
|
-
rescue StandardError => e
|
101
|
-
error_response(
|
102
|
-
e,
|
103
|
-
meta: { sql: sql, cascade: cascade, if_exists: if_exists },
|
104
|
-
payload: { view: "#{schema}.#{rel}" }
|
105
|
-
)
|
106
|
-
end
|
107
|
-
|
108
|
-
##
|
109
|
-
# Skip early if view is absent and `if_exists` is true.
|
110
|
-
#
|
111
58
|
# @api private
|
112
|
-
# @return [MatViews::ServiceResponse, nil]
|
113
59
|
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
60
|
+
# @return [MatViews::ServiceResponse]
|
61
|
+
# - `status: :deleted or :skipped` on success, with `response` containing:
|
62
|
+
# - `view` - the qualified view name
|
63
|
+
# - `row_count_before` - if requested and available
|
64
|
+
# - `row_count_after` - if requested and available
|
65
|
+
# - `status: :error` with `error` on failure, with `error` containing:
|
66
|
+
# - serlialized exception class, message, and backtrace in a hash
|
67
|
+
def _run
|
68
|
+
self.response = { view: "#{schema}.#{rel}", sql: [drop_sql] }
|
69
|
+
|
70
|
+
return ok(:skipped) unless view_exists?
|
71
|
+
|
72
|
+
response[:row_count_before] = fetch_rows_count
|
73
|
+
conn.execute(drop_sql)
|
74
|
+
response[:row_count_after] = UNKNOWN_ROW_COUNT # view is gone
|
75
|
+
ok(:deleted)
|
121
76
|
end
|
122
77
|
|
123
78
|
##
|
124
|
-
#
|
79
|
+
# Assign the request parameters.
|
80
|
+
# Called by {#call} before {#prepare}.
|
81
|
+
# Sets `concurrent: true` in the request hash.
|
125
82
|
#
|
126
83
|
# @api private
|
127
|
-
# @return [
|
84
|
+
# @return [void]
|
128
85
|
#
|
129
|
-
def
|
130
|
-
|
86
|
+
def assign_request
|
87
|
+
self.request = { row_count_strategy: row_count_strategy, cascade: cascade }
|
131
88
|
end
|
132
89
|
|
133
90
|
##
|
134
|
-
#
|
91
|
+
# Validation step (invoked by BaseService#call before execution).
|
92
|
+
# Empty for this service as no other preparation is needed.
|
135
93
|
#
|
136
94
|
# @api private
|
137
|
-
# @return [MatViews::ServiceResponse, nil]
|
138
95
|
#
|
139
|
-
|
140
|
-
|
141
|
-
return nil if if_exists # skip hard existence check
|
142
|
-
|
143
|
-
return err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
144
|
-
|
145
|
-
nil
|
146
|
-
end
|
96
|
+
# @return [void]
|
97
|
+
def prepare; end
|
147
98
|
|
148
99
|
##
|
149
|
-
#
|
100
|
+
# Build the SQL DROP statement.
|
150
101
|
#
|
151
102
|
# @api private
|
152
103
|
# @return [String]
|
153
104
|
#
|
154
|
-
def
|
105
|
+
def drop_sql
|
155
106
|
drop_mode = cascade ? ' CASCADE' : ' RESTRICT'
|
156
107
|
%(DROP MATERIALIZED VIEW IF EXISTS #{qualified_rel}#{drop_mode})
|
157
108
|
end
|
@@ -13,32 +13,25 @@ module MatViews
|
|
13
13
|
# This is the safest option for simple or low-frequency updates where
|
14
14
|
# blocking reads during refresh is acceptable.
|
15
15
|
#
|
16
|
-
#
|
17
|
-
# -
|
18
|
-
# - `:exact` → runs `COUNT(*)` (accurate, but potentially slow)
|
19
|
-
# - `nil` → no row count included in payload
|
16
|
+
# Options:
|
17
|
+
# - `row_count_strategy:` (Symbol, default: :none) → one of `:estimated`, `:exact`, or `:none or nil` to control row count reporting
|
20
18
|
#
|
21
|
-
#
|
19
|
+
# Returns a {MatViews::ServiceResponse}
|
22
20
|
#
|
23
|
-
# @
|
24
|
-
#
|
25
|
-
#
|
21
|
+
# @see MatViews::Services::ConcurrentRefresh
|
22
|
+
# @see MatViews::Services::SwapRefresh
|
23
|
+
#
|
24
|
+
# @example Direct usage
|
25
|
+
# svc = MatViews::Services::RegularRefresh.new(definition, **options)
|
26
|
+
# response = svc.call
|
27
|
+
# response.success? # => true/false
|
28
|
+
#
|
29
|
+
# @example via job, this is the typical usage and will create a run record in the DB
|
30
|
+
# When definition.refresh_strategy == "concurrent"
|
31
|
+
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::RegularRefresh, definition.id, **options)
|
26
32
|
#
|
27
33
|
class RegularRefresh < BaseService
|
28
|
-
|
29
|
-
# The row count strategy requested.
|
30
|
-
# One of `:estimated`, `:exact`, `nil`, or unrecognized symbol.
|
31
|
-
#
|
32
|
-
# @return [Symbol, nil]
|
33
|
-
attr_reader :row_count_strategy
|
34
|
-
|
35
|
-
##
|
36
|
-
# @param definition [MatViews::MatViewDefinition]
|
37
|
-
# @param row_count_strategy [Symbol, nil] row counting mode
|
38
|
-
def initialize(definition, row_count_strategy: :estimated)
|
39
|
-
super(definition)
|
40
|
-
@row_count_strategy = row_count_strategy
|
41
|
-
end
|
34
|
+
private
|
42
35
|
|
43
36
|
##
|
44
37
|
# Perform the refresh.
|
@@ -49,97 +42,48 @@ module MatViews
|
|
49
42
|
# - Optionally compute row count.
|
50
43
|
#
|
51
44
|
# @return [MatViews::ServiceResponse]
|
45
|
+
# - `status: :updated` on success, with `response` containing:
|
46
|
+
# - `view` - the qualified view name
|
47
|
+
# - `row_count_before` - if requested and available
|
48
|
+
# - `row_count_after` - if requested and available
|
49
|
+
# - `status: :error` with `error` on failure, with `error` containing:
|
50
|
+
# - serlialized exception class, message, and backtrace in a hash
|
52
51
|
#
|
53
|
-
def
|
54
|
-
prep = prepare!
|
55
|
-
return prep if prep
|
56
|
-
|
52
|
+
def _run
|
57
53
|
sql = "REFRESH MATERIALIZED VIEW #{qualified_rel}"
|
58
54
|
|
59
|
-
|
55
|
+
self.response = { view: "#{schema}.#{rel}", sql: [sql] }
|
60
56
|
|
61
|
-
|
62
|
-
|
57
|
+
response[:row_count_before] = fetch_rows_count
|
58
|
+
conn.execute(sql)
|
59
|
+
response[:row_count_after] = fetch_rows_count
|
63
60
|
|
64
|
-
ok(:updated
|
65
|
-
payload: payload,
|
66
|
-
meta: { sql: sql, row_count_strategy: row_count_strategy })
|
67
|
-
rescue StandardError => e
|
68
|
-
error_response(
|
69
|
-
e,
|
70
|
-
meta: {
|
71
|
-
sql: sql,
|
72
|
-
backtrace: Array(e.backtrace),
|
73
|
-
row_count_strategy: row_count_strategy
|
74
|
-
},
|
75
|
-
payload: { view: "#{schema}.#{rel}" }
|
76
|
-
)
|
61
|
+
ok(:updated)
|
77
62
|
end
|
78
63
|
|
79
|
-
private
|
80
|
-
|
81
|
-
# ────────────────────────────────────────────────────────────────
|
82
|
-
# internal
|
83
|
-
# ────────────────────────────────────────────────────────────────
|
84
|
-
|
85
64
|
##
|
86
|
-
#
|
65
|
+
# Validation step (invoked by BaseService#call before execution).
|
66
|
+
# Ensures view exists.
|
87
67
|
#
|
88
68
|
# @api private
|
89
|
-
# @return [MatViews::ServiceResponse, nil]
|
90
69
|
#
|
91
|
-
|
92
|
-
return err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
|
93
|
-
return err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
94
|
-
|
95
|
-
nil
|
96
|
-
end
|
97
|
-
|
98
|
-
# ────────────────────────────────────────────────────────────────
|
99
|
-
# rows counting
|
100
|
-
# ────────────────────────────────────────────────────────────────
|
101
|
-
|
102
|
-
##
|
103
|
-
# Pick the appropriate row count method.
|
104
|
-
#
|
105
|
-
# @api private
|
106
|
-
# @return [Integer, nil]
|
70
|
+
# @return [void]
|
107
71
|
#
|
108
|
-
def
|
109
|
-
|
110
|
-
when :estimated then estimated_rows_count
|
111
|
-
when :exact then exact_rows_count
|
112
|
-
end
|
113
|
-
end
|
72
|
+
def prepare
|
73
|
+
raise_err "Materialized view #{schema}.#{rel} does not exist" unless view_exists?
|
114
74
|
|
115
|
-
|
116
|
-
# Fast/approx via `pg_class.reltuples`.
|
117
|
-
# Updated by `ANALYZE` and autovacuum.
|
118
|
-
#
|
119
|
-
# @api private
|
120
|
-
# @return [Integer]
|
121
|
-
#
|
122
|
-
def estimated_rows_count
|
123
|
-
conn.select_value(<<~SQL).to_i
|
124
|
-
SELECT COALESCE(c.reltuples::bigint, 0)
|
125
|
-
FROM pg_class c
|
126
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
127
|
-
WHERE c.relkind IN ('m','r','p')
|
128
|
-
AND n.nspname = #{conn.quote(schema)}
|
129
|
-
AND c.relname = #{conn.quote(rel)}
|
130
|
-
LIMIT 1
|
131
|
-
SQL
|
75
|
+
nil
|
132
76
|
end
|
133
77
|
|
134
78
|
##
|
135
|
-
#
|
136
|
-
#
|
79
|
+
# Assign the request parameters.
|
80
|
+
# Called by {#call} before {#prepare}.
|
137
81
|
#
|
138
82
|
# @api private
|
139
|
-
# @return [
|
83
|
+
# @return [void]
|
140
84
|
#
|
141
|
-
def
|
142
|
-
|
85
|
+
def assign_request
|
86
|
+
self.request = { row_count_strategy: row_count_strategy }
|
143
87
|
end
|
144
88
|
end
|
145
89
|
end
|