Skip to main content

alopex_sql/
nim_bridge.rs

1use crate::ast::{Statement, StatementKind};
2use crate::error::{ParserError, Result};
3use crate::nim_ffi::{self, OwnedBuffer, ParseResultKind};
4
5/// Return the SQL/PromQL MessagePack wire contract version exported by Nim.
6pub fn parser_contract_version() -> String {
7    nim_ffi::parser_contract_version()
8}
9
10pub fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
11    if sql.as_bytes().contains(&0) {
12        return Err(ParserError::UnexpectedToken {
13            line: 0,
14            column: 0,
15            expected: "valid SQL without interior NUL bytes".to_string(),
16            found: "interior NUL byte".to_string(),
17        });
18    }
19
20    let result = nim_ffi::parse_sql(sql);
21    match result.kind {
22        ParseResultKind::Ok => {
23            let buffer = OwnedBuffer::new(result.buffer_ptr, result.buffer_len);
24            // 正常時の payload は最低でも MessagePack の配列ヘッダ 1 バイトを
25            // 含む。空 payload はゼロ初期化された CParseResult、つまり Nim 側
26            // から例外が漏れた事故 (issue #40 の desync 経路) を意味するため、
27            // 汎用の decode エラーではなく原因が特定できるエラーにする。
28            if buffer.as_slice().is_empty() {
29                return Err(ParserError::UnexpectedToken {
30                    line: 0,
31                    column: 0,
32                    expected: "MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
33                    found: "empty payload from Nim parser (leaked exception at FFI boundary; \
34                            see issue #40)"
35                        .to_string(),
36                });
37            }
38            rmp_serde::from_slice::<Vec<Statement>>(buffer.as_slice()).map_err(|err| {
39                ParserError::UnexpectedToken {
40                    line: 0,
41                    column: 0,
42                    expected: "MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
43                    found: err.to_string(),
44                }
45            })
46        }
47        ParseResultKind::Error => {
48            let buffer = OwnedBuffer::new(result.error_ptr.cast(), result.error_len);
49            Err(parser_error_from_nim(
50                String::from_utf8_lossy(buffer.as_slice()).as_ref(),
51            ))
52        }
53    }
54}
55
56pub fn parse_expression_sql(sql: &str) -> Result<crate::ast::Expr> {
57    let wrapped = format!("SELECT {sql}");
58    let statements = parse_sql(&wrapped)?;
59    let Some(statement) = statements.into_iter().next() else {
60        return Err(empty_expression_error());
61    };
62    let StatementKind::Select(select) = statement.kind else {
63        return Err(empty_expression_error());
64    };
65    let Some(crate::ast::SelectItem::Expr { expr, .. }) = select.projection.into_iter().next()
66    else {
67        return Err(empty_expression_error());
68    };
69    Ok(expr)
70}
71
72fn empty_expression_error() -> ParserError {
73    ParserError::UnexpectedToken {
74        line: 0,
75        column: 0,
76        expected: "expression".to_string(),
77        found: "empty parser result".to_string(),
78    }
79}
80
81// nim-sql-parser/src/alopex_sql_parser.nim の `internalDefectPrefix` と
82// 一致させる。Nim 側の `except Defect` 節が付与する接頭辞で、パーサー
83// 内部の不変条件違反 (通常の構文エラーではない) を機械的に区別するための
84// マーカー。ワイヤ契約 (MessagePack AST) には影響しない、エラー文言のみの
85// 合意。
86const INTERNAL_DEFECT_PREFIX: &str =
87    "internal parser defect (this is a parser bug, not invalid SQL): ";
88
89fn parser_error_from_nim(message: &str) -> ParserError {
90    if let Some(defect_message) = message.strip_prefix(INTERNAL_DEFECT_PREFIX) {
91        return ParserError::InternalParserDefect {
92            message: defect_message.to_string(),
93        };
94    }
95    let (line, column) = parse_nim_line_col(message).unwrap_or((0, 0));
96    ParserError::UnexpectedToken {
97        line,
98        column,
99        expected: "valid SQL".to_string(),
100        found: message.to_string(),
101    }
102}
103
104fn parse_nim_line_col(message: &str) -> Option<(u64, u64)> {
105    let after_line = message.strip_prefix("Parse error at line ")?;
106    let (line, rest) = after_line.split_once(", col ")?;
107    let (col, _) = rest.split_once(':')?;
108    Some((line.parse().ok()?, col.parse().ok()?))
109}