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
52static QUERY_HEADER_RE: LazyLock<Regex> =
55 LazyLock::new(|| Regex::new(r"--\s*name:\s*(\w+)\s+:(one|many|execresult|exec)").unwrap());
56
57pub(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}