smriti 0.5.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/Rakefile +15 -0
- data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
- data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
- data/app/assets/images/smriti/apple-touch-icon.png +0 -0
- data/app/assets/images/smriti/favicon-16x16.png +0 -0
- data/app/assets/images/smriti/favicon-32x32.png +0 -0
- data/app/assets/images/smriti/favicon-48x48.png +0 -0
- data/app/assets/images/smriti/favicon.ico +0 -0
- data/app/assets/images/smriti/favicon.svg +18 -0
- data/app/assets/images/smriti/logo.svg +18 -0
- data/app/assets/images/smriti/mask-icon.svg +5 -0
- data/app/assets/stylesheets/smriti/application.css +1040 -0
- data/app/controllers/smriti/admin/application_controller.rb +135 -0
- data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
- data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
- data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
- data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
- data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
- data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
- data/app/helpers/smriti/admin/ui_helper.rb +539 -0
- data/app/javascript/smriti/application.js +8 -0
- data/app/javascript/smriti/controllers/application.js +10 -0
- data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
- data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
- data/app/javascript/smriti/controllers/details_controller.js +200 -0
- data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
- data/app/javascript/smriti/controllers/flash_controller.js +112 -0
- data/app/javascript/smriti/controllers/index.js +10 -0
- data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
- data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
- data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
- data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
- data/app/jobs/smriti/application_job.rb +144 -0
- data/app/jobs/smriti/create_view_job.rb +87 -0
- data/app/jobs/smriti/delete_view_job.rb +89 -0
- data/app/jobs/smriti/refresh_view_job.rb +94 -0
- data/app/models/concerns/smriti_i18n.rb +139 -0
- data/app/models/concerns/smriti_paginate.rb +70 -0
- data/app/models/concerns/smriti_query_helper.rb +36 -0
- data/app/models/smriti/application_record.rb +39 -0
- data/app/models/smriti/mat_view_definition.rb +254 -0
- data/app/models/smriti/mat_view_run.rb +275 -0
- data/app/views/layouts/smriti/_footer.html.erb +47 -0
- data/app/views/layouts/smriti/_header.html.erb +25 -0
- data/app/views/layouts/smriti/admin.html.erb +47 -0
- data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
- data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
- data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
- data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
- data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
- data/app/views/smriti/admin/preferences/show.html.erb +49 -0
- data/app/views/smriti/admin/ui/_card.html.erb +15 -0
- data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
- data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
- data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
- data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
- data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
- data/app/views/smriti/admin/ui/_details.html.erb +10 -0
- data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
- data/app/views/smriti/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/ar.yml +223 -0
- data/config/locales/de.yml +230 -0
- data/config/locales/en-AU-ocker.yml +223 -0
- data/config/locales/en-AU.yml +202 -0
- data/config/locales/en-BORK.yml +225 -0
- data/config/locales/en-CA.yml +223 -0
- data/config/locales/en-GB.yml +223 -0
- data/config/locales/en-LOL.yml +219 -0
- data/config/locales/en-SCOT.yml +223 -0
- data/config/locales/en-SHAKESPEARE.yml +225 -0
- data/config/locales/en-US-pirate.yml +222 -0
- data/config/locales/en-US.yml +225 -0
- data/config/locales/en-YODA.yml +221 -0
- data/config/locales/en.yml +223 -0
- data/config/locales/es.yml +226 -0
- data/config/locales/fa.yml +223 -0
- data/config/locales/fr-CA.yml +227 -0
- data/config/locales/fr.yml +227 -0
- data/config/locales/he.yml +218 -0
- data/config/locales/hi.yml +223 -0
- data/config/locales/it.yml +225 -0
- data/config/locales/ja-JP.yml +215 -0
- data/config/locales/pt.yml +225 -0
- data/config/locales/ru.yml +228 -0
- data/config/locales/ur.yml +225 -0
- data/config/locales/zh-CN.yml +214 -0
- data/config/locales/zh-TW.yml +214 -0
- data/config/routes.rb +36 -0
- data/lib/ext/exception.rb +20 -0
- data/lib/generators/smriti/install/install_generator.rb +86 -0
- data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
- data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
- data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
- data/lib/smriti/admin/auth_bridge.rb +93 -0
- data/lib/smriti/admin/default_auth.rb +62 -0
- data/lib/smriti/configuration.rb +58 -0
- data/lib/smriti/engine.rb +82 -0
- data/lib/smriti/helpers/ui_test_ids.rb +49 -0
- data/lib/smriti/jobs/adapter.rb +81 -0
- data/lib/smriti/service_response.rb +75 -0
- data/lib/smriti/services/base_service.rb +471 -0
- data/lib/smriti/services/check_matview_exists.rb +76 -0
- data/lib/smriti/services/concurrent_refresh.rb +94 -0
- data/lib/smriti/services/create_view.rb +173 -0
- data/lib/smriti/services/delete_view.rb +111 -0
- data/lib/smriti/services/regular_refresh.rb +90 -0
- data/lib/smriti/services/swap_refresh.rb +181 -0
- data/lib/smriti/version.rb +21 -0
- data/lib/smriti.rb +64 -0
- data/lib/tasks/helpers.rb +185 -0
- data/lib/tasks/smriti_tasks.rake +151 -0
- metadata +206 -0
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
module Smriti
|
|
11
|
+
##
|
|
12
|
+
# ActiveJob that handles `REFRESH MATERIALIZED VIEW` for a given
|
|
13
|
+
# {Smriti::MatViewDefinition}.
|
|
14
|
+
#
|
|
15
|
+
# The job mirrors {Smriti::CreateViewJob}'s lifecycle:
|
|
16
|
+
# it measures duration and persists state in {Smriti::MatViewRun}.
|
|
17
|
+
#
|
|
18
|
+
# The actual refresh implementation is delegated based on
|
|
19
|
+
# `definition.refresh_strategy`:
|
|
20
|
+
#
|
|
21
|
+
# - `"concurrent"` → {Smriti::Services::ConcurrentRefresh}
|
|
22
|
+
# - `"swap"` → {Smriti::Services::SwapRefresh}
|
|
23
|
+
# - otherwise → {Smriti::Services::RegularRefresh}
|
|
24
|
+
#
|
|
25
|
+
# Row count reporting can be controlled via `row_count_strategy`:
|
|
26
|
+
# - `:estimated` (default) - fast, approximate via reltuples
|
|
27
|
+
# - `:exact` - accurate `COUNT(*)`
|
|
28
|
+
# - `nil` - skip counting
|
|
29
|
+
#
|
|
30
|
+
# @see Smriti::MatViewDefinition
|
|
31
|
+
# @see Smriti::MatViewRun
|
|
32
|
+
# @see Smriti::Services::RegularRefresh
|
|
33
|
+
# @see Smriti::Services::ConcurrentRefresh
|
|
34
|
+
# @see Smriti::Services::SwapRefresh
|
|
35
|
+
#
|
|
36
|
+
# @example Enqueue a refresh with exact row count
|
|
37
|
+
# Smriti::RefreshViewJob.perform_later(definition.id, :exact)
|
|
38
|
+
#
|
|
39
|
+
# @example Enqueue using keyword-hash form
|
|
40
|
+
# Smriti::RefreshViewJob.perform_later(definition.id, row_count_strategy: :estimated)
|
|
41
|
+
#
|
|
42
|
+
class RefreshViewJob < ApplicationJob
|
|
43
|
+
##
|
|
44
|
+
# Queue name for the job.
|
|
45
|
+
#
|
|
46
|
+
# Uses `Smriti.configuration.job_queue` when configured, otherwise `:default`.
|
|
47
|
+
#
|
|
48
|
+
queue_as { Smriti.configuration.job_queue || :default }
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Perform the refresh job for the given materialised view definition.
|
|
52
|
+
#
|
|
53
|
+
# @api public
|
|
54
|
+
#
|
|
55
|
+
# @param mat_view_definition_id [Integer, String] ID of {Smriti::MatViewDefinition}.
|
|
56
|
+
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] Serialized {Smriti::ServiceResponse#to_h}:
|
|
59
|
+
# - `:status` [Symbol]
|
|
60
|
+
# - `:error` [String, nil]
|
|
61
|
+
# - `:duration_ms` [Integer]
|
|
62
|
+
# - `:meta` [Hash]
|
|
63
|
+
#
|
|
64
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
|
65
|
+
#
|
|
66
|
+
def perform(mat_view_definition_id, row_count_strategy_arg = nil)
|
|
67
|
+
definition = Smriti::MatViewDefinition.find(mat_view_definition_id)
|
|
68
|
+
record_run(definition, :refresh) do
|
|
69
|
+
service(definition).new(definition, row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Select the refresh service class based on the definition's strategy.
|
|
77
|
+
#
|
|
78
|
+
# @api private
|
|
79
|
+
#
|
|
80
|
+
# @param definition [Smriti::MatViewDefinition]
|
|
81
|
+
# @return [Class] One of the refresh service classes.
|
|
82
|
+
#
|
|
83
|
+
def service(definition)
|
|
84
|
+
case definition.refresh_strategy
|
|
85
|
+
when 'concurrent'
|
|
86
|
+
Smriti::Services::ConcurrentRefresh
|
|
87
|
+
when 'swap'
|
|
88
|
+
Smriti::Services::SwapRefresh
|
|
89
|
+
else
|
|
90
|
+
Smriti::Services::RegularRefresh
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
#
|
|
9
|
+
# SmritiI18n
|
|
10
|
+
# ------------
|
|
11
|
+
# Concern that adds convenient **class-level** helpers for model I18n:
|
|
12
|
+
# - Humanized attribute names
|
|
13
|
+
# - Humanized enum values
|
|
14
|
+
# - Select-friendly enum option arrays
|
|
15
|
+
# - Placeholders and hints for forms
|
|
16
|
+
#
|
|
17
|
+
# These helpers rely on Rails’ standard i18n model keys using the model’s
|
|
18
|
+
# `model_name.i18n_key` (e.g. `Smriti::MatViewDefinition` → `smriti/mat_view_definition`).
|
|
19
|
+
#
|
|
20
|
+
# ## Expected i18n structure (examples)
|
|
21
|
+
#
|
|
22
|
+
# ```yml
|
|
23
|
+
# en-US:
|
|
24
|
+
# activerecord:
|
|
25
|
+
# attributes:
|
|
26
|
+
# smriti/mat_view_definition:
|
|
27
|
+
# name: "View name"
|
|
28
|
+
# sql: "SQL"
|
|
29
|
+
# enums:
|
|
30
|
+
# smriti/mat_view_definition:
|
|
31
|
+
# refresh_strategy:
|
|
32
|
+
# regular: "Regular"
|
|
33
|
+
# concurrent: "Concurrent"
|
|
34
|
+
# swap: "Swap"
|
|
35
|
+
# placeholders:
|
|
36
|
+
# smriti/mat_view_definition:
|
|
37
|
+
# name: "e.g. monthly_sales_mv"
|
|
38
|
+
# hints:
|
|
39
|
+
# smriti/mat_view_definition:
|
|
40
|
+
# sql: "Use a SELECT statement; no trailing semicolon."
|
|
41
|
+
# ```
|
|
42
|
+
#
|
|
43
|
+
# ## Usage
|
|
44
|
+
# ```ruby
|
|
45
|
+
# Smriti::MatViewDefinition.human_name(:name) # => "View name"
|
|
46
|
+
# Smriti::MatViewDefinition.human_enum_name(:refresh_strategy, :regular) # => "Regular"
|
|
47
|
+
# Smriti::MatViewDefinition.human_enum_options(:refresh_strategy)
|
|
48
|
+
# # => [["Regular","regular"], ["Concurrent","concurrent"], ["Swap","swap"]]
|
|
49
|
+
# Smriti::MatViewDefinition.placeholder_for(:name) # => "e.g. monthly_sales_mv"
|
|
50
|
+
# Smriti::MatViewDefinition.hint_for(:sql) # => "Use a SELECT statement..."
|
|
51
|
+
# ```
|
|
52
|
+
#
|
|
53
|
+
# @note Methods are added as **class methods** to the including model.
|
|
54
|
+
#
|
|
55
|
+
# @!method self.human_name(attribute)
|
|
56
|
+
# Humanized (translated) attribute label for this model.
|
|
57
|
+
# Falls back to `attribute.to_s.humanize` when missing.
|
|
58
|
+
# @param attribute [Symbol, String]
|
|
59
|
+
# @return [String]
|
|
60
|
+
#
|
|
61
|
+
# @!method self.human_enum_name(enum_name, enum_value)
|
|
62
|
+
# Humanized (translated) enum value label.
|
|
63
|
+
# Falls back to `enum_value.to_s.humanize` when missing.
|
|
64
|
+
# @param enum_name [Symbol, String] the enum definition name
|
|
65
|
+
# @param enum_value [Symbol, String, Integer] the value/key of the enum
|
|
66
|
+
# @return [String]
|
|
67
|
+
#
|
|
68
|
+
# @!method self.human_enum_options(enum_name)
|
|
69
|
+
# Options array suitable for Rails `options_for_select`.
|
|
70
|
+
# @param enum_name [Symbol, String]
|
|
71
|
+
# @return [Array<Array(String, String)>] each item is `[label, value]`
|
|
72
|
+
#
|
|
73
|
+
# @!method self.placeholder_for(attribute)
|
|
74
|
+
# Form placeholder for the given attribute.
|
|
75
|
+
# Returns empty string if not defined.
|
|
76
|
+
# @param attribute [Symbol, String]
|
|
77
|
+
# @return [String]
|
|
78
|
+
#
|
|
79
|
+
# @!method self.hint_for(attribute)
|
|
80
|
+
# Form hint/help text for the given attribute.
|
|
81
|
+
# Returns empty string if not defined.
|
|
82
|
+
# @param attribute [Symbol, String]
|
|
83
|
+
# @return [String]
|
|
84
|
+
#
|
|
85
|
+
module SmritiI18n
|
|
86
|
+
extend ActiveSupport::Concern
|
|
87
|
+
|
|
88
|
+
class_methods do
|
|
89
|
+
# @return [String]
|
|
90
|
+
def human_name(attribute)
|
|
91
|
+
I18n.t(
|
|
92
|
+
"activerecord.attributes.#{model_name.i18n_key}.#{attribute}",
|
|
93
|
+
default: attribute.to_s.humanize
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# human_enum_name(:refresh_strategy, :regular) → "Regular"
|
|
98
|
+
#
|
|
99
|
+
# @param enum_name [Symbol, String]
|
|
100
|
+
# @param enum_value [Symbol, String, Integer]
|
|
101
|
+
# @return [String]
|
|
102
|
+
def human_enum_name(enum_name, enum_value)
|
|
103
|
+
key = enum_value.to_s
|
|
104
|
+
I18n.t(
|
|
105
|
+
"activerecord.enums.#{model_name.i18n_key}.#{enum_name}.#{key}",
|
|
106
|
+
default: key.humanize
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# human_enum_options(:refresh_strategy)
|
|
111
|
+
# → [["Regular","regular"], ["Concurrent","concurrent"], ["Swap","swap"]]
|
|
112
|
+
#
|
|
113
|
+
# @param enum_name [Symbol, String]
|
|
114
|
+
# @return [Array<Array(String, String)>]
|
|
115
|
+
def human_enum_options(enum_name)
|
|
116
|
+
public_send(enum_name.to_s.pluralize).keys.map do |val|
|
|
117
|
+
[human_enum_name(enum_name, val), val]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param attribute [Symbol, String]
|
|
122
|
+
# @return [String]
|
|
123
|
+
def placeholder_for(attribute)
|
|
124
|
+
I18n.t(
|
|
125
|
+
"activerecord.placeholders.#{model_name.i18n_key}.#{attribute}",
|
|
126
|
+
default: ''
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @param attribute [Symbol, String]
|
|
131
|
+
# @return [String]
|
|
132
|
+
def hint_for(attribute)
|
|
133
|
+
I18n.t(
|
|
134
|
+
"activerecord.hints.#{model_name.i18n_key}.#{attribute}",
|
|
135
|
+
default: ''
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
# SmritiPaginate
|
|
9
|
+
# ----------------
|
|
10
|
+
# Concern that adds a class-level `paginate` scope to models, enabling
|
|
11
|
+
# simple pagination based on `page` and `per_page` parameters.
|
|
12
|
+
#
|
|
13
|
+
# ## Usage
|
|
14
|
+
# ```ruby
|
|
15
|
+
# Smriti::MatViewDefinition.paginate(total: 100, page: 2, per_page: 20)
|
|
16
|
+
# # => Returns records 21-40 of the total 100
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# @note Methods are added as **class methods** to the including model.
|
|
20
|
+
#
|
|
21
|
+
# @!method self.paginate(total:, page:, per_page:)
|
|
22
|
+
# Paginates the relation based on total records, current page, and per-page count.
|
|
23
|
+
# @param total [Integer] Total number of records in the full result set.
|
|
24
|
+
# @param page [Integer] Current page number (1-based).
|
|
25
|
+
# @param per_page [Integer] Number of records per page.
|
|
26
|
+
# @return [ActiveRecord::Relation] Paginated relation.
|
|
27
|
+
#
|
|
28
|
+
module SmritiPaginate
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
# Adds a scope for paginating records.
|
|
33
|
+
# Usage: Model.paginate(total: total_count, page: current_page, per_page: per_page_count)
|
|
34
|
+
#
|
|
35
|
+
# Calculates the correct offset and limit based on the provided parameters.
|
|
36
|
+
# Ensures page and per_page are within valid ranges.
|
|
37
|
+
# Defaults per_page to 20 if an invalid value is provided.
|
|
38
|
+
# Returns an ActiveRecord::Relation with the appropriate records.
|
|
39
|
+
#
|
|
40
|
+
# @param total [Integer] Total number of records in the full result set.
|
|
41
|
+
# @param page [Integer] Current page number (1-based).
|
|
42
|
+
# @param per_page [Integer] Number of records per page.
|
|
43
|
+
#
|
|
44
|
+
# @return [ActiveRecord::Relation] Paginated relation.
|
|
45
|
+
scope :paginate, lambda { |total:, page:, per_page:|
|
|
46
|
+
page = page.to_i
|
|
47
|
+
per_page = per_page.to_i
|
|
48
|
+
per_page = 20 if per_page <= 0
|
|
49
|
+
|
|
50
|
+
total_pages = (total.to_f / per_page).ceil
|
|
51
|
+
page = 1 if page < 1 || (page > total_pages && total_pages.positive?)
|
|
52
|
+
|
|
53
|
+
offset((page - 1) * per_page).limit(per_page)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
class_methods do
|
|
57
|
+
# Calculates the total number of pages based on total records and per-page count.
|
|
58
|
+
#
|
|
59
|
+
# @param total [Integer] Total number of records.
|
|
60
|
+
# @param per_page [Integer] Number of records per page.
|
|
61
|
+
# @return [Integer] Total number of pages.
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# total_pages(total: 100, per_page: 20) #=> 5
|
|
65
|
+
def total_pages(total:, per_page:)
|
|
66
|
+
per_page = per_page.to_i
|
|
67
|
+
(total.to_f / per_page).ceil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
# SmritiQueryHelper
|
|
9
|
+
# ----------------
|
|
10
|
+
#
|
|
11
|
+
module SmritiQueryHelper
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
class_methods do
|
|
15
|
+
def ordered_by_enum(enum_values:, enum_name:, direction:)
|
|
16
|
+
enum_pairs = enum_values.map do |name, int|
|
|
17
|
+
[int, human_enum_name(enum_name, name)]
|
|
18
|
+
end
|
|
19
|
+
enum_pairs.sort_by! { |(_int, label)| label.to_s.downcase }
|
|
20
|
+
|
|
21
|
+
when_sql = enum_pairs.each_with_index
|
|
22
|
+
.map { |(enum_int, _label), search_enum_int| "WHEN #{enum_int} THEN #{search_enum_int}" }
|
|
23
|
+
.join(' ')
|
|
24
|
+
|
|
25
|
+
order(Arel.sql("CASE #{table_name}.#{enum_name} #{when_sql} ELSE #{enum_pairs.size} END #{direction.to_s.downcase}"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def search_by_enum(enum_values:, enum_name:, term:)
|
|
29
|
+
enum_pairs = enum_values.map do |name, int|
|
|
30
|
+
[int, human_enum_name(enum_name, name)]
|
|
31
|
+
end
|
|
32
|
+
selected = enum_pairs.select { |(_int, label)| label.to_s.downcase.include?(term.downcase) }.map { |(int, _label)| int }
|
|
33
|
+
where(enum_name => selected)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
module Smriti
|
|
11
|
+
##
|
|
12
|
+
# Base model class for all ActiveRecord models in the smriti engine.
|
|
13
|
+
#
|
|
14
|
+
# Inherits from {ActiveRecord::Base} and marks itself as an abstract class.
|
|
15
|
+
# Other engine models should subclass this rather than inheriting directly
|
|
16
|
+
# from {ActiveRecord::Base}, so that shared behavior or configuration can be
|
|
17
|
+
# applied in one place.
|
|
18
|
+
#
|
|
19
|
+
# @abstract
|
|
20
|
+
#
|
|
21
|
+
# @example Define a new model under smriti
|
|
22
|
+
# class Smriti::MatViewDefinition < Smriti::ApplicationRecord
|
|
23
|
+
# self.table_name = "mat_view_definitions"
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
27
|
+
##
|
|
28
|
+
# Marks this record class as abstract, so it won’t be persisted to a table.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
self.abstract_class = true
|
|
33
|
+
|
|
34
|
+
# Include shared concerns for i18n, queries, and pagination.
|
|
35
|
+
include SmritiI18n
|
|
36
|
+
include SmritiPaginate
|
|
37
|
+
include SmritiQueryHelper
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
module Smriti
|
|
11
|
+
##
|
|
12
|
+
# Represents a **materialised view definition** managed by the engine.
|
|
13
|
+
#
|
|
14
|
+
# A definition stores the canonical name and SQL for a materialised view and
|
|
15
|
+
# drives lifecycle operations (create, refresh, delete) via background jobs
|
|
16
|
+
# and services. It also tracks operational history through associated
|
|
17
|
+
# run models.
|
|
18
|
+
#
|
|
19
|
+
# Validations ensure a sane PostgreSQL identifier for `name` and that `sql`
|
|
20
|
+
# begins with `SELECT` (case-insensitive).
|
|
21
|
+
#
|
|
22
|
+
# @see Smriti::CreateViewJob
|
|
23
|
+
# @see Smriti::RefreshViewJob
|
|
24
|
+
# @see Smriti::DeleteViewJob
|
|
25
|
+
# @see Smriti::Services::CreateView
|
|
26
|
+
# @see Smriti::Services::RegularRefresh
|
|
27
|
+
# @see Smriti::Services::ConcurrentRefresh
|
|
28
|
+
# @see Smriti::Services::SwapRefresh
|
|
29
|
+
#
|
|
30
|
+
# @example Creating a definition
|
|
31
|
+
# defn = Smriti::MatViewDefinition.create!(
|
|
32
|
+
# name: "mv_user_accounts",
|
|
33
|
+
# sql: "SELECT users.id, accounts.id AS account_id FROM users JOIN accounts ON ..."
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
# @example Enqueue a refresh
|
|
37
|
+
# Smriti::RefreshViewJob.perform_later(defn.id, :estimated)
|
|
38
|
+
#
|
|
39
|
+
class MatViewDefinition < ApplicationRecord
|
|
40
|
+
##
|
|
41
|
+
# Underlying database table name.
|
|
42
|
+
self.table_name = 'mat_view_definitions'
|
|
43
|
+
|
|
44
|
+
# ────────────────────────────────────────────────────────────────
|
|
45
|
+
# Associations
|
|
46
|
+
# ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Historical create runs linked to this definition.
|
|
50
|
+
#
|
|
51
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewRun>]
|
|
52
|
+
#
|
|
53
|
+
has_many :mat_view_runs,
|
|
54
|
+
dependent: :destroy,
|
|
55
|
+
class_name: 'Smriti::MatViewRun'
|
|
56
|
+
|
|
57
|
+
# ────────────────────────────────────────────────────────────────
|
|
58
|
+
# Validations
|
|
59
|
+
# ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# @!attribute name
|
|
63
|
+
# validates :name that must be present, unique, and a valid identifier.
|
|
64
|
+
validates :name,
|
|
65
|
+
presence: true,
|
|
66
|
+
uniqueness: true,
|
|
67
|
+
format: { with: /\A[a-zA-Z_][a-zA-Z0-9_]*\z/ }
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @!attribute sql
|
|
71
|
+
# validates :sql that must be present and begin with SELECT.
|
|
72
|
+
validates :sql,
|
|
73
|
+
presence: true,
|
|
74
|
+
format: { with: /\A\s*SELECT/i, message: :invalid }
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# @!attribute unique_index_columns
|
|
78
|
+
# validates :unique_index_columns to be non-empty when using `refresh_strategy=concurrent`.
|
|
79
|
+
validates :unique_index_columns,
|
|
80
|
+
length: { minimum: 1, message: :at_least_one },
|
|
81
|
+
if: -> { refresh_strategy == 'concurrent' }
|
|
82
|
+
|
|
83
|
+
# ────────────────────────────────────────────────────────────────
|
|
84
|
+
# Enums / configuration
|
|
85
|
+
# ────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# Refresh strategy that governs which service is used by {RefreshViewJob}.
|
|
89
|
+
#
|
|
90
|
+
# - `:regular` → {Smriti::Services::RegularRefresh}
|
|
91
|
+
# - `:concurrent` → {Smriti::Services::ConcurrentRefresh}
|
|
92
|
+
# - `:swap` → {Smriti::Services::SwapRefresh}
|
|
93
|
+
#
|
|
94
|
+
# @!attribute [rw] refresh_strategy
|
|
95
|
+
# @return [String] one of `"regular"`, `"concurrent"`, `"swap"`
|
|
96
|
+
#
|
|
97
|
+
enum :refresh_strategy, { regular: 0, concurrent: 1, swap: 2 }
|
|
98
|
+
|
|
99
|
+
# ────────────────────────────────────────────────────────────────
|
|
100
|
+
# Scopes for ordering, searching, filtering
|
|
101
|
+
# ────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
##
|
|
104
|
+
# Scope ordered by name
|
|
105
|
+
# Orders by the `name` attribute.
|
|
106
|
+
#
|
|
107
|
+
# @param dir [Symbol, String] `:asc` or `:desc`
|
|
108
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
109
|
+
scope :ordered_by_name, ->(dir) { order("name #{dir.to_s.upcase}") }
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# Scope ordered by refresh_strategy
|
|
113
|
+
# Orders by the `refresh_strategy` attribute, using humanized enum labels.
|
|
114
|
+
#
|
|
115
|
+
# @param dir [Symbol, String] `:asc` or `:desc`
|
|
116
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
117
|
+
scope :ordered_by_refresh_strategy, ->(dir) { ordered_by_enum(enum_values: refresh_strategies, enum_name: :refresh_strategy, direction: dir) }
|
|
118
|
+
|
|
119
|
+
## Scope ordered by schedule_cron
|
|
120
|
+
# Orders by the `schedule_cron` attribute, NULLs last.
|
|
121
|
+
#
|
|
122
|
+
# @param dir [Symbol, String] `:asc` or `:desc`
|
|
123
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
124
|
+
scope :ordered_by_schedule_cron, ->(dir) { order("schedule_cron #{dir.to_s.upcase} NULLS LAST") }
|
|
125
|
+
|
|
126
|
+
## Scope ordered by last_run_at
|
|
127
|
+
# Orders by the timestamp of the most recent associated run's `started_at`, NULLs last.
|
|
128
|
+
#
|
|
129
|
+
# @param dir [Symbol, String] `:asc` or `:desc`
|
|
130
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
131
|
+
scope :ordered_by_last_run_at, lambda { |dir|
|
|
132
|
+
dir = dir.to_s.casecmp('asc').zero? ? 'ASC' : 'DESC'
|
|
133
|
+
|
|
134
|
+
order(Arel.sql(<<~SQL.squish))
|
|
135
|
+
(
|
|
136
|
+
SELECT MAX(r.created_at)
|
|
137
|
+
FROM mat_view_runs r
|
|
138
|
+
WHERE r.mat_view_definition_id = mat_view_definitions.id
|
|
139
|
+
) #{dir} NULLS LAST
|
|
140
|
+
SQL
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
## Scope search by name
|
|
144
|
+
# Searches the `name` attribute using ILIKE.
|
|
145
|
+
#
|
|
146
|
+
# @param term [String] search term
|
|
147
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
148
|
+
scope :search_by_name, ->(term) { where('name ILIKE ?', "%#{term}%") }
|
|
149
|
+
|
|
150
|
+
## Scope search by refresh_strategy
|
|
151
|
+
# Searches the `refresh_strategy` attribute using humanized enum labels.
|
|
152
|
+
#
|
|
153
|
+
# @param term [String] search term
|
|
154
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
155
|
+
scope :search_by_refresh_strategy, ->(term) { search_by_enum(enum_values: refresh_strategies, enum_name: :refresh_strategy, term: term) }
|
|
156
|
+
|
|
157
|
+
## Scope search by schedule_cron
|
|
158
|
+
# Searches the `schedule_cron` attribute using ILIKE.
|
|
159
|
+
#
|
|
160
|
+
# @param term [String] search term
|
|
161
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
162
|
+
scope :search_by_schedule_cron, ->(term) { where('schedule_cron ILIKE ?', "%#{term}%") }
|
|
163
|
+
|
|
164
|
+
## Scope search by last_run_at
|
|
165
|
+
# Searches the timestamp of the most recent associated run's `started_at` using ILIKE
|
|
166
|
+
#
|
|
167
|
+
# @param term [String] search term
|
|
168
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
169
|
+
scope :search_by_last_run_at, lambda { |term|
|
|
170
|
+
where(<<~SQL, like: "%#{term}%")
|
|
171
|
+
EXISTS (
|
|
172
|
+
SELECT 1
|
|
173
|
+
FROM (
|
|
174
|
+
SELECT MAX(r.started_at) AS last_run_at
|
|
175
|
+
FROM mat_view_runs r
|
|
176
|
+
WHERE r.mat_view_definition_id = mat_view_definitions.id
|
|
177
|
+
) m
|
|
178
|
+
WHERE CAST(m.last_run_at AS TEXT) ILIKE :like
|
|
179
|
+
)
|
|
180
|
+
SQL
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
## Scope filtered by name
|
|
184
|
+
# Filters by exact match on the `name` attribute.
|
|
185
|
+
#
|
|
186
|
+
# @param name [String] filter value
|
|
187
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
188
|
+
scope :filtered_by_name, ->(name) { where(name:) }
|
|
189
|
+
|
|
190
|
+
## Scope filtered by refresh_strategy
|
|
191
|
+
# Filters by exact match on the `refresh_strategy` attribute.
|
|
192
|
+
#
|
|
193
|
+
# @param refresh_strategy [String] filter value, one of `"regular"`, `"concurrent"`, `"swap"`
|
|
194
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
195
|
+
scope :filtered_by_refresh_strategy, ->(refresh_strategy) { where(refresh_strategy:) }
|
|
196
|
+
|
|
197
|
+
## Scope filtered by schedule_cron
|
|
198
|
+
# Filters by exact match on the `schedule_cron` attribute, or NULL/empty.
|
|
199
|
+
#
|
|
200
|
+
# @param schedule_cron [String] filter value, or `"no_value"` to match NULL/empty
|
|
201
|
+
# @return [ActiveRecord::Relation<Smriti::MatViewDefinition>]
|
|
202
|
+
scope :filtered_by_schedule_cron, lambda { |schedule_cron|
|
|
203
|
+
if schedule_cron == 'no_value'
|
|
204
|
+
where(schedule_cron: nil).or(where(schedule_cron: ''))
|
|
205
|
+
else
|
|
206
|
+
where('schedule_cron ILIKE ?', "%#{schedule_cron.tr('_', ' ')}%")
|
|
207
|
+
end
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ────────────────────────────────────────────────────────────────
|
|
211
|
+
# Class methods
|
|
212
|
+
# ────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
class << self
|
|
215
|
+
##
|
|
216
|
+
# Returns options for filters in admin UI datatable.
|
|
217
|
+
#
|
|
218
|
+
# @return [Array<Array(String, String)>] array of `[label, value]` pairs
|
|
219
|
+
def filter_options_for_name
|
|
220
|
+
order(:name).distinct.pluck(:name).map { |name| [name, name] }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
# Returns options for filters in admin UI datatable.
|
|
225
|
+
#
|
|
226
|
+
# @return [Array<Array(String, String)>] array of `[label, value]` pairs
|
|
227
|
+
def filter_options_for_refresh_strategy
|
|
228
|
+
order(:refresh_strategy).distinct.pluck(:refresh_strategy).compact.map { |rs| [human_enum_name(:refresh_strategy, rs), rs] }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
##
|
|
232
|
+
# Returns options for filters in admin UI datatable.
|
|
233
|
+
#
|
|
234
|
+
# @return [Array<Array(String, String)>] array of `[label, value]` pairs
|
|
235
|
+
def filter_options_for_schedule_cron
|
|
236
|
+
order(:schedule_cron).distinct.pluck(:schedule_cron).compact.map { |sc| [sc, sc.tr(' ', '_')] }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ────────────────────────────────────────────────────────────────
|
|
241
|
+
# Instance methods
|
|
242
|
+
# ────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# Returns the most recent run associated with this definition.
|
|
246
|
+
#
|
|
247
|
+
# @return [Smriti::MatViewRun, nil] the latest run or `nil` if none exist
|
|
248
|
+
#
|
|
249
|
+
|
|
250
|
+
def last_run
|
|
251
|
+
mat_view_runs.order(created_at: :desc).first
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|