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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  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 +2 -2
  34. data/app/jobs/mat_views/create_view_job.rb +9 -8
  35. data/app/jobs/mat_views/delete_view_job.rb +8 -8
  36. data/app/jobs/mat_views/refresh_view_job.rb +8 -9
  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 +11 -13
  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/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
  115. data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
  116. data/lib/mat_views/admin/auth_bridge.rb +93 -0
  117. data/lib/mat_views/admin/default_auth.rb +61 -0
  118. data/lib/mat_views/configuration.rb +9 -0
  119. data/lib/mat_views/engine.rb +50 -2
  120. data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
  121. data/lib/mat_views/services/base_service.rb +46 -38
  122. data/lib/mat_views/services/check_matview_exists.rb +76 -0
  123. data/lib/mat_views/services/concurrent_refresh.rb +9 -6
  124. data/lib/mat_views/services/create_view.rb +15 -15
  125. data/lib/mat_views/services/delete_view.rb +8 -11
  126. data/lib/mat_views/services/regular_refresh.rb +6 -5
  127. data/lib/mat_views/services/swap_refresh.rb +11 -9
  128. data/lib/mat_views/version.rb +1 -1
  129. data/lib/mat_views.rb +10 -4
  130. data/lib/tasks/helpers.rb +13 -13
  131. data/lib/tasks/mat_views_tasks.rake +15 -15
  132. 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
@@ -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
- # materialized views, including:
14
- # - Defining materialized view definitions
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 materialized
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
- # def run
23
- # return err("missing view") unless view_exists?
24
- # # perform work...
25
- # ok(:updated, payload: { view: "#{schema}.#{rel}" })
26
- # rescue => e
27
- # error_response(e, meta: { op: "my_service" })
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 materialized view definition.
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 run
85
- assign_request
86
- prepare
87
- _run
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 {#run} before {#prepare}.
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 {#run} after {#assign_request}.
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 {#run} after {#prepare}.
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 materialized view exists for the resolved `schema` and `rel`.
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 materialized view if it exists (idempotent).
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
- # Validate SQL starts with SELECT.
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 materialized view.
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 materialized view (a PostgreSQL constraint).
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.run
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 {#run} before {#prepare}.
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
- # Perform pre-flight checks.
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 materialized views.
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.run
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.run
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 = !!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 materialized view WITH DATA.
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
- # Validate name, SQL, and concurrent strategy requirements.
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
- def prepare
103
- raise_err("Invalid view name format: #{definition.name.inspect}") unless valid_name?
104
- raise_err('SQL must start with SELECT') unless valid_sql?
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 {#run} before {#prepare}.
112
+ # Called by {#call} before {#prepare}.
113
113
  #
114
114
  # @api private
115
115
  # @return [void]