perspective_client/virtual_server/generic_sql_model.rs
1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13//! SQL query builder for virtual server operations.
14//!
15//! This module provides a stateless SQL query generator that produces
16//! generic SQL strings for perspective virtual server operations.
17
18// TODO(texodus): Missing these features
19//
20// - row expand/collapse in the datagrid needs datamodel support, this is likely
21// a "collapsed" boolean column in the temp table we `UPDATE`.
22//
23// - `on_update` real-time support will be method which takes sa view name and a
24// handler and calls the handler when the view needs to be recalculated.
25//
26// Nice to have:
27//
28// - Optional `view_change` method can be implemented for engine optimization,
29// defaulting to just delete & recreate (as Perspective engine does now).
30//
31// - Would like to add a metadata API so that e.g. Viewer debug panel could show
32// internal generated SQL.
33
34mod table_make_view;
35
36#[cfg(test)]
37mod tests;
38
39use std::fmt;
40
41use indexmap::IndexMap;
42use serde::Deserialize;
43
44use crate::config::{FilterTerm, GroupRollupMode, Scalar, Sort, SortDir, ViewConfig};
45use crate::proto::{ColumnType, ViewPort};
46use crate::virtual_server::generic_sql_model::table_make_view::ViewQueryContext;
47
48/// Error type for SQL generation operations.
49#[derive(Debug, Clone)]
50pub enum GenericSQLError {
51 /// A required column was not found in the schema.
52 ColumnNotFound(String),
53 /// An invalid configuration was provided.
54 InvalidConfig(String),
55 /// An unsupported operation was requested.
56 UnsupportedOperation(String),
57}
58
59impl fmt::Display for GenericSQLError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::ColumnNotFound(col) => write!(f, "Column not found: {}", col),
63 Self::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
64 Self::UnsupportedOperation(msg) => write!(f, "Unsupported operation: {}", msg),
65 }
66 }
67}
68
69impl std::error::Error for GenericSQLError {}
70
71/// Result type alias for SQL operations.
72pub type GenericSQLResult<T> = Result<T, GenericSQLError>;
73
74#[derive(Clone, Debug, Deserialize, Default)]
75pub struct GenericSQLVirtualServerModelArgs {
76 create_entity: Option<String>,
77 grouping_fn: Option<String>,
78}
79
80/// A stateless SQL query builder virtual server operations.
81///
82/// This struct generates SQL query strings without executing them, allowing
83/// the caller to execute the queries against a SQL connection.
84#[derive(Debug, Default, Clone)]
85pub struct GenericSQLVirtualServerModel(GenericSQLVirtualServerModelArgs);
86
87impl GenericSQLVirtualServerModel {
88 /// Creates a new `GenericSQLVirtualServerModel` instance.
89 pub fn new(args: GenericSQLVirtualServerModelArgs) -> Self {
90 Self(args)
91 }
92
93 /// Returns the SQL query to list all hosted tables.
94 ///
95 /// # Returns
96 /// SQL: `SHOW ALL TABLES`
97 pub fn get_hosted_tables(&self) -> GenericSQLResult<String> {
98 Ok("SHOW ALL TABLES".to_string())
99 }
100
101 /// Returns the SQL query to describe a table's schema.
102 ///
103 /// # Arguments
104 /// * `table_id` - The identifier of the table to describe.
105 ///
106 /// # Returns
107 /// SQL: `DESCRIBE {table_id}`
108 pub fn table_schema(&self, table_id: &str) -> GenericSQLResult<String> {
109 Ok(format!("DESCRIBE {}", table_id))
110 }
111
112 /// Returns the SQL query to get the row count of a table.
113 ///
114 /// # Arguments
115 /// * `table_id` - The identifier of the table.
116 ///
117 /// # Returns
118 /// SQL: `SELECT COUNT(*) FROM {table_id}`
119 pub fn table_size(&self, table_id: &str) -> GenericSQLResult<String> {
120 Ok(format!("SELECT COUNT(*) FROM {}", table_id))
121 }
122
123 /// Returns the SQL query to get the column count of a view.
124 ///
125 /// # Arguments
126 /// * `view_id` - The identifier of the view.
127 ///
128 /// # Returns
129 /// SQL: `SELECT COUNT(*) FROM (DESCRIBE {view_id})`
130 pub fn view_column_size(&self, view_id: &str) -> GenericSQLResult<String> {
131 Ok(format!("SELECT COUNT(*) FROM (DESCRIBE {})", view_id))
132 }
133
134 /// Returns the SQL query to validate an expression against a table.
135 ///
136 /// # Arguments
137 /// * `table_id` - The identifier of the table.
138 /// * `expression` - The SQL expression to validate.
139 ///
140 /// # Returns
141 /// SQL: `DESCRIBE (SELECT {expression} FROM {table_id})`
142 pub fn table_validate_expression(
143 &self,
144 table_id: &str,
145 expression: &str,
146 ) -> GenericSQLResult<String> {
147 Ok(format!(
148 "DESCRIBE (SELECT {} FROM {})",
149 expression, table_id
150 ))
151 }
152
153 /// Returns the SQL query to delete a view.
154 ///
155 /// # Arguments
156 /// * `view_id` - The identifier of the view to delete.
157 ///
158 /// # Returns
159 /// SQL: `DROP TABLE IF EXISTS {view_id}`
160 pub fn view_delete(&self, view_id: &str) -> GenericSQLResult<String> {
161 Ok(format!("DROP TABLE IF EXISTS {}", view_id))
162 }
163
164 /// Returns the SQL query to create a view from a table with the given
165 /// configuration.
166 ///
167 /// # Arguments
168 /// * `table_id` - The identifier of the source table.
169 /// * `view_id` - The identifier for the new view.
170 /// * `config` - The view configuration specifying columns, group_by,
171 /// split_by, etc.
172 ///
173 /// # Returns
174 /// SQL: `CREATE TABLE {view_id} AS (...)`
175 pub fn table_make_view(
176 &self,
177 table_id: &str,
178 view_id: &str,
179 config: &ViewConfig,
180 ) -> GenericSQLResult<String> {
181 let ctx = ViewQueryContext::new(self, table_id, config);
182 let query = ctx.build_query();
183 let template = self.0.create_entity.as_deref().unwrap_or("TABLE");
184 Ok(format!("CREATE {} {} AS ({})", template, view_id, query))
185 }
186
187 /// Returns the SQL query to fetch data from a view with the given viewport.
188 ///
189 /// # Arguments
190 /// * `view_id` - The identifier of the view.
191 /// * `config` - The view configuration.
192 /// * `viewport` - The viewport specifying row/column ranges.
193 /// * `schema` - The schema of the view (column names to types).
194 ///
195 /// # Returns
196 /// SQL: `SELECT ... FROM {view_id} LIMIT ... OFFSET ...`
197 pub fn view_get_data(
198 &self,
199 view_id: &str,
200 config: &ViewConfig,
201 viewport: &ViewPort,
202 schema: &IndexMap<String, ColumnType>,
203 ) -> GenericSQLResult<String> {
204 let group_by = &config.group_by;
205 let sort = &config.sort;
206 let start_col = viewport.start_col.unwrap_or(0) as usize;
207 let end_col = viewport.end_col.map(|x| x as usize);
208 let start_row = viewport.start_row.unwrap_or(0);
209 let end_row = viewport.end_row;
210 let limit_clause = if let Some(end) = end_row {
211 format!("LIMIT {} OFFSET {}", end - start_row, start_row)
212 } else {
213 String::new()
214 };
215
216 let mut data_columns: Vec<&String> = schema
217 .keys()
218 .filter(|col_name| !col_name.starts_with("__"))
219 .collect();
220
221 let col_sort_dir = sort.iter().find_map(|Sort(_, dir)| match dir {
222 SortDir::ColAsc | SortDir::ColAscAbs => Some(true),
223 SortDir::ColDesc | SortDir::ColDescAbs => Some(false),
224 _ => None,
225 });
226
227 if let Some(ascending) = col_sort_dir {
228 if ascending {
229 data_columns.sort();
230 } else {
231 data_columns.sort_by(|a, b| b.cmp(a));
232 }
233 }
234
235 let data_columns: Vec<&String> = data_columns
236 .into_iter()
237 .skip(start_col)
238 .take(end_col.map(|e| e - start_col).unwrap_or(usize::MAX))
239 .collect();
240
241 let mut group_by_cols: Vec<String> = Vec::new();
242 if !group_by.is_empty() {
243 if config.group_rollup_mode != GroupRollupMode::Flat {
244 group_by_cols.push("\"__GROUPING_ID__\"".to_string());
245 }
246 for idx in 0..group_by.len() {
247 group_by_cols.push(format!("\"__ROW_PATH_{}__\"", idx));
248 }
249 }
250
251 let all_columns: Vec<String> = group_by_cols
252 .into_iter()
253 .chain(data_columns.iter().map(|col| format!("\"{}\"", col)))
254 .collect();
255
256 Ok(format!(
257 "SELECT {} FROM {} {}",
258 all_columns.join(", "),
259 view_id,
260 limit_clause
261 )
262 .trim()
263 .to_string())
264 }
265
266 /// Returns the SQL query to describe a view's schema.
267 ///
268 /// # Arguments
269 /// * `view_id` - The identifier of the view.
270 ///
271 /// # Returns
272 /// SQL: `DESCRIBE {view_id}`
273 pub fn view_schema(&self, view_id: &str) -> GenericSQLResult<String> {
274 Ok(format!("DESCRIBE {}", view_id))
275 }
276
277 /// Returns the SQL query to get the row count of a view.
278 ///
279 /// # Arguments
280 /// * `view_id` - The identifier of the view.
281 ///
282 /// # Returns
283 /// SQL: `SELECT COUNT(*) FROM {view_id}`
284 pub fn view_size(&self, view_id: &str) -> GenericSQLResult<String> {
285 Ok(format!("SELECT COUNT(*) FROM {}", view_id))
286 }
287
288 /// Returns the SQL query to get the min and max values of a column.
289 ///
290 /// # Arguments
291 /// * `view_id` - The identifier of the view.
292 /// * `column_name` - The name of the column.
293 /// * `config` - The view configuration.
294 ///
295 /// # Returns
296 /// SQL: `SELECT MIN("column_name"), MAX("column_name") FROM {view_id}`
297 /// When the view uses ROLLUP grouping (non-flat mode with group_by),
298 /// a `WHERE __GROUPING_ID__ = 0` clause is added to exclude non-leaf rows.
299 pub fn view_get_min_max(
300 &self,
301 view_id: &str,
302 column_name: &str,
303 config: &ViewConfig,
304 ) -> GenericSQLResult<String> {
305 let has_grouping_id =
306 !config.group_by.is_empty() && config.group_rollup_mode != GroupRollupMode::Flat;
307 let where_clause = if has_grouping_id {
308 " WHERE __GROUPING_ID__ = 0"
309 } else {
310 ""
311 };
312
313 Ok(format!(
314 "SELECT MIN(\"{}\"), MAX(\"{}\") FROM {}{}",
315 column_name, column_name, view_id, where_clause
316 ))
317 }
318
319 fn filter_term_to_sql(term: &FilterTerm) -> Option<String> {
320 match term {
321 FilterTerm::Scalar(scalar) => Self::scalar_to_sql(scalar),
322 FilterTerm::Array(scalars) => {
323 let values: Vec<String> = scalars.iter().filter_map(Self::scalar_to_sql).collect();
324 if values.is_empty() {
325 None
326 } else {
327 Some(format!("({})", values.join(", ")))
328 }
329 },
330 }
331 }
332
333 fn scalar_to_sql(scalar: &Scalar) -> Option<String> {
334 match scalar {
335 Scalar::Null => None,
336 Scalar::Bool(b) => Some(if *b { "TRUE" } else { "FALSE" }.to_string()),
337 Scalar::Float(f) => Some(f.to_string()),
338 Scalar::String(s) => Some(format!("'{}'", s.replace('\'', "''"))),
339 }
340 }
341}