Skip to main content

alopex_sql/
nim_bridge.rs

1use crate::ast::dml::{FromItem, Select, SelectItem};
2use crate::ast::expr::{Expr, ExprKind};
3use crate::ast::{Statement, StatementKind};
4use crate::error::{ParserError, Result};
5use crate::nim_ffi::{self, OwnedBuffer, ParseResultKind};
6
7/// Return the SQL/PromQL MessagePack wire contract version exported by Nim.
8pub fn parser_contract_version() -> String {
9    nim_ffi::parser_contract_version()
10}
11
12pub fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
13    if sql.as_bytes().contains(&0) {
14        return Err(ParserError::UnexpectedToken {
15            line: 0,
16            column: 0,
17            expected: "valid SQL without interior NUL bytes".to_string(),
18            found: "interior NUL byte".to_string(),
19        });
20    }
21
22    let natural_join_markers = natural_join_markers(sql);
23    // Option (a): double-quoted tokens are identifiers under SQL standard and
24    // PostgreSQL rules. The currently deployed Nim lexer predates that contract
25    // and emits them as string literals, so normalize the FFI input until every
26    // parser binary has the corrected token kind.
27    let normalized_sql = normalize_quoted_identifiers(sql);
28    let result = nim_ffi::parse_sql(&normalized_sql);
29    match result.kind {
30        ParseResultKind::Ok => {
31            let buffer = OwnedBuffer::new(result.buffer_ptr, result.buffer_len);
32            // 正常時の payload は最低でも MessagePack の配列ヘッダ 1 バイトを
33            // 含む。空 payload はゼロ初期化された CParseResult、つまり Nim 側
34            // から例外が漏れた事故 (issue #40 の desync 経路) を意味するため、
35            // 汎用の decode エラーではなく原因が特定できるエラーにする。
36            if buffer.as_slice().is_empty() {
37                return Err(ParserError::UnexpectedToken {
38                    line: 0,
39                    column: 0,
40                    expected: "MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
41                    found: "empty payload from Nim parser (leaked exception at FFI boundary; \
42                            see issue #40)"
43                        .to_string(),
44                });
45            }
46            let mut statements = rmp_serde::from_slice::<Vec<Statement>>(buffer.as_slice())
47                .map_err(|err| ParserError::UnexpectedToken {
48                    line: 0,
49                    column: 0,
50                    expected: "MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
51                    found: err.to_string(),
52                })?;
53            annotate_natural_joins(&mut statements, natural_join_markers)?;
54            Ok(statements)
55        }
56        ParseResultKind::Error => {
57            let buffer = OwnedBuffer::new(result.error_ptr.cast(), result.error_len);
58            Err(parser_error_from_nim(
59                String::from_utf8_lossy(buffer.as_slice()).as_ref(),
60            ))
61        }
62    }
63}
64
65fn normalize_quoted_identifiers(sql: &str) -> String {
66    let mut normalized = String::with_capacity(sql.len());
67    let mut chars = sql.chars().peekable();
68    while let Some(ch) = chars.next() {
69        match ch {
70            '\'' => {
71                normalized.push(ch);
72                while let Some(string_ch) = chars.next() {
73                    normalized.push(string_ch);
74                    if string_ch == '\'' {
75                        if chars.peek() == Some(&'\'') {
76                            normalized.push(chars.next().expect("peeked quote"));
77                        } else {
78                            break;
79                        }
80                    }
81                }
82            }
83            '"' => {
84                // Replace each quote with a space rather than removing it, so
85                // every later token keeps its original offset and diagnostics
86                // point into the SQL the caller actually wrote.
87                normalized.push(' ');
88                while let Some(identifier_ch) = chars.next() {
89                    if identifier_ch == '"' {
90                        if chars.peek() == Some(&'"') {
91                            // An escaped quote is two characters in the input
92                            // and one in the identifier; pad to keep the width.
93                            normalized.push(chars.next().expect("peeked quote"));
94                            normalized.push(' ');
95                        } else {
96                            normalized.push(' ');
97                            break;
98                        }
99                    } else {
100                        normalized.push(identifier_ch);
101                    }
102                }
103            }
104            '-' if chars.peek() == Some(&'-') => {
105                normalized.push(ch);
106                normalized.push(chars.next().expect("peeked comment dash"));
107                for comment_ch in chars.by_ref() {
108                    normalized.push(comment_ch);
109                    if comment_ch == '\n' {
110                        break;
111                    }
112                }
113            }
114            '/' if chars.peek() == Some(&'*') => {
115                normalized.push(ch);
116                normalized.push(chars.next().expect("peeked comment star"));
117                let mut previous = '\0';
118                for comment_ch in chars.by_ref() {
119                    normalized.push(comment_ch);
120                    if previous == '*' && comment_ch == '/' {
121                        break;
122                    }
123                    previous = comment_ch;
124                }
125            }
126            ch if ch.is_ascii_alphabetic() || ch == '_' => {
127                let mut identifier = String::from(ch);
128                while chars
129                    .peek()
130                    .is_some_and(|next| next.is_ascii_alphanumeric() || *next == '_')
131                {
132                    identifier.push(chars.next().expect("peeked identifier character"));
133                }
134                // PostgreSQL folds bare identifiers to lowercase. Delimited
135                // identifiers take the `\"` branch above and keep their exact
136                // spelling for case-sensitive resolution.
137                normalized.push_str(&identifier.to_ascii_lowercase());
138            }
139            _ => normalized.push(ch),
140        }
141    }
142    normalized
143}
144
145fn natural_join_markers(sql: &str) -> Vec<bool> {
146    let mut markers = Vec::new();
147    let mut saw_natural = false;
148    let mut chars = sql.chars().peekable();
149    while let Some(ch) = chars.next() {
150        match ch {
151            '\'' | '"' => skip_quoted(&mut chars, ch),
152            '-' if chars.peek() == Some(&'-') => {
153                chars.next();
154                for comment_ch in chars.by_ref() {
155                    if comment_ch == '\n' {
156                        break;
157                    }
158                }
159            }
160            '/' if chars.peek() == Some(&'*') => {
161                chars.next();
162                let mut previous = '\0';
163                for comment_ch in chars.by_ref() {
164                    if previous == '*' && comment_ch == '/' {
165                        break;
166                    }
167                    previous = comment_ch;
168                }
169            }
170            ';' => saw_natural = false,
171            c if c.is_ascii_alphabetic() || c == '_' => {
172                let mut word = String::from(c);
173                while chars
174                    .peek()
175                    .is_some_and(|next| next.is_ascii_alphanumeric() || *next == '_')
176                {
177                    word.push(chars.next().expect("peeked identifier character"));
178                }
179                match word.to_ascii_lowercase().as_str() {
180                    "natural" => saw_natural = true,
181                    "join" => {
182                        markers.push(saw_natural);
183                        saw_natural = false;
184                    }
185                    _ => {}
186                }
187            }
188            _ => {}
189        }
190    }
191    markers
192}
193
194fn skip_quoted(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, quote: char) {
195    while let Some(ch) = chars.next() {
196        if ch == quote {
197            if chars.peek() == Some(&quote) {
198                chars.next();
199            } else {
200                break;
201            }
202        }
203    }
204}
205
206/// Apply the parser's NATURAL markers to the joins they belong to.
207///
208/// The markers arrive as a flat list alongside the AST, so they only line up
209/// while both sides walk the joins in the same order. A mismatch used to leave
210/// the remaining joins as plain joins, turning `NATURAL JOIN` into a cross
211/// product without any diagnostic. Treat it as the contract violation it is.
212fn annotate_natural_joins(statements: &mut [Statement], natural_markers: Vec<bool>) -> Result<()> {
213    let supplied = natural_markers.len();
214    let mut natural_markers = natural_markers.into_iter();
215    let mut consumed = 0usize;
216    for statement in statements {
217        if let StatementKind::Select(select) = &mut statement.kind {
218            annotate_select_natural_joins(select, &mut natural_markers, &mut consumed);
219        }
220    }
221
222    if consumed != supplied {
223        return Err(ParserError::UnexpectedToken {
224            line: 0,
225            column: 0,
226            expected: format!("{supplied} NATURAL join markers, one per join"),
227            found: format!("{consumed} joins in the AST"),
228        });
229    }
230    Ok(())
231}
232
233fn annotate_select_natural_joins(
234    select: &mut Select,
235    natural_markers: &mut impl Iterator<Item = bool>,
236    consumed: &mut usize,
237) {
238    for item in &mut select.projection {
239        if let SelectItem::Expr { expr, .. } = item {
240            annotate_expr_natural_joins(expr, natural_markers, consumed);
241        }
242    }
243    for from in &mut select.from {
244        annotate_from_natural_joins(from, natural_markers, consumed);
245    }
246    if let Some(selection) = &mut select.selection {
247        annotate_expr_natural_joins(selection, natural_markers, consumed);
248    }
249    if let Some(group_by) = &mut select.group_by {
250        for expression in group_by {
251            annotate_expr_natural_joins(expression, natural_markers, consumed);
252        }
253    }
254    if let Some(having) = &mut select.having {
255        annotate_expr_natural_joins(having, natural_markers, consumed);
256    }
257    for order_by in &mut select.order_by {
258        annotate_expr_natural_joins(&mut order_by.expr, natural_markers, consumed);
259    }
260    if let Some(limit) = &mut select.limit {
261        annotate_expr_natural_joins(limit, natural_markers, consumed);
262    }
263    if let Some(offset) = &mut select.offset {
264        annotate_expr_natural_joins(offset, natural_markers, consumed);
265    }
266}
267
268fn annotate_from_natural_joins(
269    from: &mut FromItem,
270    natural_markers: &mut impl Iterator<Item = bool>,
271    consumed: &mut usize,
272) {
273    match from {
274        FromItem::Join {
275            left,
276            right,
277            natural,
278            ..
279        } => {
280            annotate_from_natural_joins(left, natural_markers, consumed);
281            if let Some(marker) = natural_markers.next() {
282                *natural |= marker;
283                *consumed += 1;
284            }
285            annotate_from_natural_joins(right, natural_markers, consumed);
286        }
287        FromItem::Derived { subquery, .. } => {
288            if let StatementKind::Select(select) = &mut subquery.kind {
289                annotate_select_natural_joins(select, natural_markers, consumed);
290            }
291        }
292        FromItem::Table { .. } => {}
293    }
294}
295
296fn annotate_expr_natural_joins(
297    expr: &mut Expr,
298    natural_markers: &mut impl Iterator<Item = bool>,
299    consumed: &mut usize,
300) {
301    match &mut expr.kind {
302        ExprKind::ScalarSubquery { subquery } | ExprKind::Exists { subquery, .. } => {
303            if let StatementKind::Select(select) = &mut subquery.kind {
304                annotate_select_natural_joins(select, natural_markers, consumed);
305            }
306        }
307        ExprKind::InSubquery { expr, subquery, .. }
308        | ExprKind::Quantified { expr, subquery, .. } => {
309            annotate_expr_natural_joins(expr, natural_markers, consumed);
310            if let StatementKind::Select(select) = &mut subquery.kind {
311                annotate_select_natural_joins(select, natural_markers, consumed);
312            }
313        }
314        ExprKind::BinaryOp { left, right, .. } => {
315            annotate_expr_natural_joins(left, natural_markers, consumed);
316            annotate_expr_natural_joins(right, natural_markers, consumed);
317        }
318        ExprKind::UnaryOp { operand, .. } | ExprKind::IsNull { expr: operand, .. } => {
319            annotate_expr_natural_joins(operand, natural_markers, consumed);
320        }
321        ExprKind::FunctionCall { args, .. } => {
322            for argument in args {
323                annotate_expr_natural_joins(argument, natural_markers, consumed);
324            }
325        }
326        ExprKind::Between {
327            expr, low, high, ..
328        } => {
329            annotate_expr_natural_joins(expr, natural_markers, consumed);
330            annotate_expr_natural_joins(low, natural_markers, consumed);
331            annotate_expr_natural_joins(high, natural_markers, consumed);
332        }
333        ExprKind::Like {
334            expr,
335            pattern,
336            escape,
337            ..
338        } => {
339            annotate_expr_natural_joins(expr, natural_markers, consumed);
340            annotate_expr_natural_joins(pattern, natural_markers, consumed);
341            if let Some(escape) = escape {
342                annotate_expr_natural_joins(escape, natural_markers, consumed);
343            }
344        }
345        ExprKind::InList { expr, list, .. } => {
346            annotate_expr_natural_joins(expr, natural_markers, consumed);
347            for item in list {
348                annotate_expr_natural_joins(item, natural_markers, consumed);
349            }
350        }
351        ExprKind::Cast { expr, .. } => {
352            annotate_expr_natural_joins(expr, natural_markers, consumed);
353        }
354        ExprKind::Literal { .. } | ExprKind::ColumnRef { .. } | ExprKind::VectorLiteral { .. } => {}
355    }
356}
357
358pub fn parse_expression_sql(sql: &str) -> Result<crate::ast::Expr> {
359    let wrapped = format!("SELECT {sql}");
360    let statements = parse_sql(&wrapped)?;
361    let Some(statement) = statements.into_iter().next() else {
362        return Err(empty_expression_error());
363    };
364    let StatementKind::Select(select) = statement.kind else {
365        return Err(empty_expression_error());
366    };
367    let Some(crate::ast::SelectItem::Expr { expr, .. }) = select.projection.into_iter().next()
368    else {
369        return Err(empty_expression_error());
370    };
371    Ok(expr)
372}
373
374fn empty_expression_error() -> ParserError {
375    ParserError::UnexpectedToken {
376        line: 0,
377        column: 0,
378        expected: "expression".to_string(),
379        found: "empty parser result".to_string(),
380    }
381}
382
383// nim-sql-parser/src/alopex_sql_parser.nim の `internalDefectPrefix` と
384// 一致させる。Nim 側の `except Defect` 節が付与する接頭辞で、パーサー
385// 内部の不変条件違反 (通常の構文エラーではない) を機械的に区別するための
386// マーカー。ワイヤ契約 (MessagePack AST) には影響しない、エラー文言のみの
387// 合意。
388const INTERNAL_DEFECT_PREFIX: &str =
389    "internal parser defect (this is a parser bug, not invalid SQL): ";
390
391fn parser_error_from_nim(message: &str) -> ParserError {
392    if let Some(defect_message) = message.strip_prefix(INTERNAL_DEFECT_PREFIX) {
393        return ParserError::InternalParserDefect {
394            message: defect_message.to_string(),
395        };
396    }
397    let (line, column) = parse_nim_line_col(message).unwrap_or((0, 0));
398    ParserError::UnexpectedToken {
399        line,
400        column,
401        expected: "valid SQL".to_string(),
402        found: message.to_string(),
403    }
404}
405
406fn parse_nim_line_col(message: &str) -> Option<(u64, u64)> {
407    let after_line = message.strip_prefix("Parse error at line ")?;
408    let (line, rest) = after_line.split_once(", col ")?;
409    let (col, _) = rest.split_once(':')?;
410    Some((line.parse().ok()?, col.parse().ok()?))
411}