Skip to main content

sqlcx_core/parser/
mod.rs

1pub mod joins;
2pub mod mysql;
3pub mod postgres;
4pub mod sqlite;
5
6use std::collections::HashMap;
7use std::sync::LazyLock;
8
9use regex::Regex;
10
11use crate::annotations::extract_annotations;
12use crate::error::Result;
13use crate::ir::{
14    ColumnDef, EnumDef, ParamDef, QueryCommand, QueryDef, SqlType, SqlTypeCategory, TableDef,
15};
16use crate::param_naming::{RawParam, resolve_param_names};
17
18pub trait DatabaseParser {
19    fn parse_schema(&self, sql: &str) -> Result<(Vec<TableDef>, Vec<EnumDef>)>;
20    fn parse_queries(
21        &self,
22        sql: &str,
23        tables: &[TableDef],
24        enums: &[EnumDef],
25        source_file: &str,
26    ) -> Result<Vec<QueryDef>>;
27}
28
29pub fn resolve_parser(name: &str) -> Result<Box<dyn DatabaseParser>> {
30    match name {
31        "postgres" => Ok(Box::new(postgres::PostgresParser::new())),
32        "mysql" => Ok(Box::new(mysql::MySqlParser::new())),
33        "sqlite" => Ok(Box::new(sqlite::SqliteParser::new())),
34        _ => Err(crate::error::SqlcxError::UnknownParser(name.to_string())),
35    }
36}
37
38pub(crate) fn ensure_supported_select_expr(expr: &str, source_file: &str) -> Result<()> {
39    let trimmed = expr.trim();
40    if trimmed.contains('.') {
41        return Err(crate::error::SqlcxError::ParseError {
42            file: source_file.to_string(),
43            message: format!(
44                "qualified select expressions are not supported yet: `{}`",
45                trimmed
46            ),
47        });
48    }
49    Ok(())
50}
51
52// ── Shared regex for split_query_blocks ──────────────────────────────────────
53
54static QUERY_HEADER_RE: LazyLock<Regex> =
55    LazyLock::new(|| Regex::new(r"--\s*name:\s*(\w+)\s+:(one|many|execresult|exec)").unwrap());
56
57// ── Shared utilities ─────────────────────────────────────────────────────────
58
59/// Split CREATE TABLE body by commas, respecting nested parens.
60pub(crate) fn split_column_defs(body: &str) -> Vec<String> {
61    let mut parts = Vec::new();
62    let mut depth = 0i32;
63    let mut current = String::new();
64
65    for ch in body.chars() {
66        match ch {
67            '(' => {
68                depth += 1;
69                current.push(ch);
70            }
71            ')' => {
72                depth -= 1;
73                current.push(ch);
74            }
75            ',' if depth == 0 => {
76                let trimmed = current.trim().to_string();
77                if !trimmed.is_empty() {
78                    parts.push(trimmed);
79                }
80                current.clear();
81            }
82            _ => current.push(ch),
83        }
84    }
85    let trimmed = current.trim().to_string();
86    if !trimmed.is_empty() {
87        parts.push(trimmed);
88    }
89    parts
90}
91
92pub(crate) struct QueryBlock {
93    pub name: String,
94    pub command: QueryCommand,
95    pub sql: String,
96    pub comments: String,
97}
98
99pub(crate) fn split_query_blocks(sql: &str) -> Vec<QueryBlock> {
100    let header_re = &*QUERY_HEADER_RE;
101
102    let lines: Vec<&str> = sql.lines().collect();
103    let mut blocks: Vec<QueryBlock> = Vec::new();
104    let mut current: Option<QueryBlock> = None;
105    let mut comment_buffer = String::new();
106
107    for line in &lines {
108        let trimmed = line.trim();
109
110        if let Some(cap) = header_re.captures(trimmed) {
111            if let Some(block) = current.take() {
112                blocks.push(block);
113            }
114
115            let command = match &cap[2] {
116                "one" => QueryCommand::One,
117                "many" => QueryCommand::Many,
118                "execresult" => QueryCommand::ExecResult,
119                _ => QueryCommand::Exec,
120            };
121
122            let mut comments = comment_buffer.clone();
123            comments.push_str(trimmed);
124            comments.push('\n');
125            comment_buffer.clear();
126
127            current = Some(QueryBlock {
128                name: cap[1].to_string(),
129                command,
130                sql: String::new(),
131                comments,
132            });
133        } else if trimmed.starts_with("--") {
134            if let Some(ref mut block) = current {
135                block.comments.push_str(trimmed);
136                block.comments.push('\n');
137            } else {
138                comment_buffer.push_str(trimmed);
139                comment_buffer.push('\n');
140            }
141        } else if let Some(ref mut block) = current
142            && !trimmed.is_empty()
143        {
144            if !block.sql.is_empty() {
145                block.sql.push(' ');
146            }
147            block.sql.push_str(trimmed);
148        }
149    }
150
151    if let Some(block) = current {
152        blocks.push(block);
153    }
154
155    blocks
156}
157
158pub(crate) fn build_params(
159    comments: &str,
160    table: Option<&TableDef>,
161    param_indices: Vec<u32>,
162    inferred_cols: HashMap<u32, String>,
163) -> Vec<ParamDef> {
164    if param_indices.is_empty() {
165        return Vec::new();
166    }
167
168    let (_, ann) = extract_annotations(comments);
169
170    let raw_params: Vec<RawParam> = param_indices
171        .iter()
172        .map(|&idx| RawParam {
173            index: idx,
174            column: inferred_cols.get(&idx).cloned(),
175            r#override: ann.param_overrides.get(&idx).cloned(),
176        })
177        .collect();
178
179    let names = resolve_param_names(&raw_params);
180
181    param_indices
182        .iter()
183        .enumerate()
184        .map(|(i, &idx)| {
185            let col_name = inferred_cols.get(&idx);
186            let sql_type = if let (Some(tbl), Some(cn)) = (table, col_name) {
187                tbl.columns
188                    .iter()
189                    .find(|c| c.name == *cn)
190                    .map(|c| c.sql_type.clone())
191                    .unwrap_or_else(make_unknown_type)
192            } else {
193                make_unknown_type()
194            };
195
196            ParamDef {
197                index: idx,
198                name: names[i].clone(),
199                sql_type,
200            }
201        })
202        .collect()
203}
204
205pub(crate) fn make_unknown_column(name: &str) -> ColumnDef {
206    ColumnDef {
207        name: name.to_string(),
208        alias: None,
209        source_table: None,
210        sql_type: SqlType {
211            raw: "unknown".to_string(),
212            normalized: "unknown".to_string(),
213            category: SqlTypeCategory::Unknown,
214            element_type: None,
215            enum_name: None,
216            enum_values: None,
217            json_shape: None,
218        },
219        nullable: true,
220        has_default: false,
221    }
222}
223
224pub(crate) fn make_unknown_type() -> SqlType {
225    SqlType {
226        raw: "unknown".to_string(),
227        normalized: "unknown".to_string(),
228        category: SqlTypeCategory::Unknown,
229        element_type: None,
230        enum_name: None,
231        enum_values: None,
232        json_shape: None,
233    }
234}