Skip to main content

reddb_rql/
analyzer.rs

1use std::collections::HashSet;
2
3use crate::ast::CreateTableQuery;
4use reddb_types::types::{DataType, SqlTypeName};
5
6#[derive(Debug, Clone)]
7pub enum AnalysisError {
8    DuplicateColumn(String),
9    UnsupportedType(String),
10}
11
12impl std::fmt::Display for AnalysisError {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            Self::DuplicateColumn(name) => write!(f, "duplicate column name: {name}"),
16            Self::UnsupportedType(name) => write!(f, "unsupported SQL type: {name}"),
17        }
18    }
19}
20
21impl std::error::Error for AnalysisError {}
22
23#[derive(Debug, Clone)]
24pub struct AnalyzedCreateTableQuery {
25    pub name: String,
26    pub columns: Vec<AnalyzedColumnDef>,
27    pub if_not_exists: bool,
28    pub default_ttl_ms: Option<u64>,
29    pub context_index_fields: Vec<String>,
30    pub timestamps: bool,
31}
32
33#[derive(Debug, Clone)]
34pub struct AnalyzedColumnDef {
35    pub name: String,
36    pub declared_type: SqlTypeName,
37    pub storage_type: DataType,
38    pub not_null: bool,
39    pub default: Option<String>,
40    pub primary_key: bool,
41    pub unique: bool,
42}
43
44pub fn analyze_create_table(
45    query: &CreateTableQuery,
46) -> Result<AnalyzedCreateTableQuery, AnalysisError> {
47    let mut seen = HashSet::new();
48    let mut columns = Vec::with_capacity(query.columns.len());
49
50    for column in &query.columns {
51        if !seen.insert(column.name.to_ascii_lowercase()) {
52            return Err(AnalysisError::DuplicateColumn(column.name.clone()));
53        }
54
55        columns.push(AnalyzedColumnDef {
56            name: column.name.clone(),
57            declared_type: column.sql_type.clone(),
58            storage_type: resolve_sql_type_name(&column.sql_type)?,
59            not_null: column.not_null,
60            default: column.default.clone(),
61            primary_key: column.primary_key,
62            unique: column.unique,
63        });
64    }
65
66    Ok(AnalyzedCreateTableQuery {
67        name: query.name.clone(),
68        columns,
69        if_not_exists: query.if_not_exists,
70        default_ttl_ms: query.default_ttl_ms,
71        context_index_fields: query.context_index_fields.clone(),
72        timestamps: query.timestamps,
73    })
74}
75
76pub fn resolve_declared_data_type(declared: &str) -> Result<DataType, AnalysisError> {
77    resolve_sql_type_name(&SqlTypeName::parse_declared(declared))
78}
79
80pub fn resolve_sql_type_name(sql_type: &SqlTypeName) -> Result<DataType, AnalysisError> {
81    DataType::from_sql_type_name(sql_type)
82        .ok_or_else(|| AnalysisError::UnsupportedType(sql_type.base_name()))
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::ast::{CreateColumnDef, CreateTableQuery};
89    use reddb_types::catalog::CollectionModel;
90
91    fn column(name: &str, declared: &str) -> CreateColumnDef {
92        CreateColumnDef {
93            name: name.to_string(),
94            data_type: declared.to_string(),
95            sql_type: SqlTypeName::parse_declared(declared),
96            not_null: false,
97            default: None,
98            compress: None,
99            unique: false,
100            primary_key: false,
101            enum_variants: Vec::new(),
102            array_element: None,
103            decimal_precision: None,
104        }
105    }
106
107    fn create_table(columns: Vec<CreateColumnDef>) -> CreateTableQuery {
108        CreateTableQuery {
109            collection_model: CollectionModel::Table,
110            name: "orders".to_string(),
111            columns,
112            if_not_exists: true,
113            default_ttl_ms: Some(60_000),
114            metrics_rollup_policies: Vec::new(),
115            context_index_fields: vec!["description".to_string()],
116            context_index_enabled: true,
117            timestamps: true,
118            partition_by: None,
119            tenant_by: None,
120            append_only: false,
121            subscriptions: Vec::new(),
122            analytics_config: Vec::new(),
123            vault_own_master_key: false,
124            ai_policy: None,
125        }
126    }
127
128    #[test]
129    fn analyze_create_table_resolves_columns_and_preserves_options() {
130        let mut id = column("id", "INTEGER");
131        id.primary_key = true;
132        id.not_null = true;
133
134        let mut description = column("description", "VARCHAR");
135        description.default = Some("'new'".to_string());
136        description.unique = true;
137
138        let query = create_table(vec![id, description]);
139        let analyzed = analyze_create_table(&query).unwrap();
140
141        assert_eq!(analyzed.name, "orders");
142        assert!(analyzed.if_not_exists);
143        assert_eq!(analyzed.default_ttl_ms, Some(60_000));
144        assert_eq!(analyzed.context_index_fields, ["description"]);
145        assert!(analyzed.timestamps);
146        assert_eq!(analyzed.columns.len(), 2);
147        assert_eq!(analyzed.columns[0].name, "id");
148        assert_eq!(analyzed.columns[0].storage_type, DataType::Integer);
149        assert!(analyzed.columns[0].not_null);
150        assert!(analyzed.columns[0].primary_key);
151        assert_eq!(analyzed.columns[1].declared_type.base_name(), "VARCHAR");
152        assert_eq!(analyzed.columns[1].storage_type, DataType::Text);
153        assert_eq!(analyzed.columns[1].default.as_deref(), Some("'new'"));
154        assert!(analyzed.columns[1].unique);
155    }
156
157    #[test]
158    fn duplicate_columns_are_case_insensitive() {
159        let query = create_table(vec![column("Id", "INT"), column("id", "INT")]);
160        let err = analyze_create_table(&query).unwrap_err();
161
162        assert!(matches!(err, AnalysisError::DuplicateColumn(ref name) if name == "id"));
163        assert_eq!(err.to_string(), "duplicate column name: id");
164    }
165
166    #[test]
167    fn unsupported_type_is_reported_with_normalized_name() {
168        let query = create_table(vec![column("mystery", "not_a_real_type")]);
169        let err = analyze_create_table(&query).unwrap_err();
170
171        assert!(
172            matches!(err, AnalysisError::UnsupportedType(ref name) if name == "NOT_A_REAL_TYPE")
173        );
174        assert_eq!(err.to_string(), "unsupported SQL type: NOT_A_REAL_TYPE");
175    }
176
177    #[test]
178    fn resolve_declared_data_type_accepts_sql_aliases() {
179        assert_eq!(
180            resolve_declared_data_type("varchar").unwrap(),
181            DataType::Text
182        );
183        assert_eq!(
184            resolve_declared_data_type("numeric(10)").unwrap(),
185            DataType::Decimal
186        );
187        assert_eq!(
188            resolve_declared_data_type("timestamptz").unwrap(),
189            DataType::TimestampMs
190        );
191        assert!(resolve_declared_data_type("definitely_not_sql").is_err());
192    }
193}