Skip to main content

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}