mat_views 0.1.2 → 0.2.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 +6 -6
- data/app/jobs/mat_views/application_job.rb +105 -0
- data/app/jobs/mat_views/create_view_job.rb +16 -118
- data/app/jobs/mat_views/delete_view_job.rb +18 -125
- data/app/jobs/mat_views/refresh_view_job.rb +7 -127
- data/app/models/mat_views/mat_view_run.rb +23 -3
- data/lib/ext/exception.rb +20 -0
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +2 -2
- 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 +179 -24
- data/lib/mat_views/services/concurrent_refresh.rb +35 -121
- data/lib/mat_views/services/create_view.rb +64 -47
- data/lib/mat_views/services/delete_view.rb +41 -87
- data/lib/mat_views/services/regular_refresh.rb +35 -92
- data/lib/mat_views/services/swap_refresh.rb +75 -117
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +3 -2
- data/lib/tasks/helpers.rb +19 -19
- data/lib/tasks/mat_views_tasks.rake +35 -29
- metadata +3 -2
@@ -0,0 +1,20 @@
|
|
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
|
+
# Extend the Exception class to add a method for serializing error details.
|
9
|
+
class Exception
|
10
|
+
# Serialize the exception into a hash with message, class, and backtrace.
|
11
|
+
#
|
12
|
+
# @return [Hash] serialized error details
|
13
|
+
def mv_serialize_error
|
14
|
+
{
|
15
|
+
message: message,
|
16
|
+
class: self.class.name,
|
17
|
+
backtrace: Array(backtrace)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
@@ -23,8 +23,8 @@ class CreateMatViewRuns < ActiveRecord::Migration[7.1]
|
|
23
23
|
t.datetime :started_at, comment: 'Timestamp when the operation started'
|
24
24
|
t.datetime :finished_at, comment: 'Timestamp when the operation finished'
|
25
25
|
t.integer :duration_ms, comment: 'Duration of the operation in milliseconds'
|
26
|
-
t.
|
27
|
-
t.jsonb
|
26
|
+
t.jsonb :error, comment: 'Error details if the operation failed. :message, :class, :backtrace'
|
27
|
+
t.jsonb :meta, null: false, default: {}, comment: 'Additional metadata about the run, such as job ID, row count or parameters'
|
28
28
|
|
29
29
|
t.timestamps
|
30
30
|
end
|
@@ -58,19 +58,22 @@ module MatViews
|
|
58
58
|
# @raise [ArgumentError] if the configured adapter is not recognized.
|
59
59
|
#
|
60
60
|
def self.enqueue(job_class, queue:, args: [])
|
61
|
-
|
61
|
+
queue_str = queue.to_s
|
62
|
+
job_adapter = MatViews.configuration.job_adapter
|
63
|
+
|
64
|
+
case job_adapter
|
62
65
|
when :active_job
|
63
|
-
job_class.set(queue:
|
66
|
+
job_class.set(queue: queue_str).perform_later(*args)
|
64
67
|
when :sidekiq
|
65
68
|
Sidekiq::Client.push(
|
66
69
|
'class' => job_class.name,
|
67
|
-
'queue' =>
|
70
|
+
'queue' => queue_str,
|
68
71
|
'args' => args
|
69
72
|
)
|
70
73
|
when :resque
|
71
|
-
Resque.enqueue_to(
|
74
|
+
Resque.enqueue_to(queue_str, job_class, *args)
|
72
75
|
else
|
73
|
-
raise ArgumentError, "Unknown job adapter: #{
|
76
|
+
raise ArgumentError, "Unknown job adapter: #{job_adapter.inspect}"
|
74
77
|
end
|
75
78
|
end
|
76
79
|
end
|
@@ -10,16 +10,19 @@ module MatViews
|
|
10
10
|
# Encapsulates the result of a service operation within MatViews.
|
11
11
|
#
|
12
12
|
# Provides a consistent contract for all services by standardizing:
|
13
|
-
# - `status`: Symbol representing outcome (`:ok`, `:created`, `:updated`,
|
13
|
+
# - `status`: Symbol representing outcome (`:ok`, `:created`, `:updated`,
|
14
14
|
# `:skipped`, `:deleted`, `:error`)
|
15
|
-
# - `
|
16
|
-
# - `
|
17
|
-
# - `
|
15
|
+
# - `request`: Request detailed that service was invoked with
|
16
|
+
# - `response`: Response detailed that service returned, nil with :error status
|
17
|
+
# - `error`: Exception or error message, with :error status
|
18
|
+
# - `message`: String description of the error
|
19
|
+
# - `class`: Exception class name
|
20
|
+
# - `backtrace`: Array of strings
|
18
21
|
#
|
19
22
|
# @example Successful response
|
20
23
|
# MatViews::ServiceResponse.new(
|
21
24
|
# status: :updated,
|
22
|
-
#
|
25
|
+
# response: { ... }
|
23
26
|
# )
|
24
27
|
#
|
25
28
|
# @example Error response
|
@@ -29,32 +32,44 @@ module MatViews
|
|
29
32
|
# )
|
30
33
|
#
|
31
34
|
class ServiceResponse
|
32
|
-
attr_reader :status, :
|
35
|
+
attr_reader :status, :request, :error, :response
|
36
|
+
|
37
|
+
# acceptable status values
|
38
|
+
ACCEPTABLE_STATES = %i[ok created updated skipped deleted error].freeze
|
39
|
+
|
40
|
+
# statuses indicating success
|
41
|
+
OK_STATES = %i[ok created updated skipped deleted].freeze
|
42
|
+
|
43
|
+
# statuses indicating error
|
44
|
+
ERROR_STATES = %i[error].freeze
|
33
45
|
|
34
46
|
# @param status [Symbol] the outcome status
|
35
|
-
# @param
|
47
|
+
# @param request [Hash] request details
|
48
|
+
# @param response [Hash] response details
|
36
49
|
# @param error [Exception, String, nil] error details if applicable
|
37
|
-
|
38
|
-
|
50
|
+
def initialize(status:, request: {}, response: {}, error: nil)
|
51
|
+
raise ArgumentError, 'status is required' unless ACCEPTABLE_STATES.include?(status&.to_sym)
|
52
|
+
raise ArgumentError, 'error must be an Exception object' if error && !error.is_a?(Exception)
|
53
|
+
|
39
54
|
@status = status.to_sym
|
40
|
-
@
|
41
|
-
@
|
42
|
-
@
|
55
|
+
@request = request
|
56
|
+
@response = response
|
57
|
+
@error = error&.mv_serialize_error
|
43
58
|
end
|
44
59
|
|
45
60
|
# @return [Boolean] whether the response represents a success
|
46
61
|
def success?
|
47
|
-
|
62
|
+
OK_STATES.include?(status)
|
48
63
|
end
|
49
64
|
|
50
65
|
# @return [Boolean] whether the response represents an error
|
51
66
|
def error?
|
52
|
-
|
67
|
+
ERROR_STATES.include?(status)
|
53
68
|
end
|
54
69
|
|
55
70
|
# @return [Hash] hash representation of the response
|
56
71
|
def to_h
|
57
|
-
{ status:,
|
72
|
+
{ status:, request:, response:, error: }.compact
|
58
73
|
end
|
59
74
|
end
|
60
75
|
end
|
@@ -29,18 +29,115 @@ module MatViews
|
|
29
29
|
# end
|
30
30
|
#
|
31
31
|
class BaseService
|
32
|
+
# Constant indicating unknown row count
|
33
|
+
UNKNOWN_ROW_COUNT = -1
|
34
|
+
|
35
|
+
# Allowed row count strategies
|
36
|
+
ALLOWED_ROW_STRATEGIES = %i[none estimated exact].freeze
|
37
|
+
|
38
|
+
# Default row count strategy
|
39
|
+
DEFAULT_ROW_STRATEGY = :estimated
|
40
|
+
|
41
|
+
# Default strategy when nil or unrecognized value is given
|
42
|
+
DEFAULT_NIL_STRATEGY = :none
|
43
|
+
|
32
44
|
##
|
33
45
|
# @return [MatViews::MatViewDefinition] The target materialized view definition.
|
34
46
|
attr_reader :definition
|
35
47
|
|
48
|
+
##
|
49
|
+
# Row count strategy (`:estimated`, `:exact`, `nil`).
|
50
|
+
#
|
51
|
+
# @return [Symbol, nil]
|
52
|
+
attr_reader :row_count_strategy
|
53
|
+
|
54
|
+
##
|
55
|
+
# request hash to be returned in service response
|
56
|
+
# @return [Hash]
|
57
|
+
attr_accessor :request
|
58
|
+
|
59
|
+
##
|
60
|
+
# response hash to be returned in service response
|
61
|
+
# @return [Hash]
|
62
|
+
attr_accessor :response
|
63
|
+
|
36
64
|
##
|
37
65
|
# @param definition [MatViews::MatViewDefinition]
|
38
|
-
|
66
|
+
# @param row_count_strategy [Symbol, nil] one of `:estimated`, `:exact`, or `nil` (default: `:estimated`)
|
67
|
+
#
|
68
|
+
def initialize(definition, row_count_strategy: DEFAULT_ROW_STRATEGY)
|
39
69
|
@definition = definition
|
70
|
+
@row_count_strategy = extract_row_strategy(row_count_strategy)
|
71
|
+
@request = {}
|
72
|
+
@response = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Execute the service operation.
|
77
|
+
#
|
78
|
+
# Calls {#assign_request}, {#prepare} and {#_run} in order.
|
79
|
+
#
|
80
|
+
# Concrete subclasses must implement these methods.
|
81
|
+
#
|
82
|
+
# @return [MatViews::ServiceResponse]
|
83
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
84
|
+
def run
|
85
|
+
assign_request
|
86
|
+
prepare
|
87
|
+
_run
|
88
|
+
rescue StandardError => e
|
89
|
+
error_response(e)
|
40
90
|
end
|
41
91
|
|
42
92
|
private
|
43
93
|
|
94
|
+
##
|
95
|
+
# Assign the request parameters.
|
96
|
+
# Called by {#run} before {#prepare}.
|
97
|
+
#
|
98
|
+
# Must be implemented in concrete subclasses.
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
# @return [void]
|
102
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
103
|
+
#
|
104
|
+
def assign_request
|
105
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Perform pre-flight checks.
|
110
|
+
# Called by {#run} after {#assign_request}.
|
111
|
+
#
|
112
|
+
# Must be implemented in concrete subclasses.
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
# @return [nil] on success
|
116
|
+
# @raise [StandardError] on failure
|
117
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
118
|
+
#
|
119
|
+
def prepare
|
120
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Execute the service operation.
|
125
|
+
# Called by {#run} after {#prepare}.
|
126
|
+
#
|
127
|
+
# Must be implemented in concrete subclasses.
|
128
|
+
#
|
129
|
+
# @api private
|
130
|
+
# @return [MatViews::ServiceResponse]
|
131
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
132
|
+
#
|
133
|
+
def _run
|
134
|
+
raise NotImplementedError, "Must implement #{self.class}##{__method__}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def extract_row_strategy(value)
|
138
|
+
ALLOWED_ROW_STRATEGIES.include?(value) ? value : DEFAULT_NIL_STRATEGY
|
139
|
+
end
|
140
|
+
|
44
141
|
# ────────────────────────────────────────────────────────────────
|
45
142
|
# Schema / resolution helpers
|
46
143
|
# ────────────────────────────────────────────────────────────────
|
@@ -56,9 +153,9 @@ module MatViews
|
|
56
153
|
#
|
57
154
|
def first_existing_schema
|
58
155
|
raw_path = conn.schema_search_path.presence || 'public'
|
59
|
-
candidates = raw_path.split(',').filter_map { |
|
156
|
+
candidates = raw_path.split(',').filter_map { |token| resolve_schema_token(token.strip) }
|
60
157
|
candidates << 'public' unless candidates.include?('public')
|
61
|
-
candidates.find { |
|
158
|
+
candidates.find { |schema_str| schema_exists?(schema_str) } || 'public'
|
62
159
|
end
|
63
160
|
|
64
161
|
##
|
@@ -204,42 +301,34 @@ module MatViews
|
|
204
301
|
# Build a success response.
|
205
302
|
#
|
206
303
|
# @api private
|
207
|
-
# @param status [Symbol] e.g., `:ok`, `:created`, `:updated`, `:
|
208
|
-
# @param payload [Hash] optional payload
|
209
|
-
# @param meta [Hash] optional metadata
|
304
|
+
# @param status [Symbol] e.g., `:ok`, `:created`, `:updated`, `:skipped`, `:deleted`
|
210
305
|
# @return [MatViews::ServiceResponse]
|
211
306
|
#
|
212
|
-
def ok(status
|
213
|
-
MatViews::ServiceResponse.new(status
|
307
|
+
def ok(status)
|
308
|
+
MatViews::ServiceResponse.new(status:, request:, response:)
|
214
309
|
end
|
215
310
|
|
216
311
|
##
|
217
|
-
#
|
312
|
+
# Raise a StandardError with the given message.
|
218
313
|
#
|
219
314
|
# @api private
|
220
315
|
# @param msg [String]
|
221
|
-
# @return [
|
316
|
+
# @return [void]
|
317
|
+
# @raise [StandardError] with `msg`
|
222
318
|
#
|
223
|
-
def
|
224
|
-
|
319
|
+
def raise_err(msg)
|
320
|
+
raise StandardError, msg
|
225
321
|
end
|
226
322
|
|
227
323
|
##
|
228
324
|
# Build an error response from an exception, including backtrace.
|
229
325
|
#
|
230
326
|
# @api private
|
231
|
-
# @param
|
232
|
-
# @param payload [Hash]
|
233
|
-
# @param meta [Hash]
|
327
|
+
# @param error [Exception]
|
234
328
|
# @return [MatViews::ServiceResponse]
|
235
329
|
#
|
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
|
-
)
|
330
|
+
def error_response(error)
|
331
|
+
MatViews::ServiceResponse.new(status: :error, error:, request:, response:)
|
243
332
|
end
|
244
333
|
|
245
334
|
# ────────────────────────────────────────────────────────────────
|
@@ -278,9 +367,10 @@ module MatViews
|
|
278
367
|
#
|
279
368
|
def pg_idle?
|
280
369
|
rc = conn.raw_connection
|
281
|
-
|
370
|
+
return true unless rc.respond_to?(:transaction_status)
|
371
|
+
|
282
372
|
# Only use CONCURRENTLY outside any tx/savepoint.
|
283
|
-
|
373
|
+
rc.transaction_status == PG::PQTRANS_IDLE
|
284
374
|
rescue StandardError
|
285
375
|
false
|
286
376
|
end
|
@@ -303,6 +393,71 @@ module MatViews
|
|
303
393
|
def valid_name?
|
304
394
|
/\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(definition.name.to_s)
|
305
395
|
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Check for any UNIQUE index on the materialized view, required by CONCURRENTLY.
|
399
|
+
#
|
400
|
+
# @api private
|
401
|
+
# @return [Boolean]
|
402
|
+
#
|
403
|
+
def unique_index_exists?
|
404
|
+
conn.select_value(<<~SQL).to_i.positive?
|
405
|
+
SELECT COUNT(*)
|
406
|
+
FROM pg_index i
|
407
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
408
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
409
|
+
WHERE n.nspname = #{conn.quote(schema)}
|
410
|
+
AND c.relname = #{conn.quote(rel)}
|
411
|
+
AND i.indisunique = TRUE
|
412
|
+
SQL
|
413
|
+
end
|
414
|
+
|
415
|
+
# ────────────────────────────────────────────────────────────────
|
416
|
+
# rows counting
|
417
|
+
# ────────────────────────────────────────────────────────────────
|
418
|
+
|
419
|
+
##
|
420
|
+
# Compute row count based on the configured strategy.
|
421
|
+
#
|
422
|
+
# @api private
|
423
|
+
# @return [Integer, nil]
|
424
|
+
#
|
425
|
+
def fetch_rows_count
|
426
|
+
case row_count_strategy
|
427
|
+
when :estimated then estimated_rows_count
|
428
|
+
when :exact then exact_rows_count
|
429
|
+
else
|
430
|
+
UNKNOWN_ROW_COUNT
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Fast, approximate row count via `pg_class.reltuples`.
|
436
|
+
#
|
437
|
+
# @api private
|
438
|
+
# @return [Integer]
|
439
|
+
#
|
440
|
+
def estimated_rows_count
|
441
|
+
conn.select_value(<<~SQL).to_i
|
442
|
+
SELECT COALESCE(c.reltuples::bigint, 0)
|
443
|
+
FROM pg_class c
|
444
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
445
|
+
WHERE c.relkind IN ('m','r','p')
|
446
|
+
AND n.nspname = #{conn.quote(schema)}
|
447
|
+
AND c.relname = #{conn.quote(rel)}
|
448
|
+
LIMIT 1
|
449
|
+
SQL
|
450
|
+
end
|
451
|
+
|
452
|
+
##
|
453
|
+
# Accurate row count using `COUNT(*)` on the materialized view.
|
454
|
+
#
|
455
|
+
# @api private
|
456
|
+
# @return [Integer]
|
457
|
+
#
|
458
|
+
def exact_rows_count
|
459
|
+
conn.select_value("SELECT COUNT(*) FROM #{qualified_rel}").to_i
|
460
|
+
end
|
306
461
|
end
|
307
462
|
end
|
308
463
|
end
|
@@ -14,39 +14,26 @@ module MatViews
|
|
14
14
|
#
|
15
15
|
# It keeps the view readable during refresh, but **requires at least one
|
16
16
|
# UNIQUE index** on the materialized view (a PostgreSQL constraint).
|
17
|
-
# Returns a {MatViews::ServiceResponse}.
|
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,
|
27
|
+
# svc = MatViews::Services::ConcurrentRefresh.new(definition, **options)
|
29
28
|
# response = svc.run
|
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
|
-
def initialize(definition, row_count_strategy: :estimated)
|
47
|
-
super(definition)
|
48
|
-
@row_count_strategy = row_count_strategy
|
49
|
-
end
|
36
|
+
private
|
50
37
|
|
51
38
|
##
|
52
39
|
# Execute the concurrent refresh.
|
@@ -55,122 +42,49 @@ module MatViews
|
|
55
42
|
# If validation fails, returns an error {MatViews::ServiceResponse}.
|
56
43
|
#
|
57
44
|
# @return [MatViews::ServiceResponse]
|
58
|
-
# - `status: :updated`
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
64
|
-
|
65
|
-
return prep if prep
|
66
|
-
|
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
|
51
|
+
def _run
|
67
52
|
sql = "REFRESH MATERIALIZED VIEW CONCURRENTLY #{qualified_rel}"
|
53
|
+
self.response = { view: "#{schema}.#{rel}", sql: [sql] }
|
68
54
|
|
55
|
+
response[:row_count_before] = fetch_rows_count
|
69
56
|
conn.execute(sql)
|
57
|
+
response[:row_count_after] = fetch_rows_count
|
70
58
|
|
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
|
59
|
+
ok(:updated)
|
106
60
|
end
|
107
61
|
|
108
|
-
# ────────────────────────────────────────────────────────────────
|
109
|
-
# helpers: validation / schema / pg introspection
|
110
|
-
# (mirrors RegularRefresh for consistency)
|
111
|
-
# ────────────────────────────────────────────────────────────────
|
112
|
-
|
113
62
|
##
|
114
|
-
#
|
63
|
+
# Assign the request parameters.
|
64
|
+
# Called by {#run} before {#prepare}.
|
65
|
+
# Sets `concurrent: true` in the request hash.
|
115
66
|
#
|
116
67
|
# @api private
|
117
|
-
# @return [
|
68
|
+
# @return [void]
|
118
69
|
#
|
119
|
-
def
|
120
|
-
|
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
|
70
|
+
def assign_request
|
71
|
+
self.request = { row_count_strategy: row_count_strategy, concurrent: true }
|
129
72
|
end
|
130
73
|
|
131
|
-
# ────────────────────────────────────────────────────────────────
|
132
|
-
# rows counting (same as RegularRefresh)
|
133
|
-
# ────────────────────────────────────────────────────────────────
|
134
|
-
|
135
74
|
##
|
136
|
-
#
|
137
|
-
#
|
138
|
-
# @api private
|
139
|
-
# @return [Integer, nil]
|
140
|
-
#
|
141
|
-
def fetch_rows_count
|
142
|
-
case row_count_strategy
|
143
|
-
when :estimated then estimated_rows_count
|
144
|
-
when :exact then exact_rows_count
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
##
|
149
|
-
# Fast, approximate row count via `pg_class.reltuples`.
|
75
|
+
# Perform pre-flight checks.
|
76
|
+
# Called by {#run} after {#assign_request}.
|
150
77
|
#
|
151
78
|
# @api private
|
152
|
-
# @return [
|
79
|
+
# @return [nil] on success
|
80
|
+
# @raise [StandardError] on failure
|
153
81
|
#
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
82
|
+
def prepare
|
83
|
+
raise_err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
|
84
|
+
raise_err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
85
|
+
raise_err("Materialized view #{schema}.#{rel} must have a unique index for concurrent refresh") unless unique_index_exists?
|
165
86
|
|
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
|
87
|
+
nil
|
174
88
|
end
|
175
89
|
end
|
176
90
|
end
|