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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
  4. data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
  5. data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
  6. data/app/assets/images/mat_views/favicon-16x16.png +0 -0
  7. data/app/assets/images/mat_views/favicon-32x32.png +0 -0
  8. data/app/assets/images/mat_views/favicon-48x48.png +0 -0
  9. data/app/assets/images/mat_views/favicon.ico +0 -0
  10. data/app/assets/images/mat_views/favicon.svg +18 -0
  11. data/app/assets/images/mat_views/logo.svg +18 -0
  12. data/app/assets/images/mat_views/mask-icon.svg +5 -0
  13. data/app/assets/stylesheets/mat_views/application.css +323 -12
  14. data/app/controllers/mat_views/admin/application_controller.rb +135 -0
  15. data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
  16. data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
  17. data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
  18. data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
  19. data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
  20. data/app/javascript/mat_views/application.js +8 -0
  21. data/app/javascript/mat_views/controllers/application.js +10 -0
  22. data/app/javascript/mat_views/controllers/details_controller.js +122 -0
  23. data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
  24. data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
  25. data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
  26. data/app/javascript/mat_views/controllers/index.js +10 -0
  27. data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
  28. data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
  29. data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
  30. data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
  31. data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
  32. data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
  33. data/app/jobs/mat_views/application_job.rb +107 -2
  34. data/app/jobs/mat_views/create_view_job.rb +21 -122
  35. data/app/jobs/mat_views/delete_view_job.rb +22 -129
  36. data/app/jobs/mat_views/refresh_view_job.rb +12 -133
  37. data/app/models/concerns/mat_views_i18n.rb +139 -0
  38. data/app/models/mat_views/application_record.rb +1 -0
  39. data/app/models/mat_views/mat_view_definition.rb +12 -7
  40. data/app/models/mat_views/mat_view_run.rb +34 -16
  41. data/app/views/layouts/mat_views/_footer.html.erb +41 -0
  42. data/app/views/layouts/mat_views/_header.html.erb +25 -0
  43. data/app/views/layouts/mat_views/admin.html.erb +47 -0
  44. data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
  45. data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
  46. data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  47. data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
  48. data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
  49. data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
  50. data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
  51. data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
  52. data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
  53. data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
  54. data/app/views/mat_views/admin/runs/index.html.erb +38 -0
  55. data/app/views/mat_views/admin/runs/show.html.erb +64 -0
  56. data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
  57. data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
  58. data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
  59. data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
  60. data/config/importmap.rb +9 -0
  61. data/config/locales/en-AU-ocker.yml +187 -0
  62. data/config/locales/en-AU.yml +187 -0
  63. data/config/locales/en-BB.yml +187 -0
  64. data/config/locales/en-BD.yml +187 -0
  65. data/config/locales/en-BE.yml +187 -0
  66. data/config/locales/en-BORK.yml +187 -0
  67. data/config/locales/en-BS.yml +187 -0
  68. data/config/locales/en-BZ.yml +187 -0
  69. data/config/locales/en-CA.yml +187 -0
  70. data/config/locales/en-CM.yml +187 -0
  71. data/config/locales/en-CY.yml +187 -0
  72. data/config/locales/en-EG.yml +187 -0
  73. data/config/locales/en-FJ.yml +187 -0
  74. data/config/locales/en-GB.yml +187 -0
  75. data/config/locales/en-GH.yml +187 -0
  76. data/config/locales/en-GI.yml +187 -0
  77. data/config/locales/en-GM.yml +187 -0
  78. data/config/locales/en-GY.yml +187 -0
  79. data/config/locales/en-HK.yml +187 -0
  80. data/config/locales/en-IE.yml +187 -0
  81. data/config/locales/en-IN.yml +187 -0
  82. data/config/locales/en-JM.yml +187 -0
  83. data/config/locales/en-KE.yml +187 -0
  84. data/config/locales/en-LK.yml +187 -0
  85. data/config/locales/en-LOL.yml +187 -0
  86. data/config/locales/en-LR.yml +187 -0
  87. data/config/locales/en-MS.yml +187 -0
  88. data/config/locales/en-MT.yml +187 -0
  89. data/config/locales/en-MW.yml +187 -0
  90. data/config/locales/en-MY.yml +187 -0
  91. data/config/locales/en-NG.yml +187 -0
  92. data/config/locales/en-NP.yml +187 -0
  93. data/config/locales/en-NZ.yml +187 -0
  94. data/config/locales/en-PG.yml +187 -0
  95. data/config/locales/en-PH.yml +187 -0
  96. data/config/locales/en-PK.yml +187 -0
  97. data/config/locales/en-RW.yml +187 -0
  98. data/config/locales/en-SCOT.yml +187 -0
  99. data/config/locales/en-SG.yml +187 -0
  100. data/config/locales/en-SHAKESPEARE.yml +187 -0
  101. data/config/locales/en-SL.yml +187 -0
  102. data/config/locales/en-SS.yml +187 -0
  103. data/config/locales/en-TH.yml +187 -0
  104. data/config/locales/en-TT.yml +187 -0
  105. data/config/locales/en-TZ.yml +187 -0
  106. data/config/locales/en-UG.yml +187 -0
  107. data/config/locales/en-US-pirate.yml +187 -0
  108. data/config/locales/en-US.yml +187 -0
  109. data/config/locales/en-YODA.yml +187 -0
  110. data/config/locales/en-ZA.yml +187 -0
  111. data/config/locales/en-ZW.yml +187 -0
  112. data/config/locales/en.yml +187 -0
  113. data/config/routes.rb +27 -3
  114. data/lib/ext/exception.rb +20 -0
  115. data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
  116. data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +7 -7
  117. data/lib/mat_views/admin/auth_bridge.rb +93 -0
  118. data/lib/mat_views/admin/default_auth.rb +61 -0
  119. data/lib/mat_views/configuration.rb +9 -0
  120. data/lib/mat_views/engine.rb +50 -2
  121. data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
  122. data/lib/mat_views/jobs/adapter.rb +8 -5
  123. data/lib/mat_views/service_response.rb +30 -15
  124. data/lib/mat_views/services/base_service.rb +204 -41
  125. data/lib/mat_views/services/check_matview_exists.rb +76 -0
  126. data/lib/mat_views/services/concurrent_refresh.rb +38 -121
  127. data/lib/mat_views/services/create_view.rb +72 -55
  128. data/lib/mat_views/services/delete_view.rb +46 -95
  129. data/lib/mat_views/services/regular_refresh.rb +38 -94
  130. data/lib/mat_views/services/swap_refresh.rb +83 -123
  131. data/lib/mat_views/version.rb +1 -1
  132. data/lib/mat_views.rb +13 -6
  133. data/lib/tasks/helpers.rb +27 -27
  134. data/lib/tasks/mat_views_tasks.rake +48 -42
  135. 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 materialized view.
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 materialized view and atomically swaps it in. This approach
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
- # Supports optional row count strategies:
25
- # - `:estimated` approximate, using `pg_class.reltuples`
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
- # @return [MatViews::ServiceResponse]
27
+ # Returns a {MatViews::ServiceResponse}
30
28
  #
31
- # @example
32
- # svc = MatViews::Services::SwapRefresh.new(defn, row_count_strategy: :exact)
33
- # svc.run
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
- def run
55
- prep = prepare!
56
- return prep if prep
57
-
58
- create_sql = %(CREATE MATERIALIZED VIEW #{q_tmp} AS #{definition.sql} WITH DATA)
59
- steps = [create_sql]
60
- conn.execute(create_sql)
61
-
62
- steps.concat(swap_index)
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
- private
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
- # internal
83
- # ────────────────────────────────────────────────────────────────
62
+ ok(:updated)
63
+ end
84
64
 
85
65
  ##
86
- # Ensure name validity and existence of original view.
66
+ # Validation step (invoked by BaseService#call before execution).
67
+ # Ensures view exists.
87
68
  #
88
69
  # @api private
89
- # @return [MatViews::ServiceResponse, nil]
90
- def prepare!
91
- return err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
92
- return err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
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 swap_index
103
- steps = []
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
- rename_orig_sql = %(ALTER MATERIALIZED VIEW #{qualified_rel} RENAME TO #{conn.quote_column_name(old_rel)})
106
- steps << rename_orig_sql
107
- conn.execute(rename_orig_sql)
104
+ steps.each { |step| conn.execute(step) }
105
+ end
108
106
 
109
- rename_tmp_sql = %(ALTER MATERIALIZED VIEW #{q_tmp} RENAME TO #{conn.quote_column_name(rel)})
110
- steps << rename_tmp_sql
111
- conn.execute(rename_tmp_sql)
107
+ # prepend the create step
108
+ steps.unshift(create_temp_view_sql)
109
+ steps
110
+ end
112
111
 
113
- drop_old_sql = %(DROP MATERIALIZED VIEW #{q_old})
114
- steps << drop_old_sql
115
- conn.execute(drop_old_sql)
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
- recreate_declared_unique_indexes!(schema:, rel:, steps:)
118
- end
119
- steps
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 materialized view name.
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 materialized view name.
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
- # @param schema [String]
163
- # @param rel [String]
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 { |c| conn.quote_column_name(c) }.join(', ')
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
- sql = %(CREATE UNIQUE INDEX #{idx_name} ON #{q_rel} (#{quoted_cols}))
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
@@ -17,5 +17,5 @@ module MatViews
17
17
  # - PATCH: Backwards-compatible bug fixes
18
18
  #
19
19
  # @return [String] the current gem version
20
- VERSION = '0.1.2'
20
+ VERSION = '0.3.0'
21
21
  end
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 materialized views in Rails applications.
27
+ # PostgreSQL materialised views in Rails applications.
23
28
  #
24
29
  # Features include:
25
- # - Declarative definitions for materialized views
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 materialized views consistently.
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
- attr_accessor :configuration
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 = :materialized
55
+ # config.job_queue = :materialised
51
56
  # end
52
57
  def configure
53
- self.configuration ||= Configuration.new
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 materialized view definitions
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 :estimated if blank.
54
+ # Defaults to :none if blank.
55
55
  def parse_row_count_strategy(arg)
56
- s = (arg || ENV.fetch('ROW_COUNT_STRATEGY', nil)).to_s.strip
57
- return :estimated if s.empty?
56
+ str = (arg || ENV.fetch('ROW_COUNT_STRATEGY', nil)).to_s.strip
57
+ return :none if str.empty?
58
58
 
59
- s.to_sym
59
+ str.to_sym
60
60
  end
61
61
 
62
- # Check if a materialized view exists in schema.
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
- raise 'view_name is required' if raw_name.nil? || raw_name.to_s.strip.empty?
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 raw_name.to_s.include?('.')
85
- parts = raw_name.to_s.split('.', 2)
85
+ if raw_name_string.include?('.')
86
+ parts = raw_name_string.split('.', 2)
86
87
  [parts[0], parts[1]]
87
88
  else
88
- [nil, raw_name.to_s]
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} confirmation skipped.")
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 definition_id [Integer] MatViewDefinition ID
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!(definition_id, force)
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: q,
135
- args: [definition_id, force]
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 definition_id [Integer] MatViewDefinition ID
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!(definition_id, row_count_strategy)
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: q,
152
- args: [definition_id, row_count_strategy]
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 definition_id [Integer] MatViewDefinition ID
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 materialized view, optionally with CASCADE.
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!(definition_id, cascade)
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: q,
180
- args: [definition_id, cascade]
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!(defn.id, force)
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[definition_id force yes] => :environment do |_t, args|
25
- raise 'mat_views:create_by_id requires a definition_id parameter' if args[:definition_id].to_s.strip.empty?
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[:definition_id])
31
- raise "No MatViews::MatViewDefinition found for id=#{args[:definition_id]}" unless defn
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!(defn.id, force)
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 materialized views'
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!(defn.id, force) }
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!(defn.id, rcs)
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[definition_id row_count_strategy yes] => :environment do |_t, args|
70
- raise 'mat_views:refresh_by_id requires a definition_id parameter' if args[:definition_id].to_s.strip.empty?
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[:definition_id])
76
- raise "No MatViews::MatViewDefinition found for id=#{args[:definition_id]}" unless defn
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!(defn.id, rcs)
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 materialized views'
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!(defn.id, rcs) }
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!(defn.id, cascade)
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[definition_id cascade yes] => :environment do |_t, args|
115
- raise 'mat_views:delete_by_id requires a definition_id parameter' if args[:definition_id].to_s.strip.empty?
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[:definition_id])
121
- raise "No MatViews::MatViewDefinition found for id=#{args[:definition_id]}" unless defn
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!(defn.id, cascade)
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 materialized views'
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!(defn.id, cascade) }
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