mod table_make_view;
#[cfg(test)]
mod tests;
use std::fmt;
use indexmap::IndexMap;
use serde::Deserialize;
use crate::config::{FilterTerm, GroupRollupMode, Scalar, Sort, SortDir, ViewConfig};
use crate::proto::{ColumnType, ViewPort};
use crate::virtual_server::generic_sql_model::table_make_view::ViewQueryContext;
#[derive(Debug, Clone)]
pub enum GenericSQLError {
ColumnNotFound(String),
InvalidConfig(String),
UnsupportedOperation(String),
}
impl fmt::Display for GenericSQLError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ColumnNotFound(col) => write!(f, "Column not found: {}", col),
Self::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
Self::UnsupportedOperation(msg) => write!(f, "Unsupported operation: {}", msg),
}
}
}
impl std::error::Error for GenericSQLError {}
pub type GenericSQLResult<T> = Result<T, GenericSQLError>;
#[derive(Clone, Debug, Deserialize, Default)]
pub struct GenericSQLVirtualServerModelArgs {
create_entity: Option<String>,
grouping_fn: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct GenericSQLVirtualServerModel(GenericSQLVirtualServerModelArgs);
impl GenericSQLVirtualServerModel {
pub fn new(args: GenericSQLVirtualServerModelArgs) -> Self {
Self(args)
}
pub fn get_hosted_tables(&self) -> GenericSQLResult<String> {
Ok("SHOW ALL TABLES".to_string())
}
pub fn table_schema(&self, table_id: &str) -> GenericSQLResult<String> {
Ok(format!("DESCRIBE {}", table_id))
}
pub fn table_size(&self, table_id: &str) -> GenericSQLResult<String> {
Ok(format!("SELECT COUNT(*) FROM {}", table_id))
}
pub fn view_column_size(&self, view_id: &str) -> GenericSQLResult<String> {
Ok(format!("SELECT COUNT(*) FROM (DESCRIBE {})", view_id))
}
pub fn table_validate_expression(
&self,
table_id: &str,
expression: &str,
) -> GenericSQLResult<String> {
Ok(format!(
"DESCRIBE (SELECT {} FROM {})",
expression, table_id
))
}
pub fn view_delete(&self, view_id: &str) -> GenericSQLResult<String> {
Ok(format!("DROP TABLE IF EXISTS {}", view_id))
}
pub fn table_make_view(
&self,
table_id: &str,
view_id: &str,
config: &ViewConfig,
) -> GenericSQLResult<String> {
let ctx = ViewQueryContext::new(self, table_id, config);
let query = ctx.build_query();
let template = self.0.create_entity.as_deref().unwrap_or("TABLE");
Ok(format!("CREATE {} {} AS ({})", template, view_id, query))
}
pub fn view_get_data(
&self,
view_id: &str,
config: &ViewConfig,
viewport: &ViewPort,
schema: &IndexMap<String, ColumnType>,
) -> GenericSQLResult<String> {
let group_by = &config.group_by;
let sort = &config.sort;
let start_col = viewport.start_col.unwrap_or(0) as usize;
let end_col = viewport.end_col.map(|x| x as usize);
let start_row = viewport.start_row.unwrap_or(0);
let end_row = viewport.end_row;
let limit_clause = if let Some(end) = end_row {
format!("LIMIT {} OFFSET {}", end - start_row, start_row)
} else {
String::new()
};
let mut data_columns: Vec<&String> = schema
.keys()
.filter(|col_name| !col_name.starts_with("__"))
.collect();
let col_sort_dir = sort.iter().find_map(|Sort(_, dir)| match dir {
SortDir::ColAsc | SortDir::ColAscAbs => Some(true),
SortDir::ColDesc | SortDir::ColDescAbs => Some(false),
_ => None,
});
if let Some(ascending) = col_sort_dir {
if ascending {
data_columns.sort();
} else {
data_columns.sort_by(|a, b| b.cmp(a));
}
}
let data_columns: Vec<&String> = data_columns
.into_iter()
.skip(start_col)
.take(end_col.map(|e| e - start_col).unwrap_or(usize::MAX))
.collect();
let mut group_by_cols: Vec<String> = Vec::new();
if !group_by.is_empty() {
if config.group_rollup_mode != GroupRollupMode::Flat {
group_by_cols.push("\"__GROUPING_ID__\"".to_string());
}
for idx in 0..group_by.len() {
group_by_cols.push(format!("\"__ROW_PATH_{}__\"", idx));
}
}
let all_columns: Vec<String> = group_by_cols
.into_iter()
.chain(data_columns.iter().map(|col| format!("\"{}\"", col)))
.collect();
Ok(format!(
"SELECT {} FROM {} {}",
all_columns.join(", "),
view_id,
limit_clause
)
.trim()
.to_string())
}
pub fn view_schema(&self, view_id: &str) -> GenericSQLResult<String> {
Ok(format!("DESCRIBE {}", view_id))
}
pub fn view_size(&self, view_id: &str) -> GenericSQLResult<String> {
Ok(format!("SELECT COUNT(*) FROM {}", view_id))
}
pub fn view_get_min_max(
&self,
view_id: &str,
column_name: &str,
config: &ViewConfig,
) -> GenericSQLResult<String> {
let has_grouping_id =
!config.group_by.is_empty() && config.group_rollup_mode != GroupRollupMode::Flat;
let where_clause = if has_grouping_id {
" WHERE __GROUPING_ID__ = 0"
} else {
""
};
Ok(format!(
"SELECT MIN(\"{}\"), MAX(\"{}\") FROM {}{}",
column_name, column_name, view_id, where_clause
))
}
fn filter_term_to_sql(term: &FilterTerm) -> Option<String> {
match term {
FilterTerm::Scalar(scalar) => Self::scalar_to_sql(scalar),
FilterTerm::Array(scalars) => {
let values: Vec<String> = scalars.iter().filter_map(Self::scalar_to_sql).collect();
if values.is_empty() {
None
} else {
Some(format!("({})", values.join(", ")))
}
},
}
}
fn scalar_to_sql(scalar: &Scalar) -> Option<String> {
match scalar {
Scalar::Null => None,
Scalar::Bool(b) => Some(if *b { "TRUE" } else { "FALSE" }.to_string()),
Scalar::Float(f) => Some(f.to_string()),
Scalar::String(s) => Some(format!("'{}'", s.replace('\'', "''"))),
}
}
}