mat_views 0.2.0 → 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 +4 -4
- 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 +2 -2
- data/app/jobs/mat_views/create_view_job.rb +9 -8
- data/app/jobs/mat_views/delete_view_job.rb +8 -8
- data/app/jobs/mat_views/refresh_view_job.rb +8 -9
- 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 +11 -13
- 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/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
- 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/services/base_service.rb +46 -38
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +9 -6
- data/lib/mat_views/services/create_view.rb +15 -15
- data/lib/mat_views/services/delete_view.rb +8 -11
- data/lib/mat_views/services/regular_refresh.rb +6 -5
- data/lib/mat_views/services/swap_refresh.rb +11 -9
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +10 -4
- data/lib/tasks/helpers.rb +13 -13
- data/lib/tasks/mat_views_tasks.rake +15 -15
- metadata +130 -5
@@ -0,0 +1,93 @@
|
|
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 Admin
|
10
|
+
#
|
11
|
+
# MatViews::Admin::AuthBridge
|
12
|
+
# ---------------------------
|
13
|
+
# Bridge module that wires the MatViews admin engine to a **host-provided**
|
14
|
+
# authentication/authorization layer, while providing safe defaults.
|
15
|
+
#
|
16
|
+
# ### How it works
|
17
|
+
# - Includes {MatViews::Admin::DefaultAuth} first (fallback, no-op/hostable).
|
18
|
+
# - Then includes the **host auth module** returned by {.host_auth_module}.
|
19
|
+
# Because Ruby searches the most recently included module first, the host
|
20
|
+
# module cleanly **overrides** any defaults from `DefaultAuth`.
|
21
|
+
# - Registers a before_action `authenticate_mat_views!`.
|
22
|
+
# - Exposes helpers: `mat_views_current_user` and its alias {#user}.
|
23
|
+
#
|
24
|
+
# ### Host integration options (define one of these):
|
25
|
+
# 1) A top-level module:
|
26
|
+
# ```ruby
|
27
|
+
# # app/lib/mat_views_admin.rb (or any autoloaded path)
|
28
|
+
# module MatViewsAdmin
|
29
|
+
# def authenticate_mat_views!; end
|
30
|
+
# def authorize_mat_views!(*); end
|
31
|
+
# def mat_views_current_user; end
|
32
|
+
# end
|
33
|
+
# ```
|
34
|
+
# 2) A namespaced module:
|
35
|
+
# ```ruby
|
36
|
+
# # app/lib/mat_views/admin/host_auth.rb
|
37
|
+
# module MatViews
|
38
|
+
# module Admin
|
39
|
+
# module HostAuth
|
40
|
+
# def authenticate_mat_views!; end
|
41
|
+
# def authorize_mat_views!(*); end
|
42
|
+
# def mat_views_current_user; end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# If neither module is present, a blank `Module.new` is included and the
|
49
|
+
# defaults in {MatViews::Admin::DefaultAuth} remain in effect.
|
50
|
+
#
|
51
|
+
# @see MatViews::Admin::DefaultAuth
|
52
|
+
#
|
53
|
+
module AuthBridge
|
54
|
+
extend ActiveSupport::Concern
|
55
|
+
|
56
|
+
included do
|
57
|
+
# Include defaults first, so the host module (included below) can override.
|
58
|
+
include MatViews::Admin::DefaultAuth
|
59
|
+
include host_auth_module
|
60
|
+
|
61
|
+
before_action :authenticate_mat_views!
|
62
|
+
helper_method :mat_views_current_user, :user
|
63
|
+
end
|
64
|
+
|
65
|
+
# Convenience alias for `mat_views_current_user` exposed to views.
|
66
|
+
#
|
67
|
+
# @return [Object, nil] the current user object as defined by host auth
|
68
|
+
def user = mat_views_current_user
|
69
|
+
|
70
|
+
class_methods do
|
71
|
+
# Resolves the host's auth module, if any.
|
72
|
+
#
|
73
|
+
# Lookup order:
|
74
|
+
# 1. `::MatViewsAdmin`
|
75
|
+
# 2. `::MatViews::Admin::HostAuth`
|
76
|
+
# 3. Fallback: a blank Module (no overrides)
|
77
|
+
#
|
78
|
+
# @return [Module] the module to include for host auth overrides
|
79
|
+
def host_auth_module
|
80
|
+
if Object.const_defined?('MatViewsAdmin')
|
81
|
+
::MatViewsAdmin
|
82
|
+
elsif Object.const_defined?('MatViews') &&
|
83
|
+
MatViews.const_defined?('Admin') &&
|
84
|
+
MatViews::Admin.const_defined?('HostAuth')
|
85
|
+
::MatViews::Admin::HostAuth
|
86
|
+
else
|
87
|
+
Module.new
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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 Admin
|
10
|
+
# MatViews::Admin::DefaultAuth
|
11
|
+
# ----------------------------
|
12
|
+
# Development-friendly **fallback** authentication/authorization for the MatViews
|
13
|
+
# admin UI. It is included first by {MatViews::Admin::AuthBridge}, and is meant
|
14
|
+
# to be overridden by a host-provided module (`MatViewsAdmin` or
|
15
|
+
# `MatViews::Admin::HostAuth`).
|
16
|
+
#
|
17
|
+
# ❗ **Not for production**: this module allows all access and returns a dummy user.
|
18
|
+
#
|
19
|
+
# @see MatViews::Admin::AuthBridge
|
20
|
+
#
|
21
|
+
module DefaultAuth
|
22
|
+
# Minimal stand-in user object used by the default auth.
|
23
|
+
#
|
24
|
+
# @!attribute [rw] email
|
25
|
+
# @return [String] the email address of the sample user
|
26
|
+
class SampleUser
|
27
|
+
attr_accessor :email
|
28
|
+
|
29
|
+
# @param email [String]
|
30
|
+
def initialize(email) = @email = email
|
31
|
+
|
32
|
+
# @return [String] the user's email
|
33
|
+
def to_s = email
|
34
|
+
end
|
35
|
+
|
36
|
+
# Authenticates the current request.
|
37
|
+
# Always returns true in the default implementation.
|
38
|
+
#
|
39
|
+
# @return [Boolean] true
|
40
|
+
# rubocop:disable Naming/PredicateMethod
|
41
|
+
def authenticate_mat_views! = true
|
42
|
+
# rubocop:enable Naming/PredicateMethod
|
43
|
+
|
44
|
+
# Returns the current user object.
|
45
|
+
# In the default implementation this is a {SampleUser}.
|
46
|
+
#
|
47
|
+
# @return [SampleUser]
|
48
|
+
def mat_views_current_user = SampleUser.new('sample-user@example.com')
|
49
|
+
|
50
|
+
# Authorizes an action on a record.
|
51
|
+
# Always returns true in the default implementation.
|
52
|
+
#
|
53
|
+
# @param _action [Symbol, String] the attempted action (ignored)
|
54
|
+
# @param _record [Object] the target record or symbol (ignored)
|
55
|
+
# @return [Boolean] true
|
56
|
+
# rubocop:disable Naming/PredicateMethod
|
57
|
+
def authorize_mat_views!(_action, _record) = true
|
58
|
+
# rubocop:enable Naming/PredicateMethod
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -17,6 +17,7 @@ module MatViews
|
|
17
17
|
# MatViews.configure do |config|
|
18
18
|
# config.job_adapter = :sidekiq
|
19
19
|
# config.job_queue = :low_priority
|
20
|
+
# config.admin_ui = { row_count_strategy: :estimated }
|
20
21
|
# end
|
21
22
|
#
|
22
23
|
# Supported job adapters:
|
@@ -37,6 +38,11 @@ module MatViews
|
|
37
38
|
# @return [Symbol, String]
|
38
39
|
attr_accessor :job_queue
|
39
40
|
|
41
|
+
##
|
42
|
+
# admin_ui configuration
|
43
|
+
# @return [Hash]
|
44
|
+
attr_accessor :admin_ui
|
45
|
+
|
40
46
|
##
|
41
47
|
# Initialize with defaults.
|
42
48
|
#
|
@@ -44,6 +50,9 @@ module MatViews
|
|
44
50
|
def initialize
|
45
51
|
@job_adapter = :active_job
|
46
52
|
@job_queue = :default
|
53
|
+
@admin_ui = {
|
54
|
+
row_count_strategy: :none
|
55
|
+
}
|
47
56
|
end
|
48
57
|
end
|
49
58
|
end
|
data/lib/mat_views/engine.rb
CHANGED
@@ -5,13 +5,19 @@
|
|
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
|
+
##
|
9
|
+
# MatViews is a Rails engine that provides first-class support for
|
10
|
+
# PostgreSQL materialised views in Rails applications.
|
8
11
|
module MatViews
|
12
|
+
class << self
|
13
|
+
attr_accessor :importmap
|
14
|
+
end
|
9
15
|
##
|
10
16
|
# Rails Engine for MatViews.
|
11
17
|
#
|
12
18
|
# This engine encapsulates all functionality related to
|
13
|
-
#
|
14
|
-
# - Defining
|
19
|
+
# materialised views, including:
|
20
|
+
# - Defining materialised view definitions
|
15
21
|
# - Creating and refreshing views
|
16
22
|
# - Managing background jobs for refresh/create/delete
|
17
23
|
#
|
@@ -30,5 +36,47 @@ module MatViews
|
|
30
36
|
initializer 'mat_views.load_config' do
|
31
37
|
MatViews.configuration ||= MatViews::Configuration.new
|
32
38
|
end
|
39
|
+
|
40
|
+
initializer 'mat_views.javascript' do |app|
|
41
|
+
app.config.assets.paths << root.join('app/javascript')
|
42
|
+
end
|
43
|
+
|
44
|
+
initializer 'mat_views.importmap', before: 'importmap' do |_app|
|
45
|
+
next unless defined?(Importmap)
|
46
|
+
|
47
|
+
MatViews.importmap = Importmap::Map.new
|
48
|
+
MatViews.importmap.draw(root.join('config/importmap.rb'))
|
49
|
+
MatViews.importmap.cache_sweeper(watches: root.join('app/javascript'))
|
50
|
+
|
51
|
+
ActiveSupport.on_load(:action_controller_base) do
|
52
|
+
before_action { MatViews.importmap.cache_sweeper.execute_if_updated }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.locale_code_mapping
|
57
|
+
@locale_code_mapping ||= begin
|
58
|
+
mappings = Dir[root.join('config', 'locales', '*.yml')].map.to_h do |file|
|
59
|
+
code = File.basename(file, '.yml').to_sym
|
60
|
+
name = I18n.t('i18n.name', locale: code)
|
61
|
+
[code, name]
|
62
|
+
end
|
63
|
+
mappings.sort_by { |code, _name| code.to_s }.to_h
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.available_locales
|
68
|
+
@available_locales ||= locale_code_mapping.keys.freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.default_locale = :en
|
72
|
+
def self.loaded_spec = Gem.loaded_specs['mat_views']
|
73
|
+
def self.project_name = loaded_spec&.name
|
74
|
+
def self.project_version = MatViews::VERSION
|
75
|
+
def self.project_homepage = loaded_spec&.homepage
|
76
|
+
def self.company_name = 'Codevedas Inc.'
|
77
|
+
def self.documentation_uri = loaded_spec&.metadata&.[]('documentation_uri')
|
78
|
+
def self.bug_tracker_uri = loaded_spec&.metadata&.[]('bug_tracker_uri')
|
79
|
+
def self.support_uri = loaded_spec&.metadata&.[]('support_uri')
|
80
|
+
def self.rubygems_uri = loaded_spec&.metadata&.[]('rubygems_uri')
|
33
81
|
end
|
34
82
|
end
|
@@ -0,0 +1,43 @@
|
|
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 Helpers
|
10
|
+
module UiTestIds
|
11
|
+
GEM_LINK = 'gem_link'
|
12
|
+
PROJECT_HOMEPAGE_LINK = 'project_homepage_link'
|
13
|
+
OPEN_ISSUE_LINK = 'open_issue_link'
|
14
|
+
DOCUMENTATION_LINK = 'documentation_link'
|
15
|
+
SUPPORT_LINK = 'support_link'
|
16
|
+
|
17
|
+
DRAWER_REFRESH_LINK = 'drawer_refresh_link'
|
18
|
+
DRAWER_CLOSE_LINK = 'drawer_close_link'
|
19
|
+
|
20
|
+
HEADER_LINK = 'header_link'
|
21
|
+
PREFERENCES_LINK = 'preferences_link'
|
22
|
+
|
23
|
+
DEFINITIONS_TAB_LINK = 'definitions_tab_link'
|
24
|
+
RUNS_TAB_LINK = 'runs_tab_link'
|
25
|
+
|
26
|
+
NEW_DEFINITION_LINK = 'new_definition_link'
|
27
|
+
SUBMIT_BUTTON = 'submit_button'
|
28
|
+
CANCEL_BUTTON = 'cancel_button'
|
29
|
+
VIEW_HISTORY_LINK = 'view_history_link'
|
30
|
+
VIEW_LINK = 'view_link'
|
31
|
+
EDIT_LINK = 'edit_link'
|
32
|
+
DELETE_LINK = 'delete_link'
|
33
|
+
REFRESH_LINK = 'refresh_link'
|
34
|
+
DROP_LINK = 'drop_link'
|
35
|
+
DROP_CASCADE_LINK = 'drop_cascade_link'
|
36
|
+
CREATE_MV_LINK = 'create_mv_link'
|
37
|
+
RESET_FILTERS_LINK = 'reset_filters_link'
|
38
|
+
|
39
|
+
PREFERENCES_SAVE_BUTTON = 'preferences_save_button'
|
40
|
+
PREFERENCES_CANCEL_BUTTON = 'preferences_cancel_button'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -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,12 +19,17 @@ 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
|
#
|
@@ -42,7 +47,7 @@ module MatViews
|
|
42
47
|
DEFAULT_NIL_STRATEGY = :none
|
43
48
|
|
44
49
|
##
|
45
|
-
# @return [MatViews::MatViewDefinition] The target
|
50
|
+
# @return [MatViews::MatViewDefinition] The target materialised view definition.
|
46
51
|
attr_reader :definition
|
47
52
|
|
48
53
|
##
|
@@ -61,6 +66,11 @@ module MatViews
|
|
61
66
|
# @return [Hash]
|
62
67
|
attr_accessor :response
|
63
68
|
|
69
|
+
##
|
70
|
+
# wrap in transaction
|
71
|
+
# @return [Boolean]
|
72
|
+
attr_accessor :use_transaction
|
73
|
+
|
64
74
|
##
|
65
75
|
# @param definition [MatViews::MatViewDefinition]
|
66
76
|
# @param row_count_strategy [Symbol, nil] one of `:estimated`, `:exact`, or `nil` (default: `:estimated`)
|
@@ -70,6 +80,7 @@ module MatViews
|
|
70
80
|
@row_count_strategy = extract_row_strategy(row_count_strategy)
|
71
81
|
@request = {}
|
72
82
|
@response = {}
|
83
|
+
@use_transaction = true
|
73
84
|
end
|
74
85
|
|
75
86
|
##
|
@@ -81,19 +92,35 @@ module MatViews
|
|
81
92
|
#
|
82
93
|
# @return [MatViews::ServiceResponse]
|
83
94
|
# @raise [NotImplementedError] if not implemented in subclass
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
95
|
+
def call
|
96
|
+
if use_transaction
|
97
|
+
ActiveRecord::Base.transaction { run_core }
|
98
|
+
else
|
99
|
+
run_core
|
100
|
+
end
|
88
101
|
rescue StandardError => e
|
102
|
+
# finish pending transaction if any
|
103
|
+
# eg: current transaction is aborted, commands ignored until end of transaction block
|
89
104
|
error_response(e)
|
90
105
|
end
|
91
106
|
|
92
107
|
private
|
93
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
|
+
|
94
121
|
##
|
95
122
|
# Assign the request parameters.
|
96
|
-
# Called by {#
|
123
|
+
# Called by {#call} before {#prepare}.
|
97
124
|
#
|
98
125
|
# Must be implemented in concrete subclasses.
|
99
126
|
#
|
@@ -107,7 +134,7 @@ module MatViews
|
|
107
134
|
|
108
135
|
##
|
109
136
|
# Perform pre-flight checks.
|
110
|
-
# Called by {#
|
137
|
+
# Called by {#call} after {#assign_request}.
|
111
138
|
#
|
112
139
|
# Must be implemented in concrete subclasses.
|
113
140
|
#
|
@@ -122,7 +149,7 @@ module MatViews
|
|
122
149
|
|
123
150
|
##
|
124
151
|
# Execute the service operation.
|
125
|
-
# Called by {#
|
152
|
+
# Called by {#call} after {#prepare}.
|
126
153
|
#
|
127
154
|
# Must be implemented in concrete subclasses.
|
128
155
|
#
|
@@ -197,7 +224,7 @@ module MatViews
|
|
197
224
|
# ────────────────────────────────────────────────────────────────
|
198
225
|
|
199
226
|
##
|
200
|
-
# Whether the
|
227
|
+
# Whether the materialised view exists for the resolved `schema` and `rel`.
|
201
228
|
#
|
202
229
|
# @api private
|
203
230
|
# @return [Boolean]
|
@@ -222,7 +249,7 @@ module MatViews
|
|
222
249
|
end
|
223
250
|
|
224
251
|
##
|
225
|
-
# Drop the
|
252
|
+
# Drop the materialised view if it exists (idempotent).
|
226
253
|
#
|
227
254
|
# @api private
|
228
255
|
# @return [void]
|
@@ -376,26 +403,7 @@ module MatViews
|
|
376
403
|
end
|
377
404
|
|
378
405
|
##
|
379
|
-
#
|
380
|
-
#
|
381
|
-
# @api private
|
382
|
-
# @return [Boolean]
|
383
|
-
#
|
384
|
-
def valid_sql?
|
385
|
-
definition.sql.to_s.strip.upcase.start_with?('SELECT')
|
386
|
-
end
|
387
|
-
|
388
|
-
##
|
389
|
-
# Validate that the view name is a sane PostgreSQL identifier.
|
390
|
-
#
|
391
|
-
# @api private
|
392
|
-
# @return [Boolean]
|
393
|
-
def valid_name?
|
394
|
-
/\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(definition.name.to_s)
|
395
|
-
end
|
396
|
-
|
397
|
-
##
|
398
|
-
# Check for any UNIQUE index on the materialized view, required by CONCURRENTLY.
|
406
|
+
# Check for any UNIQUE index on the materialised view, required by CONCURRENTLY.
|
399
407
|
#
|
400
408
|
# @api private
|
401
409
|
# @return [Boolean]
|
@@ -450,7 +458,7 @@ module MatViews
|
|
450
458
|
end
|
451
459
|
|
452
460
|
##
|
453
|
-
# Accurate row count using `COUNT(*)` on the
|
461
|
+
# Accurate row count using `COUNT(*)` on the materialised view.
|
454
462
|
#
|
455
463
|
# @api private
|
456
464
|
# @return [Integer]
|
@@ -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,7 +13,7 @@ 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
|
16
|
+
# UNIQUE index** on the materialised view (a PostgreSQL constraint).
|
17
17
|
#
|
18
18
|
# Options:
|
19
19
|
# - `row_count_strategy:` (Symbol, default: :none) → one of `:estimated`, `:exact`, or `:none or nil` to control row count reporting
|
@@ -25,7 +25,7 @@ module MatViews
|
|
25
25
|
#
|
26
26
|
# @example Direct usage
|
27
27
|
# svc = MatViews::Services::ConcurrentRefresh.new(definition, **options)
|
28
|
-
# response = svc.
|
28
|
+
# response = svc.call
|
29
29
|
# response.success? # => true/false
|
30
30
|
#
|
31
31
|
# @example via job, this is the typical usage and will create a run record in the DB
|
@@ -33,6 +33,11 @@ module MatViews
|
|
33
33
|
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::RefreshViewJob, definition.id, **options)
|
34
34
|
#
|
35
35
|
class ConcurrentRefresh < BaseService
|
36
|
+
def initialize(definition, row_count_strategy: :estimated)
|
37
|
+
super
|
38
|
+
@use_transaction = false
|
39
|
+
end
|
40
|
+
|
36
41
|
private
|
37
42
|
|
38
43
|
##
|
@@ -61,7 +66,7 @@ module MatViews
|
|
61
66
|
|
62
67
|
##
|
63
68
|
# Assign the request parameters.
|
64
|
-
# Called by {#
|
69
|
+
# Called by {#call} before {#prepare}.
|
65
70
|
# Sets `concurrent: true` in the request hash.
|
66
71
|
#
|
67
72
|
# @api private
|
@@ -72,15 +77,13 @@ module MatViews
|
|
72
77
|
end
|
73
78
|
|
74
79
|
##
|
75
|
-
#
|
76
|
-
# Called by {#run} after {#assign_request}.
|
80
|
+
# Validation step (invoked by BaseService#call before execution).
|
77
81
|
#
|
78
82
|
# @api private
|
79
83
|
# @return [nil] on success
|
80
84
|
# @raise [StandardError] on failure
|
81
85
|
#
|
82
86
|
def prepare
|
83
|
-
raise_err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
|
84
87
|
raise_err("Materialized view #{schema}.#{rel} does not exist") unless view_exists?
|
85
88
|
raise_err("Materialized view #{schema}.#{rel} must have a unique index for concurrent refresh") unless unique_index_exists?
|
86
89
|
|
@@ -8,7 +8,7 @@
|
|
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
|
@@ -25,12 +25,12 @@ module MatViews
|
|
25
25
|
#
|
26
26
|
# @example Create a new matview (no force)
|
27
27
|
# svc = MatViews::Services::CreateView.new(defn, **options)
|
28
|
-
# response = svc.
|
28
|
+
# response = svc.call
|
29
29
|
# response.status # => :created or :skipped
|
30
30
|
#
|
31
31
|
# @example Force recreate an existing matview
|
32
32
|
# svc = MatViews::Services::CreateView.new(defn, force: true)
|
33
|
-
# svc.
|
33
|
+
# svc.call
|
34
34
|
#
|
35
35
|
# @example via job, this is the typical usage and will create a run record in the DB
|
36
36
|
# MatViews::Jobs::Adapter.enqueue(MatViews::Services::CreateViewJob, definition.id, **options)
|
@@ -53,7 +53,11 @@ module MatViews
|
|
53
53
|
# - `nil` → skip row count
|
54
54
|
def initialize(definition, force: false, row_count_strategy: :estimated)
|
55
55
|
super(definition, row_count_strategy: row_count_strategy)
|
56
|
-
@force =
|
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?
|
57
61
|
end
|
58
62
|
|
59
63
|
private
|
@@ -63,7 +67,7 @@ module MatViews
|
|
63
67
|
#
|
64
68
|
# - Validates name, SQL, and concurrent-index requirements.
|
65
69
|
# - Handles existing view: skipped (default) or drop+recreate (`force: true`).
|
66
|
-
# - Creates the
|
70
|
+
# - Creates the materialised view WITH DATA.
|
67
71
|
# - Creates a UNIQUE index if refresh strategy is concurrent.
|
68
72
|
#
|
69
73
|
# @api private
|
@@ -94,22 +98,18 @@ module MatViews
|
|
94
98
|
end
|
95
99
|
|
96
100
|
##
|
97
|
-
#
|
101
|
+
# Validation step (invoked by BaseService#call before execution).
|
102
|
+
# Empty for this service as no other preparation is needed.
|
98
103
|
#
|
99
104
|
# @api private
|
100
|
-
# @return [MatViews::ServiceResponse, nil] error response or nil if OK
|
101
105
|
#
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
raise_err('refresh_strategy=concurrent requires unique_index_columns (non-empty)') if strategy == 'concurrent' && cols.empty?
|
106
|
-
|
107
|
-
nil
|
108
|
-
end
|
106
|
+
# @return [void]
|
107
|
+
#
|
108
|
+
def prepare; end
|
109
109
|
|
110
110
|
##
|
111
111
|
# Assign the request parameters.
|
112
|
-
# Called by {#
|
112
|
+
# Called by {#call} before {#prepare}.
|
113
113
|
#
|
114
114
|
# @api private
|
115
115
|
# @return [void]
|