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,7 +8,7 @@
|
|
8
8
|
module MatViews
|
9
9
|
module Services
|
10
10
|
##
|
11
|
-
# Base class for service objects that operate on PostgreSQL
|
11
|
+
# Base class for service objects that operate on PostgreSQL materialised
|
12
12
|
# views (create/refresh/delete, schema discovery, quoting, and common
|
13
13
|
# response helpers).
|
14
14
|
#
|
@@ -19,28 +19,152 @@ module MatViews
|
|
19
19
|
#
|
20
20
|
# @example Subclassing BaseService
|
21
21
|
# class MyService < MatViews::Services::BaseService
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
22
|
+
# private
|
23
|
+
# def assign_request
|
24
|
+
# # assign @request hash keys
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def prepare
|
28
|
+
# # perform pre-flight checks, raise StandardError on failure
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def _run
|
32
|
+
# # perform the operation, return a MatViews::ServiceResponse
|
28
33
|
# end
|
29
34
|
# end
|
30
35
|
#
|
31
36
|
class BaseService
|
37
|
+
# Constant indicating unknown row count
|
38
|
+
UNKNOWN_ROW_COUNT = -1
|
39
|
+
|
40
|
+
# Allowed row count strategies
|
41
|
+
ALLOWED_ROW_STRATEGIES = %i[none estimated exact].freeze
|
42
|
+
|
43
|
+
# Default row count strategy
|
44
|
+
DEFAULT_ROW_STRATEGY = :estimated
|
45
|
+
|
46
|
+
# Default strategy when nil or unrecognized value is given
|
47
|
+
DEFAULT_NIL_STRATEGY = :none
|
48
|
+
|
32
49
|
##
|
33
|
-
# @return [MatViews::MatViewDefinition] The target
|
50
|
+
# @return [MatViews::MatViewDefinition] The target materialised view definition.
|
34
51
|
attr_reader :definition
|
35
52
|
|
53
|
+
##
|
54
|
+
# Row count strategy (`:estimated`, `:exact`, `nil`).
|
55
|
+
#
|
56
|
+
# @return [Symbol, nil]
|
57
|
+
attr_reader :row_count_strategy
|
58
|
+
|
59
|
+
##
|
60
|
+
# request hash to be returned in service response
|
61
|
+
# @return [Hash]
|
62
|
+
attr_accessor :request
|
63
|
+
|
64
|
+
##
|
65
|
+
# response hash to be returned in service response
|
66
|
+
# @return [Hash]
|
67
|
+
attr_accessor :response
|
68
|
+
|
69
|
+
##
|
70
|
+
# wrap in transaction
|
71
|
+
# @return [Boolean]
|
72
|
+
attr_accessor :use_transaction
|
73
|
+
|
36
74
|
##
|
37
75
|
# @param definition [MatViews::MatViewDefinition]
|
38
|
-
|
76
|
+
# @param row_count_strategy [Symbol, nil] one of `:estimated`, `:exact`, or `nil` (default: `:estimated`)
|
77
|
+
#
|
78
|
+
def initialize(definition, row_count_strategy: DEFAULT_ROW_STRATEGY)
|
39
79
|
@definition = definition
|
80
|
+
@row_count_strategy = extract_row_strategy(row_count_strategy)
|
81
|
+
@request = {}
|
82
|
+
@response = {}
|
83
|
+
@use_transaction = true
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Execute the service operation.
|
88
|
+
#
|
89
|
+
# Calls {#assign_request}, {#prepare} and {#_run} in order.
|
90
|
+
#
|
91
|
+
# Concrete subclasses must implement these methods.
|
92
|
+
#
|
93
|
+
# @return [MatViews::ServiceResponse]
|
94
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
95
|
+
def call
|
96
|
+
if use_transaction
|
97
|
+
ActiveRecord::Base.transaction { run_core }
|
98
|
+
else
|
99
|
+
run_core
|
100
|
+
end
|
101
|
+
rescue StandardError => e
|
102
|
+
# finish pending transaction if any
|
103
|
+
# eg: current transaction is aborted, commands ignored until end of transaction block
|
104
|
+
error_response(e)
|
40
105
|
end
|
41
106
|
|
42
107
|
private
|
43
108
|
|
109
|
+
##
|
110
|
+
# Core run logic without transaction wrapper.
|
111
|
+
# Called by {#call}.
|
112
|
+
#
|
113
|
+
# @api private
|
114
|
+
# @return [MatViews::ServiceResponse]
|
115
|
+
def run_core
|
116
|
+
assign_request
|
117
|
+
prepare
|
118
|
+
_run
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Assign the request parameters.
|
123
|
+
# Called by {#call} before {#prepare}.
|
124
|
+
#
|
125
|
+
# Must be implemented in concrete subclasses.
|
126
|
+
#
|
127
|
+
# @api private
|
128
|
+
# @return [void]
|
129
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
130
|
+
#
|
131
|
+
def assign_request
|
132
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Perform pre-flight checks.
|
137
|
+
# Called by {#call} after {#assign_request}.
|
138
|
+
#
|
139
|
+
# Must be implemented in concrete subclasses.
|
140
|
+
#
|
141
|
+
# @api private
|
142
|
+
# @return [nil] on success
|
143
|
+
# @raise [StandardError] on failure
|
144
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
145
|
+
#
|
146
|
+
def prepare
|
147
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Execute the service operation.
|
152
|
+
# Called by {#call} after {#prepare}.
|
153
|
+
#
|
154
|
+
# Must be implemented in concrete subclasses.
|
155
|
+
#
|
156
|
+
# @api private
|
157
|
+
# @return [MatViews::ServiceResponse]
|
158
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
159
|
+
#
|
160
|
+
def _run
|
161
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def extract_row_strategy(value)
|
165
|
+
ALLOWED_ROW_STRATEGIES.include?(value) ? value : DEFAULT_NIL_STRATEGY
|
166
|
+
end
|
167
|
+
|
44
168
|
# ────────────────────────────────────────────────────────────────
|
45
169
|
# Schema / resolution helpers
|
46
170
|
# ────────────────────────────────────────────────────────────────
|
@@ -56,9 +180,9 @@ module MatViews
|
|
56
180
|
#
|
57
181
|
def first_existing_schema
|
58
182
|
raw_path = conn.schema_search_path.presence || 'public'
|
59
|
-
candidates = raw_path.split(',').filter_map { |
|
183
|
+
candidates = raw_path.split(',').filter_map { |token| resolve_schema_token(token.strip) }
|
60
184
|
candidates << 'public' unless candidates.include?('public')
|
61
|
-
candidates.find { |
|
185
|
+
candidates.find { |schema_str| schema_exists?(schema_str) } || 'public'
|
62
186
|
end
|
63
187
|
|
64
188
|
##
|
@@ -100,7 +224,7 @@ module MatViews
|
|
100
224
|
# ────────────────────────────────────────────────────────────────
|
101
225
|
|
102
226
|
##
|
103
|
-
# Whether the
|
227
|
+
# Whether the materialised view exists for the resolved `schema` and `rel`.
|
104
228
|
#
|
105
229
|
# @api private
|
106
230
|
# @return [Boolean]
|
@@ -125,7 +249,7 @@ module MatViews
|
|
125
249
|
end
|
126
250
|
|
127
251
|
##
|
128
|
-
# Drop the
|
252
|
+
# Drop the materialised view if it exists (idempotent).
|
129
253
|
#
|
130
254
|
# @api private
|
131
255
|
# @return [void]
|
@@ -204,42 +328,34 @@ module MatViews
|
|
204
328
|
# Build a success response.
|
205
329
|
#
|
206
330
|
# @api private
|
207
|
-
# @param status [Symbol] e.g., `:ok`, `:created`, `:updated`, `:
|
208
|
-
# @param payload [Hash] optional payload
|
209
|
-
# @param meta [Hash] optional metadata
|
331
|
+
# @param status [Symbol] e.g., `:ok`, `:created`, `:updated`, `:skipped`, `:deleted`
|
210
332
|
# @return [MatViews::ServiceResponse]
|
211
333
|
#
|
212
|
-
def ok(status
|
213
|
-
MatViews::ServiceResponse.new(status
|
334
|
+
def ok(status)
|
335
|
+
MatViews::ServiceResponse.new(status:, request:, response:)
|
214
336
|
end
|
215
337
|
|
216
338
|
##
|
217
|
-
#
|
339
|
+
# Raise a StandardError with the given message.
|
218
340
|
#
|
219
341
|
# @api private
|
220
342
|
# @param msg [String]
|
221
|
-
# @return [
|
343
|
+
# @return [void]
|
344
|
+
# @raise [StandardError] with `msg`
|
222
345
|
#
|
223
|
-
def
|
224
|
-
|
346
|
+
def raise_err(msg)
|
347
|
+
raise StandardError, msg
|
225
348
|
end
|
226
349
|
|
227
350
|
##
|
228
351
|
# Build an error response from an exception, including backtrace.
|
229
352
|
#
|
230
353
|
# @api private
|
231
|
-
# @param
|
232
|
-
# @param payload [Hash]
|
233
|
-
# @param meta [Hash]
|
354
|
+
# @param error [Exception]
|
234
355
|
# @return [MatViews::ServiceResponse]
|
235
356
|
#
|
236
|
-
def error_response(
|
237
|
-
MatViews::ServiceResponse.new(
|
238
|
-
status: :error,
|
239
|
-
error: "#{exception.class}: #{exception.message}",
|
240
|
-
payload: payload,
|
241
|
-
meta: { backtrace: Array(exception.backtrace), **meta }
|
242
|
-
)
|
357
|
+
def error_response(error)
|
358
|
+
MatViews::ServiceResponse.new(status: :error, error:, request:, response:)
|
243
359
|
end
|
244
360
|
|
245
361
|
# ────────────────────────────────────────────────────────────────
|
@@ -278,30 +394,77 @@ module MatViews
|
|
278
394
|
#
|
279
395
|
def pg_idle?
|
280
396
|
rc = conn.raw_connection
|
281
|
-
|
397
|
+
return true unless rc.respond_to?(:transaction_status)
|
398
|
+
|
282
399
|
# Only use CONCURRENTLY outside any tx/savepoint.
|
283
|
-
|
400
|
+
rc.transaction_status == PG::PQTRANS_IDLE
|
284
401
|
rescue StandardError
|
285
402
|
false
|
286
403
|
end
|
287
404
|
|
288
405
|
##
|
289
|
-
#
|
406
|
+
# Check for any UNIQUE index on the materialised view, required by CONCURRENTLY.
|
290
407
|
#
|
291
408
|
# @api private
|
292
409
|
# @return [Boolean]
|
293
410
|
#
|
294
|
-
def
|
295
|
-
|
411
|
+
def unique_index_exists?
|
412
|
+
conn.select_value(<<~SQL).to_i.positive?
|
413
|
+
SELECT COUNT(*)
|
414
|
+
FROM pg_index i
|
415
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
416
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
417
|
+
WHERE n.nspname = #{conn.quote(schema)}
|
418
|
+
AND c.relname = #{conn.quote(rel)}
|
419
|
+
AND i.indisunique = TRUE
|
420
|
+
SQL
|
296
421
|
end
|
297
422
|
|
423
|
+
# ────────────────────────────────────────────────────────────────
|
424
|
+
# rows counting
|
425
|
+
# ────────────────────────────────────────────────────────────────
|
426
|
+
|
298
427
|
##
|
299
|
-
#
|
428
|
+
# Compute row count based on the configured strategy.
|
300
429
|
#
|
301
430
|
# @api private
|
302
|
-
# @return [
|
303
|
-
|
304
|
-
|
431
|
+
# @return [Integer, nil]
|
432
|
+
#
|
433
|
+
def fetch_rows_count
|
434
|
+
case row_count_strategy
|
435
|
+
when :estimated then estimated_rows_count
|
436
|
+
when :exact then exact_rows_count
|
437
|
+
else
|
438
|
+
UNKNOWN_ROW_COUNT
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
##
|
443
|
+
# Fast, approximate row count via `pg_class.reltuples`.
|
444
|
+
#
|
445
|
+
# @api private
|
446
|
+
# @return [Integer]
|
447
|
+
#
|
448
|
+
def estimated_rows_count
|
449
|
+
conn.select_value(<<~SQL).to_i
|
450
|
+
SELECT COALESCE(c.reltuples::bigint, 0)
|
451
|
+
FROM pg_class c
|
452
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
453
|
+
WHERE c.relkind IN ('m','r','p')
|
454
|
+
AND n.nspname = #{conn.quote(schema)}
|
455
|
+
AND c.relname = #{conn.quote(rel)}
|
456
|
+
LIMIT 1
|
457
|
+
SQL
|
458
|
+
end
|
459
|
+
|
460
|
+
##
|
461
|
+
# Accurate row count using `COUNT(*)` on the materialised view.
|
462
|
+
#
|
463
|
+
# @api private
|
464
|
+
# @return [Integer]
|
465
|
+
#
|
466
|
+
def exact_rows_count
|
467
|
+
conn.select_value("SELECT COUNT(*) FROM #{qualified_rel}").to_i
|
305
468
|
end
|
306
469
|
end
|
307
470
|
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
module Services
|
10
|
+
# MatViews::Services::CheckMatviewExists
|
11
|
+
# --------------------------------------
|
12
|
+
# Service object that checks whether the underlying PostgreSQL **materialised view**
|
13
|
+
# for a given {MatViews::MatViewDefinition} currently exists.
|
14
|
+
#
|
15
|
+
# ### Contract
|
16
|
+
# - Inherits from {MatViews::Services::BaseService}.
|
17
|
+
# - Uses BaseService helpers such as `definition`, `view_exists?`,
|
18
|
+
# `ok`, `raise_err`, and the `request`/`response` accessors.
|
19
|
+
# - The public entrypoint is `#call` (defined in BaseService), which will call the
|
20
|
+
# private lifecycle hooks here: {#prepare}, {#assign_request}, and {#_run}.
|
21
|
+
#
|
22
|
+
# ### Result
|
23
|
+
# - On success: status `:ok`, with `response: { exists: true|false }`.
|
24
|
+
# - On validation failure (bad view name): raises via {BaseService#raise_err}.
|
25
|
+
#
|
26
|
+
# @example Check if a materialised view exists
|
27
|
+
# defn = MatViews::MatViewDefinition.find(1)
|
28
|
+
# res = MatViews::Services::CheckMatviewExists.new(defn).call
|
29
|
+
# if res.success?
|
30
|
+
# puts res.response[:exists] ? "Exists" : "Missing"
|
31
|
+
# else
|
32
|
+
# warn res.error
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @see MatViews::Services::BaseService
|
36
|
+
# @see MatViews::MatViewDefinition
|
37
|
+
#
|
38
|
+
class CheckMatviewExists < BaseService
|
39
|
+
private
|
40
|
+
|
41
|
+
# Core execution step (invoked by BaseService#call).
|
42
|
+
#
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
# Sets {#response} to `{ exists: Boolean }` and marks the service as ok.
|
46
|
+
#
|
47
|
+
# @return [void]
|
48
|
+
def _run
|
49
|
+
self.response = { exists: view_exists? }
|
50
|
+
ok(:ok)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validation step (invoked by BaseService#call before execution).
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
#
|
57
|
+
# Empty for this service as no other preparation is needed.
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
def prepare; end
|
61
|
+
|
62
|
+
# Request initialization (invoked by BaseService#call).
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
#
|
66
|
+
# Establishes a canonical, immutable snapshot of the input request
|
67
|
+
# for logging/inspection purposes. This service does not require inputs,
|
68
|
+
# so it assigns an empty Hash.
|
69
|
+
#
|
70
|
+
# @return [void]
|
71
|
+
def assign_request
|
72
|
+
self.request = {}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -13,41 +13,33 @@ module MatViews
|
|
13
13
|
# `REFRESH MATERIALIZED VIEW CONCURRENTLY <schema>.<rel>`
|
14
14
|
#
|
15
15
|
# It keeps the view readable during refresh, but **requires at least one
|
16
|
-
# UNIQUE index** on the
|
17
|
-
# Returns a {MatViews::ServiceResponse}.
|
16
|
+
# UNIQUE index** on the materialised view (a PostgreSQL constraint).
|
18
17
|
#
|
19
|
-
#
|
20
|
-
# - `:estimated`
|
21
|
-
#
|
22
|
-
#
|
18
|
+
# Options:
|
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}
|
23
22
|
#
|
24
23
|
# @see MatViews::Services::RegularRefresh
|
25
24
|
# @see MatViews::Services::SwapRefresh
|
26
25
|
#
|
27
26
|
# @example Direct usage
|
28
|
-
# svc = MatViews::Services::ConcurrentRefresh.new(definition,
|
29
|
-
# response = svc.
|
27
|
+
# svc = MatViews::Services::ConcurrentRefresh.new(definition, **options)
|
28
|
+
# response = svc.call
|
30
29
|
# response.success? # => true/false
|
31
30
|
#
|
32
|
-
# @example
|
33
|
-
#
|
34
|
-
# MatViews::
|
31
|
+
# @example via job, this is the typical usage and will create a run record in the DB
|
32
|
+
# When definition.refresh_strategy == "concurrent"
|
33
|
+
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::RefreshViewJob, definition.id, **options)
|
35
34
|
#
|
36
35
|
class ConcurrentRefresh < BaseService
|
37
|
-
##
|
38
|
-
# Strategy for computing row count after refresh.
|
39
|
-
#
|
40
|
-
# @return [Symbol, nil] one of `:estimated`, `:exact`, or `nil`
|
41
|
-
attr_reader :row_count_strategy
|
42
|
-
|
43
|
-
##
|
44
|
-
# @param definition [MatViews::MatViewDefinition]
|
45
|
-
# @param row_count_strategy [Symbol, nil] `:estimated` (default), `:exact`, or `nil`
|
46
36
|
def initialize(definition, row_count_strategy: :estimated)
|
47
|
-
super
|
48
|
-
@
|
37
|
+
super
|
38
|
+
@use_transaction = false
|
49
39
|
end
|
50
40
|
|
41
|
+
private
|
42
|
+
|
51
43
|
##
|
52
44
|
# Execute the concurrent refresh.
|
53
45
|
#
|
@@ -55,122 +47,47 @@ module MatViews
|
|
55
47
|
# If validation fails, returns an error {MatViews::ServiceResponse}.
|
56
48
|
#
|
57
49
|
# @return [MatViews::ServiceResponse]
|
58
|
-
# - `status: :updated`
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
64
|
-
|
65
|
-
return prep if prep
|
66
|
-
|
50
|
+
# - `status: :updated` on success, with `response` containing:
|
51
|
+
# - `view` - the qualified view name
|
52
|
+
# - `row_count_before` - if requested and available
|
53
|
+
# - `row_count_after` - if requested and available
|
54
|
+
# - `status: :error` with `error` on failure, with `error` containing:
|
55
|
+
# - serlialized exception class, message, and backtrace in a hash
|
56
|
+
def _run
|
67
57
|
sql = "REFRESH MATERIALIZED VIEW CONCURRENTLY #{qualified_rel}"
|
58
|
+
self.response = { view: "#{schema}.#{rel}", sql: [sql] }
|
68
59
|
|
60
|
+
response[:row_count_before] = fetch_rows_count
|
69
61
|
conn.execute(sql)
|
62
|
+
response[:row_count_after] = fetch_rows_count
|
70
63
|
|
71
|
-
|
72
|
-
payload[:row_count] = fetch_rows_count if row_count_strategy.present?
|
73
|
-
|
74
|
-
ok(:updated,
|
75
|
-
payload: payload,
|
76
|
-
meta: { sql: sql, row_count_strategy: row_count_strategy, concurrent: true })
|
77
|
-
rescue PG::ObjectInUse => e
|
78
|
-
# Common lock/contention error during concurrent refreshes.
|
79
|
-
error_response(e,
|
80
|
-
meta: { sql: sql, row_count_strategy: row_count_strategy, concurrent: true },
|
81
|
-
payload: { view: "#{schema}.#{rel}" })
|
82
|
-
rescue StandardError => e
|
83
|
-
error_response(e,
|
84
|
-
meta: { sql: sql, backtrace: Array(e.backtrace), row_count_strategy: row_count_strategy, concurrent: true },
|
85
|
-
payload: { view: "#{schema}.#{rel}" })
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
# ────────────────────────────────────────────────────────────────
|
91
|
-
# internal
|
92
|
-
# ────────────────────────────────────────────────────────────────
|
93
|
-
|
94
|
-
##
|
95
|
-
# Perform pre-flight checks.
|
96
|
-
#
|
97
|
-
# @api private
|
98
|
-
# @return [MatViews::ServiceResponse, nil] error response or `nil` if OK
|
99
|
-
#
|
100
|
-
def prepare!
|
101
|
-
return err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
|
102
|
-
return err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
103
|
-
return err("Materialized view #{schema}.#{rel} must have a unique index for concurrent refresh") unless unique_index_exists?
|
104
|
-
|
105
|
-
nil
|
106
|
-
end
|
107
|
-
|
108
|
-
# ────────────────────────────────────────────────────────────────
|
109
|
-
# helpers: validation / schema / pg introspection
|
110
|
-
# (mirrors RegularRefresh for consistency)
|
111
|
-
# ────────────────────────────────────────────────────────────────
|
112
|
-
|
113
|
-
##
|
114
|
-
# Check for any UNIQUE index on the materialized view, required by CONCURRENTLY.
|
115
|
-
#
|
116
|
-
# @api private
|
117
|
-
# @return [Boolean]
|
118
|
-
#
|
119
|
-
def unique_index_exists?
|
120
|
-
conn.select_value(<<~SQL).to_i.positive?
|
121
|
-
SELECT COUNT(*)
|
122
|
-
FROM pg_index i
|
123
|
-
JOIN pg_class c ON c.oid = i.indrelid
|
124
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
125
|
-
WHERE n.nspname = #{conn.quote(schema)}
|
126
|
-
AND c.relname = #{conn.quote(rel)}
|
127
|
-
AND i.indisunique = TRUE
|
128
|
-
SQL
|
64
|
+
ok(:updated)
|
129
65
|
end
|
130
66
|
|
131
|
-
# ────────────────────────────────────────────────────────────────
|
132
|
-
# rows counting (same as RegularRefresh)
|
133
|
-
# ────────────────────────────────────────────────────────────────
|
134
|
-
|
135
67
|
##
|
136
|
-
#
|
68
|
+
# Assign the request parameters.
|
69
|
+
# Called by {#call} before {#prepare}.
|
70
|
+
# Sets `concurrent: true` in the request hash.
|
137
71
|
#
|
138
72
|
# @api private
|
139
|
-
# @return [
|
73
|
+
# @return [void]
|
140
74
|
#
|
141
|
-
def
|
142
|
-
|
143
|
-
when :estimated then estimated_rows_count
|
144
|
-
when :exact then exact_rows_count
|
145
|
-
end
|
75
|
+
def assign_request
|
76
|
+
self.request = { row_count_strategy: row_count_strategy, concurrent: true }
|
146
77
|
end
|
147
78
|
|
148
79
|
##
|
149
|
-
#
|
80
|
+
# Validation step (invoked by BaseService#call before execution).
|
150
81
|
#
|
151
82
|
# @api private
|
152
|
-
# @return [
|
83
|
+
# @return [nil] on success
|
84
|
+
# @raise [StandardError] on failure
|
153
85
|
#
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
FROM pg_class c
|
158
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
159
|
-
WHERE c.relkind IN ('m','r','p')
|
160
|
-
AND n.nspname = #{conn.quote(schema)}
|
161
|
-
AND c.relname = #{conn.quote(rel)}
|
162
|
-
LIMIT 1
|
163
|
-
SQL
|
164
|
-
end
|
86
|
+
def prepare
|
87
|
+
raise_err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
88
|
+
raise_err("Materialized view #{schema}.#{rel} must have a unique index for concurrent refresh") unless unique_index_exists?
|
165
89
|
|
166
|
-
|
167
|
-
# Accurate row count using `COUNT(*)` on the materialized view.
|
168
|
-
#
|
169
|
-
# @api private
|
170
|
-
# @return [Integer]
|
171
|
-
#
|
172
|
-
def exact_rows_count
|
173
|
-
conn.select_value("SELECT COUNT(*) FROM #{qualified_rel}").to_i
|
90
|
+
nil
|
174
91
|
end
|
175
92
|
end
|
176
93
|
end
|