Skip to main content

alopex_sql/
nim_bridge.rs

1use crate::ast::ddl::CreateContinuousAggregate;
2use crate::ast::dml::{FromItem, QueryBody, Select, SelectItem, SetOperation, SetOperator, Values};
3use crate::ast::expr::{Expr, ExprKind};
4use crate::ast::{Location, Span, Statement, StatementKind};
5use crate::error::{ParserError, Result};
6use crate::nim_ffi::{self, OwnedBuffer, ParseResultKind};
7use serde::Deserialize;
8
9const MAX_SQL_INPUT_BYTES: usize = 1_048_576;
10const MAX_MESSAGEPACK_PAYLOAD_BYTES: usize = 1_048_576;
11const MAX_MESSAGEPACK_DEPTH: usize = 128;
12const MAX_MESSAGEPACK_VALUES: usize = 65_536;
13const SELECT_WRAPPER_PREFIX: &str = "SELECT ";
14const PARSER_CONTRACT_DESCRIPTOR: &str = include_str!("../nim-sql-parser/PARSER_CONTRACT_VERSION");
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum InputPreflightError {
18    TooLarge,
19    LengthOverflow,
20    InteriorNul,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24enum MessagePackPreflightError {
25    TooLarge,
26    TooDeep,
27    TooManyValues,
28    Truncated,
29    ReservedMarker,
30    TrailingBytes,
31}
32
33#[derive(Deserialize)]
34#[serde(deny_unknown_fields)]
35struct StagedContinuousAggregateStatement {
36    kind: StagedContinuousAggregateKind,
37    span: Span,
38}
39
40#[derive(Deserialize)]
41#[serde(tag = "variant")]
42enum StagedContinuousAggregateKind {
43    CreateContinuousAggregate(CreateContinuousAggregate),
44}
45
46/// Exact Select wire adapter for the continuous-aggregate payload.
47///
48/// Existing top-level Select statements encode their variant through
49/// `StatementKind`. The nested continuous-aggregate query is a named Select
50/// payload in its own right, so it carries and validates an explicit
51/// `variant: Select` field.
52pub(crate) mod continuous_aggregate_select_wire {
53    use crate::ast::{
54        Expr, FromItem, OrderByExpr, Select, SelectItem, SetOperation, Span, WithClause,
55    };
56    use serde::de::Error as _;
57    use serde::{Deserialize, Deserializer, Serialize, Serializer};
58
59    #[derive(Serialize)]
60    struct SelectWireRef<'a> {
61        variant: &'static str,
62        distinct: bool,
63        projection: &'a [SelectItem],
64        from: &'a [FromItem],
65        selection: &'a Option<Expr>,
66        group_by: Option<Vec<&'a Expr>>,
67        having: &'a Option<Expr>,
68        set_operations: &'a [SetOperation],
69        order_by: &'a [OrderByExpr],
70        limit: &'a Option<Expr>,
71        offset: &'a Option<Expr>,
72        span: Span,
73    }
74
75    #[derive(Deserialize)]
76    #[serde(deny_unknown_fields)]
77    struct SelectWire {
78        variant: String,
79        #[serde(default)]
80        with: Option<WithClause>,
81        distinct: bool,
82        projection: Vec<SelectItem>,
83        from: Vec<FromItem>,
84        selection: Option<Expr>,
85        group_by: Option<Vec<Expr>>,
86        having: Option<Expr>,
87        #[serde(default)]
88        set_operations: Vec<SetOperation>,
89        order_by: Vec<OrderByExpr>,
90        limit: Option<Expr>,
91        offset: Option<Expr>,
92        span: Span,
93    }
94
95    pub(crate) fn serialize<S>(
96        select: &Select,
97        serializer: S,
98    ) -> std::result::Result<S::Ok, S::Error>
99    where
100        S: Serializer,
101    {
102        use serde::ser::Error as _;
103        // The staged wire payload keeps the frozen `[Expr]` group_by shape.
104        // Grouping-set modifiers are rejected by the Nim staging validator
105        // (issue #149, D10), so any other variant here is a producer defect.
106        let group_by = select
107            .group_by
108            .as_ref()
109            .map(|items| {
110                items
111                    .iter()
112                    .map(|item| match item {
113                        crate::ast::GroupByItem::Expr { expr } => Ok(expr),
114                        _ => Err(S::Error::custom(
115                            "staged continuous-aggregate queries cannot carry \
116                             ROLLUP/CUBE/GROUPING SETS",
117                        )),
118                    })
119                    .collect::<std::result::Result<Vec<_>, S::Error>>()
120            })
121            .transpose()?;
122        SelectWireRef {
123            variant: "Select",
124            distinct: select.distinct,
125            projection: &select.projection,
126            from: &select.from,
127            selection: &select.selection,
128            group_by,
129            having: &select.having,
130            set_operations: &select.set_operations,
131            order_by: &select.order_by,
132            limit: &select.limit,
133            offset: &select.offset,
134            span: select.span,
135        }
136        .serialize(serializer)
137    }
138
139    pub(crate) fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Select, D::Error>
140    where
141        D: Deserializer<'de>,
142    {
143        let wire = SelectWire::deserialize(deserializer)?;
144        if wire.variant != "Select" {
145            return Err(D::Error::custom(format!(
146                "expected nested query variant `Select`, found `{}`",
147                wire.variant
148            )));
149        }
150        Ok(Select {
151            with: wire.with,
152            distinct: wire.distinct,
153            // The staged wire payload is frozen and cannot carry DISTINCT ON;
154            // the Nim staging validator rejects it before encoding.
155            distinct_on: Vec::new(),
156            projection: wire.projection,
157            from: wire.from,
158            selection: wire.selection,
159            // The frozen staged wire carries plain expressions; wrap them in
160            // the current GroupByItem::Expr form (issue #149, D10).
161            group_by: wire.group_by.map(|items| {
162                items
163                    .into_iter()
164                    .map(|expr| crate::ast::GroupByItem::Expr { expr })
165                    .collect()
166            }),
167            having: wire.having,
168            windows: Vec::new(),
169            qualify: None,
170            set_operations: wire.set_operations,
171            order_by: wire.order_by,
172            limit: wire.limit,
173            offset: wire.offset,
174            // The staged wire payload is frozen and cannot carry WITH TIES;
175            // the Nim staging validator rejects it before encoding.
176            limit_with_ties: false,
177            span: wire.span,
178        })
179    }
180}
181
182impl CreateContinuousAggregate {
183    /// Decode the staged continuous-aggregate wire shape.
184    ///
185    /// The linked-version check is the same one used by the production parser
186    /// path and always runs before payload preflight.
187    #[doc(hidden)]
188    pub fn decode_staged_messagepack(linked_parser_contract: &str, payload: &[u8]) -> Result<Self> {
189        ensure_linked_parser_contract(linked_parser_contract)?;
190        validate_bounded_messagepack(payload).map_err(messagepack_preflight_error)?;
191        let decoded = rmp_serde::from_slice::<StagedContinuousAggregateStatement>(payload)
192            .map_err(messagepack_decode_error)?;
193        let StagedContinuousAggregateKind::CreateContinuousAggregate(statement) = decoded.kind;
194        if decoded.span != statement.span {
195            return Err(ParserError::UnexpectedToken {
196                line: 0,
197                column: 0,
198                expected: "matching outer and kind spans in MessagePack AST".to_string(),
199                found: "continuous aggregate outer span differs from kind span".to_string(),
200            });
201        }
202        Ok(statement)
203    }
204}
205
206/// Return the SQL/PromQL MessagePack wire contract version exported by Nim.
207pub fn parser_contract_version() -> String {
208    nim_ffi::parser_contract_version()
209}
210
211pub fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
212    preflight_input(sql, 0).map_err(parser_error_from_preflight)?;
213    parse_sql_preflighted(sql)
214}
215
216fn parse_sql_preflighted(sql: &str) -> Result<Vec<Statement>> {
217    let tokens = scan_top_level_tokens(sql);
218    let ranges = top_level_statement_ranges(sql, &tokens);
219    let contains_set_operation = ranges.iter().any(|(start, end)| {
220        tokens.iter().any(|token| {
221            token.start >= *start
222                && token.end <= *end
223                && matches!(token.kind, TopLevelTokenKind::Word)
224                && set_operator(&sql[token.start..token.end]).is_some()
225        })
226    });
227    if contains_set_operation
228        && let Ok(statements) = parse_sql_via_ffi(sql)
229        && statements.len() == ranges.len()
230    {
231        return Ok(statements);
232    }
233    if let Some(statements) = parse_set_operation_batch(sql)? {
234        return Ok(statements);
235    }
236    parse_sql_via_ffi(sql)
237}
238
239fn parse_sql_via_ffi(sql: &str) -> Result<Vec<Statement>> {
240    ensure_linked_parser_contract(&nim_ffi::parser_contract_version())?;
241    let natural_join_markers = natural_join_markers(sql);
242    // Option (a): double-quoted tokens are identifiers under SQL standard and
243    // PostgreSQL rules. The currently deployed Nim lexer predates that contract
244    // and emits them as string literals, so normalize the FFI input until every
245    // parser binary has the corrected token kind.
246    let normalized_sql = normalize_quoted_identifiers(sql);
247    let result = nim_ffi::parse_sql(&normalized_sql).map_err(parser_error_from_ffi_input)?;
248    match result.kind {
249        ParseResultKind::Ok => {
250            let buffer = OwnedBuffer::new(result.buffer_ptr, result.buffer_len);
251            // 正常時の payload は最低でも MessagePack の配列ヘッダ 1 バイトを
252            // 含む。空 payload はゼロ初期化された CParseResult、つまり Nim 側
253            // から例外が漏れた事故 (issue #40 の desync 経路) を意味するため、
254            // 汎用の decode エラーではなく原因が特定できるエラーにする。
255            if buffer.as_slice().is_empty() {
256                return Err(ParserError::UnexpectedToken {
257                    line: 0,
258                    column: 0,
259                    expected: "MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
260                    found: "empty payload from Nim parser (leaked exception at FFI boundary; \
261                            see issue #40)"
262                        .to_string(),
263                });
264            }
265            validate_bounded_messagepack(buffer.as_slice()).map_err(messagepack_preflight_error)?;
266            let mut statements = rmp_serde::from_slice::<Vec<Statement>>(buffer.as_slice())
267                .map_err(messagepack_decode_error)?;
268            annotate_natural_joins(&mut statements, natural_join_markers)?;
269            Ok(statements)
270        }
271        ParseResultKind::Error => {
272            let buffer = OwnedBuffer::new(result.error_ptr.cast(), result.error_len);
273            Err(parser_error_from_nim(
274                String::from_utf8_lossy(buffer.as_slice()).as_ref(),
275            ))
276        }
277    }
278}
279
280#[derive(Debug, Clone, Copy)]
281enum TopLevelTokenKind {
282    Word,
283    Semicolon,
284}
285
286#[derive(Debug, Clone, Copy)]
287struct TopLevelToken {
288    kind: TopLevelTokenKind,
289    start: usize,
290    end: usize,
291}
292
293#[derive(Debug, Clone, Copy)]
294struct SetOperationSpec {
295    operator: SetOperator,
296    all: bool,
297    span: Span,
298}
299
300fn parse_set_operation_batch(sql: &str) -> Result<Option<Vec<Statement>>> {
301    let tokens = scan_top_level_tokens(sql);
302    let ranges = top_level_statement_ranges(sql, &tokens);
303
304    let contains_set_operation = ranges.iter().any(|(start, end)| {
305        tokens.iter().any(|token| {
306            token.start >= *start
307                && token.end <= *end
308                && matches!(token.kind, TopLevelTokenKind::Word)
309                && set_operator(&sql[token.start..token.end]).is_some()
310        })
311    });
312    if !contains_set_operation {
313        return Ok(None);
314    }
315
316    let mut statements = Vec::new();
317    for (start, end) in ranges {
318        let words = tokens
319            .iter()
320            .copied()
321            .filter(|token| {
322                token.start >= start
323                    && token.end <= end
324                    && matches!(token.kind, TopLevelTokenKind::Word)
325            })
326            .collect::<Vec<_>>();
327        if words
328            .iter()
329            .any(|word| set_operator(&sql[word.start..word.end]).is_some())
330        {
331            statements.push(parse_set_operation_statement(sql, start, end, &words)?);
332        } else {
333            statements.extend(parse_sql_via_ffi(&sql[start..end])?);
334        }
335    }
336    Ok(Some(statements))
337}
338
339fn top_level_statement_ranges(sql: &str, tokens: &[TopLevelToken]) -> Vec<(usize, usize)> {
340    let mut ranges = Vec::new();
341    let mut start = 0;
342    for token in tokens {
343        if matches!(token.kind, TopLevelTokenKind::Semicolon) {
344            if !sql[start..token.start].trim().is_empty() {
345                ranges.push((start, token.start));
346            }
347            start = token.end;
348        }
349    }
350    if !sql[start..].trim().is_empty() {
351        ranges.push((start, sql.len()));
352    }
353    ranges
354}
355
356fn parse_set_operation_statement(
357    sql: &str,
358    statement_start: usize,
359    statement_end: usize,
360    words: &[TopLevelToken],
361) -> Result<Statement> {
362    let mut branch_ranges = Vec::new();
363    let mut operations = Vec::new();
364    let mut branch_start = statement_start;
365
366    for (index, word) in words.iter().enumerate() {
367        let Some(operator) = set_operator(&sql[word.start..word.end]) else {
368            continue;
369        };
370        branch_ranges.push((branch_start, word.start));
371        let all_word = words
372            .get(index + 1)
373            .filter(|next| sql[next.start..next.end].eq_ignore_ascii_case("all"));
374        let all = all_word.is_some();
375        branch_start = all_word.map_or(word.end, |token| token.end);
376        operations.push(SetOperationSpec {
377            operator,
378            all,
379            span: span_for_offsets(sql, word.start, word.end),
380        });
381    }
382    branch_ranges.push((branch_start, statement_end));
383
384    if branch_ranges.len() != operations.len() + 1 {
385        return Err(set_operation_parser_error(
386            "a SELECT query on both sides of every set operator",
387            "malformed set-operation chain",
388        ));
389    }
390
391    let mut branches = branch_ranges
392        .into_iter()
393        .map(|(start, end)| parse_query_body_fragment(&sql[start..end]))
394        .collect::<Result<Vec<_>>>()?;
395    let final_branch = branches.last_mut().ok_or_else(|| {
396        set_operation_parser_error("at least one query body", "empty set-operation chain")
397    })?;
398    let tail = take_query_tail(final_branch);
399
400    let mut terms = Vec::new();
401    let mut outer_operations = Vec::new();
402    let mut current = branches.remove(0);
403    for (operation, right) in operations.into_iter().zip(branches) {
404        if operation.operator == SetOperator::Intersect {
405            query_set_operations_mut(&mut current).push(SetOperation {
406                operator: operation.operator,
407                all: operation.all,
408                right: Box::new(right),
409                span: operation.span,
410            });
411        } else {
412            terms.push(current);
413            outer_operations.push(operation);
414            current = right;
415        }
416    }
417    terms.push(current);
418
419    let mut root = terms.remove(0);
420    for (operation, right) in outer_operations.into_iter().zip(terms) {
421        query_set_operations_mut(&mut root).push(SetOperation {
422            operator: operation.operator,
423            all: operation.all,
424            right: Box::new(right),
425            span: operation.span,
426        });
427    }
428    set_query_tail(&mut root, tail);
429    let span = query_body_span(&root);
430    Ok(Statement {
431        kind: match root {
432            QueryBody::Select(select) => StatementKind::Select(select),
433            QueryBody::Values(values) => StatementKind::Values(values),
434        },
435        span,
436    })
437}
438
439fn parse_query_body_fragment(sql: &str) -> Result<QueryBody> {
440    let mut statements = parse_sql_via_ffi(sql.trim())?;
441    if statements.len() != 1 {
442        return Err(set_operation_parser_error(
443            "exactly one query body per set-operation input",
444            format!("{} statements", statements.len()),
445        ));
446    }
447    match statements.remove(0).kind {
448        StatementKind::Select(select) => Ok(QueryBody::Select(select)),
449        StatementKind::Values(values) => Ok(QueryBody::Values(values)),
450        _ => Err(set_operation_parser_error(
451            "SELECT or VALUES query in set operation",
452            "non-query statement",
453        )),
454    }
455}
456
457fn query_set_operations_mut(body: &mut QueryBody) -> &mut Vec<SetOperation> {
458    match body {
459        QueryBody::Select(select) => &mut select.set_operations,
460        QueryBody::Values(values) => &mut values.set_operations,
461    }
462}
463
464type QueryTail = (
465    Vec<crate::ast::OrderByExpr>,
466    Option<Expr>,
467    Option<Expr>,
468    bool,
469);
470
471fn take_query_tail(body: &mut QueryBody) -> QueryTail {
472    match body {
473        QueryBody::Select(select) => (
474            std::mem::take(&mut select.order_by),
475            select.limit.take(),
476            select.offset.take(),
477            std::mem::take(&mut select.limit_with_ties),
478        ),
479        QueryBody::Values(values) => (
480            std::mem::take(&mut values.order_by),
481            values.limit.take(),
482            values.offset.take(),
483            std::mem::take(&mut values.limit_with_ties),
484        ),
485    }
486}
487
488fn set_query_tail(body: &mut QueryBody, tail: QueryTail) {
489    let (order_by, limit, offset, limit_with_ties) = tail;
490    match body {
491        QueryBody::Select(select) => {
492            select.order_by = order_by;
493            select.limit = limit;
494            select.offset = offset;
495            select.limit_with_ties = limit_with_ties;
496        }
497        QueryBody::Values(values) => {
498            values.order_by = order_by;
499            values.limit = limit;
500            values.offset = offset;
501            values.limit_with_ties = limit_with_ties;
502        }
503    }
504}
505
506fn query_body_span(body: &QueryBody) -> Span {
507    match body {
508        QueryBody::Select(select) => select.span,
509        QueryBody::Values(values) => values.span,
510    }
511}
512
513fn set_operator(operator: &str) -> Option<SetOperator> {
514    if operator.eq_ignore_ascii_case("union") {
515        Some(SetOperator::Union)
516    } else if operator.eq_ignore_ascii_case("intersect") {
517        Some(SetOperator::Intersect)
518    } else if operator.eq_ignore_ascii_case("except") {
519        Some(SetOperator::Except)
520    } else {
521        None
522    }
523}
524
525fn set_operation_parser_error(
526    expected: impl Into<String>,
527    found: impl Into<String>,
528) -> ParserError {
529    ParserError::UnexpectedToken {
530        line: 0,
531        column: 0,
532        expected: expected.into(),
533        found: found.into(),
534    }
535}
536
537fn span_for_offsets(sql: &str, start: usize, end: usize) -> Span {
538    fn location(sql: &str, offset: usize) -> Location {
539        let prefix = &sql[..offset.min(sql.len())];
540        let line = prefix.bytes().filter(|byte| *byte == b'\n').count() as u64 + 1;
541        let column = prefix
542            .rsplit_once('\n')
543            .map_or(prefix.len(), |(_, tail)| tail.len()) as u64
544            + 1;
545        Location::new(line, column)
546    }
547    Span::new(location(sql, start), location(sql, end.saturating_sub(1)))
548}
549
550fn scan_top_level_tokens(sql: &str) -> Vec<TopLevelToken> {
551    let bytes = sql.as_bytes();
552    let mut tokens = Vec::new();
553    let mut index = 0;
554    let mut depth = 0_u32;
555
556    while index < bytes.len() {
557        match bytes[index] {
558            b'\'' | b'"' => {
559                let quote = bytes[index];
560                index += 1;
561                while index < bytes.len() {
562                    if bytes[index] == quote {
563                        if bytes.get(index + 1) == Some(&quote) {
564                            index += 2;
565                        } else {
566                            index += 1;
567                            break;
568                        }
569                    } else {
570                        index += 1;
571                    }
572                }
573            }
574            b'-' if bytes.get(index + 1) == Some(&b'-') => {
575                index += 2;
576                while index < bytes.len() && bytes[index] != b'\n' {
577                    index += 1;
578                }
579            }
580            b'/' if bytes.get(index + 1) == Some(&b'*') => {
581                index += 2;
582                while index + 1 < bytes.len() && !(bytes[index] == b'*' && bytes[index + 1] == b'/')
583                {
584                    index += 1;
585                }
586                index = (index + 2).min(bytes.len());
587            }
588            b'(' => {
589                depth += 1;
590                index += 1;
591            }
592            b')' => {
593                depth = depth.saturating_sub(1);
594                index += 1;
595            }
596            b';' if depth == 0 => {
597                tokens.push(TopLevelToken {
598                    kind: TopLevelTokenKind::Semicolon,
599                    start: index,
600                    end: index + 1,
601                });
602                index += 1;
603            }
604            byte if depth == 0 && (byte.is_ascii_alphabetic() || byte == b'_') => {
605                let start = index;
606                index += 1;
607                while index < bytes.len()
608                    && (bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_')
609                {
610                    index += 1;
611                }
612                tokens.push(TopLevelToken {
613                    kind: TopLevelTokenKind::Word,
614                    start,
615                    end: index,
616                });
617            }
618            _ => index += 1,
619        }
620    }
621    tokens
622}
623
624fn expected_parser_contract() -> &'static str {
625    PARSER_CONTRACT_DESCRIPTOR.trim()
626}
627
628fn ensure_linked_parser_contract(linked_parser_contract: &str) -> Result<()> {
629    ensure_parser_contract(expected_parser_contract(), linked_parser_contract)
630}
631
632fn ensure_parser_contract(expected: &str, linked_parser_contract: &str) -> Result<()> {
633    if linked_parser_contract == expected {
634        return Ok(());
635    }
636    Err(ParserError::UnexpectedToken {
637        line: 0,
638        column: 0,
639        expected: format!("linked Nim parser contract {expected}"),
640        found: format!("linked Nim parser contract {linked_parser_contract}"),
641    })
642}
643
644fn messagepack_decode_error(error: rmp_serde::decode::Error) -> ParserError {
645    ParserError::UnexpectedToken {
646        line: 0,
647        column: 0,
648        expected: "bounded MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
649        found: error.to_string(),
650    }
651}
652
653fn messagepack_preflight_error(error: MessagePackPreflightError) -> ParserError {
654    let found = match error {
655        MessagePackPreflightError::TooLarge => {
656            format!("MessagePack payload exceeds {MAX_MESSAGEPACK_PAYLOAD_BYTES} bytes")
657        }
658        MessagePackPreflightError::TooDeep => {
659            format!("MessagePack nesting exceeds {MAX_MESSAGEPACK_DEPTH} levels")
660        }
661        MessagePackPreflightError::TooManyValues => {
662            format!("MessagePack collection limit of {MAX_MESSAGEPACK_VALUES} values exceeded")
663        }
664        MessagePackPreflightError::Truncated => "truncated MessagePack payload".to_string(),
665        MessagePackPreflightError::ReservedMarker => "reserved MessagePack marker 0xc1".to_string(),
666        MessagePackPreflightError::TrailingBytes => {
667            "trailing bytes after MessagePack payload".to_string()
668        }
669    };
670    ParserError::UnexpectedToken {
671        line: 0,
672        column: 0,
673        expected: "bounded MessagePack AST matching docs/ffi-ast-contract.md".to_string(),
674        found,
675    }
676}
677
678fn validate_bounded_messagepack(
679    payload: &[u8],
680) -> std::result::Result<(), MessagePackPreflightError> {
681    if payload.len() > MAX_MESSAGEPACK_PAYLOAD_BYTES {
682        return Err(MessagePackPreflightError::TooLarge);
683    }
684    let mut scanner = MessagePackScanner {
685        payload,
686        position: 0,
687        values: 0,
688    };
689    scanner.scan_value(1)?;
690    if scanner.position != payload.len() {
691        return Err(MessagePackPreflightError::TrailingBytes);
692    }
693    Ok(())
694}
695
696struct MessagePackScanner<'a> {
697    payload: &'a [u8],
698    position: usize,
699    values: usize,
700}
701
702impl MessagePackScanner<'_> {
703    fn scan_value(&mut self, depth: usize) -> std::result::Result<(), MessagePackPreflightError> {
704        if depth > MAX_MESSAGEPACK_DEPTH {
705            return Err(MessagePackPreflightError::TooDeep);
706        }
707        self.values = self
708            .values
709            .checked_add(1)
710            .ok_or(MessagePackPreflightError::TooManyValues)?;
711        if self.values > MAX_MESSAGEPACK_VALUES {
712            return Err(MessagePackPreflightError::TooManyValues);
713        }
714
715        let marker = self.read_byte()?;
716        match marker {
717            0x00..=0x7f | 0xc0 | 0xc2 | 0xc3 | 0xe0..=0xff => Ok(()),
718            0x80..=0x8f => self.scan_map(usize::from(marker & 0x0f), depth),
719            0x90..=0x9f => self.scan_children(usize::from(marker & 0x0f), depth),
720            0xa0..=0xbf => self.skip(usize::from(marker & 0x1f)),
721            0xc1 => Err(MessagePackPreflightError::ReservedMarker),
722            0xc4 | 0xd9 => {
723                let length = usize::from(self.read_byte()?);
724                self.skip(length)
725            }
726            0xc5 | 0xda => {
727                let length = usize::from(self.read_u16()?);
728                self.skip(length)
729            }
730            0xc6 | 0xdb => {
731                let length = usize::try_from(self.read_u32()?)
732                    .map_err(|_| MessagePackPreflightError::TooLarge)?;
733                self.skip(length)
734            }
735            0xc7 => {
736                let length = usize::from(self.read_byte()?);
737                self.skip_ext(length)
738            }
739            0xc8 => {
740                let length = usize::from(self.read_u16()?);
741                self.skip_ext(length)
742            }
743            0xc9 => {
744                let length = usize::try_from(self.read_u32()?)
745                    .map_err(|_| MessagePackPreflightError::TooLarge)?;
746                self.skip_ext(length)
747            }
748            0xca => self.skip(4),
749            0xcb => self.skip(8),
750            0xcc | 0xd0 => self.skip(1),
751            0xcd | 0xd1 => self.skip(2),
752            0xce | 0xd2 => self.skip(4),
753            0xcf | 0xd3 => self.skip(8),
754            0xd4 => self.skip_ext(1),
755            0xd5 => self.skip_ext(2),
756            0xd6 => self.skip_ext(4),
757            0xd7 => self.skip_ext(8),
758            0xd8 => self.skip_ext(16),
759            0xdc => {
760                let count = usize::from(self.read_u16()?);
761                self.scan_children(count, depth)
762            }
763            0xdd => {
764                let count = usize::try_from(self.read_u32()?)
765                    .map_err(|_| MessagePackPreflightError::TooManyValues)?;
766                self.scan_children(count, depth)
767            }
768            0xde => {
769                let count = usize::from(self.read_u16()?);
770                self.scan_map(count, depth)
771            }
772            0xdf => {
773                let count = usize::try_from(self.read_u32()?)
774                    .map_err(|_| MessagePackPreflightError::TooManyValues)?;
775                self.scan_map(count, depth)
776            }
777        }
778    }
779
780    fn scan_map(
781        &mut self,
782        entries: usize,
783        depth: usize,
784    ) -> std::result::Result<(), MessagePackPreflightError> {
785        let children = entries
786            .checked_mul(2)
787            .ok_or(MessagePackPreflightError::TooManyValues)?;
788        self.scan_children(children, depth)
789    }
790
791    fn scan_children(
792        &mut self,
793        children: usize,
794        depth: usize,
795    ) -> std::result::Result<(), MessagePackPreflightError> {
796        if children > MAX_MESSAGEPACK_VALUES {
797            return Err(MessagePackPreflightError::TooManyValues);
798        }
799        for _ in 0..children {
800            self.scan_value(depth + 1)?;
801        }
802        Ok(())
803    }
804
805    fn skip_ext(
806        &mut self,
807        payload_length: usize,
808    ) -> std::result::Result<(), MessagePackPreflightError> {
809        let total = payload_length
810            .checked_add(1)
811            .ok_or(MessagePackPreflightError::TooLarge)?;
812        self.skip(total)
813    }
814
815    fn read_byte(&mut self) -> std::result::Result<u8, MessagePackPreflightError> {
816        let byte = *self
817            .payload
818            .get(self.position)
819            .ok_or(MessagePackPreflightError::Truncated)?;
820        self.position += 1;
821        Ok(byte)
822    }
823
824    fn read_u16(&mut self) -> std::result::Result<u16, MessagePackPreflightError> {
825        let bytes = self.take(2)?;
826        Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
827    }
828
829    fn read_u32(&mut self) -> std::result::Result<u32, MessagePackPreflightError> {
830        let bytes = self.take(4)?;
831        Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
832    }
833
834    fn skip(&mut self, length: usize) -> std::result::Result<(), MessagePackPreflightError> {
835        self.take(length).map(|_| ())
836    }
837
838    fn take(&mut self, length: usize) -> std::result::Result<&[u8], MessagePackPreflightError> {
839        let end = self
840            .position
841            .checked_add(length)
842            .ok_or(MessagePackPreflightError::Truncated)?;
843        let bytes = self
844            .payload
845            .get(self.position..end)
846            .ok_or(MessagePackPreflightError::Truncated)?;
847        self.position = end;
848        Ok(bytes)
849    }
850}
851
852fn normalize_quoted_identifiers(sql: &str) -> String {
853    let mut normalized = String::with_capacity(sql.len());
854    let mut chars = sql.chars().peekable();
855    while let Some(ch) = chars.next() {
856        match ch {
857            '\'' => {
858                normalized.push(ch);
859                while let Some(string_ch) = chars.next() {
860                    normalized.push(string_ch);
861                    if string_ch == '\'' {
862                        if chars.peek() == Some(&'\'') {
863                            normalized.push(chars.next().expect("peeked quote"));
864                        } else {
865                            break;
866                        }
867                    }
868                }
869            }
870            '"' => {
871                // Replace each quote with a space rather than removing it, so
872                // every later token keeps its original offset and diagnostics
873                // point into the SQL the caller actually wrote.
874                normalized.push(' ');
875                while let Some(identifier_ch) = chars.next() {
876                    if identifier_ch == '"' {
877                        if chars.peek() == Some(&'"') {
878                            // An escaped quote is two characters in the input
879                            // and one in the identifier; pad to keep the width.
880                            normalized.push(chars.next().expect("peeked quote"));
881                            normalized.push(' ');
882                        } else {
883                            normalized.push(' ');
884                            break;
885                        }
886                    } else {
887                        normalized.push(identifier_ch);
888                    }
889                }
890            }
891            '-' if chars.peek() == Some(&'-') => {
892                normalized.push(ch);
893                normalized.push(chars.next().expect("peeked comment dash"));
894                for comment_ch in chars.by_ref() {
895                    normalized.push(comment_ch);
896                    if comment_ch == '\n' {
897                        break;
898                    }
899                }
900            }
901            '/' if chars.peek() == Some(&'*') => {
902                normalized.push(ch);
903                normalized.push(chars.next().expect("peeked comment star"));
904                let mut previous = '\0';
905                for comment_ch in chars.by_ref() {
906                    normalized.push(comment_ch);
907                    if previous == '*' && comment_ch == '/' {
908                        break;
909                    }
910                    previous = comment_ch;
911                }
912            }
913            ch if ch.is_ascii_alphabetic() || ch == '_' => {
914                let mut identifier = String::from(ch);
915                while chars
916                    .peek()
917                    .is_some_and(|next| next.is_ascii_alphanumeric() || *next == '_')
918                {
919                    identifier.push(chars.next().expect("peeked identifier character"));
920                }
921                // PostgreSQL folds bare identifiers to lowercase. Delimited
922                // identifiers take the `\"` branch above and keep their exact
923                // spelling for case-sensitive resolution.
924                normalized.push_str(&identifier.to_ascii_lowercase());
925            }
926            _ => normalized.push(ch),
927        }
928    }
929    normalized
930}
931
932fn natural_join_markers(sql: &str) -> Vec<bool> {
933    let mut markers = Vec::new();
934    let mut saw_natural = false;
935    let mut chars = sql.chars().peekable();
936    while let Some(ch) = chars.next() {
937        match ch {
938            '\'' | '"' => skip_quoted(&mut chars, ch),
939            '-' if chars.peek() == Some(&'-') => {
940                chars.next();
941                for comment_ch in chars.by_ref() {
942                    if comment_ch == '\n' {
943                        break;
944                    }
945                }
946            }
947            '/' if chars.peek() == Some(&'*') => {
948                chars.next();
949                let mut previous = '\0';
950                for comment_ch in chars.by_ref() {
951                    if previous == '*' && comment_ch == '/' {
952                        break;
953                    }
954                    previous = comment_ch;
955                }
956            }
957            ';' => saw_natural = false,
958            c if c.is_ascii_alphabetic() || c == '_' => {
959                let mut word = String::from(c);
960                while chars
961                    .peek()
962                    .is_some_and(|next| next.is_ascii_alphanumeric() || *next == '_')
963                {
964                    word.push(chars.next().expect("peeked identifier character"));
965                }
966                match word.to_ascii_lowercase().as_str() {
967                    "natural" => saw_natural = true,
968                    "join" => {
969                        markers.push(saw_natural);
970                        saw_natural = false;
971                    }
972                    _ => {}
973                }
974            }
975            _ => {}
976        }
977    }
978    markers
979}
980
981fn skip_quoted(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, quote: char) {
982    while let Some(ch) = chars.next() {
983        if ch == quote {
984            if chars.peek() == Some(&quote) {
985                chars.next();
986            } else {
987                break;
988            }
989        }
990    }
991}
992
993/// Apply the parser's NATURAL markers to the joins they belong to.
994///
995/// The markers arrive as a flat list alongside the AST, so they only line up
996/// while both sides walk the joins in the same order. A mismatch used to leave
997/// the remaining joins as plain joins, turning `NATURAL JOIN` into a cross
998/// product without any diagnostic. Treat it as the contract violation it is.
999fn annotate_natural_joins(statements: &mut [Statement], natural_markers: Vec<bool>) -> Result<()> {
1000    let supplied = natural_markers.len();
1001    let mut natural_markers = natural_markers.into_iter();
1002    let mut consumed = 0usize;
1003    for statement in statements {
1004        match &mut statement.kind {
1005            StatementKind::Select(select) => {
1006                annotate_select_natural_joins(select, &mut natural_markers, &mut consumed);
1007            }
1008            StatementKind::Values(values) => {
1009                annotate_values_natural_joins(values, &mut natural_markers, &mut consumed);
1010            }
1011            _ => {}
1012        }
1013    }
1014
1015    if consumed != supplied {
1016        return Err(ParserError::UnexpectedToken {
1017            line: 0,
1018            column: 0,
1019            expected: format!("{supplied} NATURAL join markers, one per join"),
1020            found: format!("{consumed} joins in the AST"),
1021        });
1022    }
1023    Ok(())
1024}
1025
1026fn annotate_select_natural_joins(
1027    select: &mut Select,
1028    natural_markers: &mut impl Iterator<Item = bool>,
1029    consumed: &mut usize,
1030) {
1031    if let Some(with) = &mut select.with {
1032        for cte in &mut with.ctes {
1033            annotate_query_body_natural_joins(&mut cte.query, natural_markers, consumed);
1034        }
1035    }
1036    for item in &mut select.projection {
1037        if let SelectItem::Expr { expr, .. } = item {
1038            annotate_expr_natural_joins(expr, natural_markers, consumed);
1039        }
1040    }
1041    for from in &mut select.from {
1042        annotate_from_natural_joins(from, natural_markers, consumed);
1043    }
1044    if let Some(selection) = &mut select.selection {
1045        annotate_expr_natural_joins(selection, natural_markers, consumed);
1046    }
1047    if let Some(group_by) = &mut select.group_by {
1048        for item in group_by {
1049            for expression in item.exprs_mut() {
1050                annotate_expr_natural_joins(expression, natural_markers, consumed);
1051            }
1052        }
1053    }
1054    if let Some(having) = &mut select.having {
1055        annotate_expr_natural_joins(having, natural_markers, consumed);
1056    }
1057    for operation in &mut select.set_operations {
1058        annotate_query_body_natural_joins(&mut operation.right, natural_markers, consumed);
1059    }
1060    for order_by in &mut select.order_by {
1061        annotate_expr_natural_joins(&mut order_by.expr, natural_markers, consumed);
1062    }
1063    if let Some(limit) = &mut select.limit {
1064        annotate_expr_natural_joins(limit, natural_markers, consumed);
1065    }
1066    if let Some(offset) = &mut select.offset {
1067        annotate_expr_natural_joins(offset, natural_markers, consumed);
1068    }
1069}
1070
1071fn annotate_values_natural_joins(
1072    values: &mut Values,
1073    natural_markers: &mut impl Iterator<Item = bool>,
1074    consumed: &mut usize,
1075) {
1076    if let Some(with) = &mut values.with {
1077        for cte in &mut with.ctes {
1078            annotate_query_body_natural_joins(&mut cte.query, natural_markers, consumed);
1079        }
1080    }
1081    for row in &mut values.rows {
1082        for expr in row {
1083            annotate_expr_natural_joins(expr, natural_markers, consumed);
1084        }
1085    }
1086    for operation in &mut values.set_operations {
1087        annotate_query_body_natural_joins(&mut operation.right, natural_markers, consumed);
1088    }
1089    for order_by in &mut values.order_by {
1090        annotate_expr_natural_joins(&mut order_by.expr, natural_markers, consumed);
1091    }
1092    if let Some(limit) = &mut values.limit {
1093        annotate_expr_natural_joins(limit, natural_markers, consumed);
1094    }
1095    if let Some(offset) = &mut values.offset {
1096        annotate_expr_natural_joins(offset, natural_markers, consumed);
1097    }
1098}
1099
1100fn annotate_query_body_natural_joins(
1101    body: &mut QueryBody,
1102    natural_markers: &mut impl Iterator<Item = bool>,
1103    consumed: &mut usize,
1104) {
1105    match body {
1106        QueryBody::Select(select) => {
1107            annotate_select_natural_joins(select, natural_markers, consumed);
1108        }
1109        QueryBody::Values(values) => {
1110            annotate_values_natural_joins(values, natural_markers, consumed);
1111        }
1112    }
1113}
1114
1115fn annotate_from_natural_joins(
1116    from: &mut FromItem,
1117    natural_markers: &mut impl Iterator<Item = bool>,
1118    consumed: &mut usize,
1119) {
1120    match from {
1121        FromItem::Join {
1122            left,
1123            right,
1124            natural,
1125            ..
1126        } => {
1127            annotate_from_natural_joins(left, natural_markers, consumed);
1128            if let Some(marker) = natural_markers.next() {
1129                *natural |= marker;
1130                *consumed += 1;
1131            }
1132            annotate_from_natural_joins(right, natural_markers, consumed);
1133        }
1134        FromItem::Derived { subquery, .. } => {
1135            annotate_query_body_natural_joins(subquery, natural_markers, consumed);
1136        }
1137        FromItem::Function { args, .. } => {
1138            // Arguments are ordinary expressions and may hold subqueries, so
1139            // they consume markers in the parser's emission order.
1140            for arg in args {
1141                annotate_expr_natural_joins(arg, natural_markers, consumed);
1142            }
1143        }
1144        FromItem::Table { .. } => {}
1145    }
1146}
1147
1148fn annotate_expr_natural_joins(
1149    expr: &mut Expr,
1150    natural_markers: &mut impl Iterator<Item = bool>,
1151    consumed: &mut usize,
1152) {
1153    match &mut expr.kind {
1154        ExprKind::ScalarSubquery { subquery } | ExprKind::Exists { subquery, .. } => {
1155            if let StatementKind::Select(select) = &mut subquery.kind {
1156                annotate_select_natural_joins(select, natural_markers, consumed);
1157            }
1158        }
1159        ExprKind::InSubquery { expr, subquery, .. }
1160        | ExprKind::Quantified { expr, subquery, .. } => {
1161            annotate_expr_natural_joins(expr, natural_markers, consumed);
1162            if let StatementKind::Select(select) = &mut subquery.kind {
1163                annotate_select_natural_joins(select, natural_markers, consumed);
1164            }
1165        }
1166        ExprKind::BinaryOp { left, right, .. } => {
1167            annotate_expr_natural_joins(left, natural_markers, consumed);
1168            annotate_expr_natural_joins(right, natural_markers, consumed);
1169        }
1170        ExprKind::UnaryOp { operand, .. } | ExprKind::IsNull { expr: operand, .. } => {
1171            annotate_expr_natural_joins(operand, natural_markers, consumed);
1172        }
1173        ExprKind::TruthPredicate { expr, .. } => {
1174            annotate_expr_natural_joins(expr, natural_markers, consumed);
1175        }
1176        ExprKind::IsDistinctFrom { left, right, .. } => {
1177            annotate_expr_natural_joins(left, natural_markers, consumed);
1178            annotate_expr_natural_joins(right, natural_markers, consumed);
1179        }
1180        ExprKind::Row { items } => {
1181            for item in items {
1182                annotate_expr_natural_joins(item, natural_markers, consumed);
1183            }
1184        }
1185        ExprKind::Case {
1186            operand,
1187            branches,
1188            else_expr,
1189        } => {
1190            if let Some(operand) = operand {
1191                annotate_expr_natural_joins(operand, natural_markers, consumed);
1192            }
1193            for branch in branches {
1194                annotate_expr_natural_joins(&mut branch.when, natural_markers, consumed);
1195                annotate_expr_natural_joins(&mut branch.then, natural_markers, consumed);
1196            }
1197            if let Some(else_expr) = else_expr {
1198                annotate_expr_natural_joins(else_expr, natural_markers, consumed);
1199            }
1200        }
1201        ExprKind::FunctionCall {
1202            args,
1203            order_by,
1204            within_group,
1205            filter,
1206            ..
1207        } => {
1208            for argument in args {
1209                annotate_expr_natural_joins(argument, natural_markers, consumed);
1210            }
1211            for order in order_by {
1212                annotate_expr_natural_joins(&mut order.expr, natural_markers, consumed);
1213            }
1214            for order in within_group {
1215                annotate_expr_natural_joins(&mut order.expr, natural_markers, consumed);
1216            }
1217            if let Some(filter) = filter {
1218                annotate_expr_natural_joins(filter, natural_markers, consumed);
1219            }
1220        }
1221        ExprKind::Between {
1222            expr, low, high, ..
1223        } => {
1224            annotate_expr_natural_joins(expr, natural_markers, consumed);
1225            annotate_expr_natural_joins(low, natural_markers, consumed);
1226            annotate_expr_natural_joins(high, natural_markers, consumed);
1227        }
1228        ExprKind::Like {
1229            expr,
1230            pattern,
1231            escape,
1232            ..
1233        } => {
1234            annotate_expr_natural_joins(expr, natural_markers, consumed);
1235            annotate_expr_natural_joins(pattern, natural_markers, consumed);
1236            if let Some(escape) = escape {
1237                annotate_expr_natural_joins(escape, natural_markers, consumed);
1238            }
1239        }
1240        ExprKind::InList { expr, list, .. } => {
1241            annotate_expr_natural_joins(expr, natural_markers, consumed);
1242            for item in list {
1243                annotate_expr_natural_joins(item, natural_markers, consumed);
1244            }
1245        }
1246        ExprKind::Cast { expr, .. } | ExprKind::TryCast { expr, .. } => {
1247            annotate_expr_natural_joins(expr, natural_markers, consumed);
1248        }
1249        ExprKind::Literal { .. } | ExprKind::ColumnRef { .. } | ExprKind::VectorLiteral { .. } => {}
1250    }
1251}
1252
1253pub fn parse_expression_sql(sql: &str) -> Result<crate::ast::Expr> {
1254    let wrapped_len =
1255        preflight_input(sql, SELECT_WRAPPER_PREFIX.len()).map_err(parser_error_from_preflight)?;
1256    let mut wrapped = String::with_capacity(wrapped_len);
1257    wrapped.push_str(SELECT_WRAPPER_PREFIX);
1258    wrapped.push_str(sql);
1259    let statements = parse_sql_preflighted(&wrapped)?;
1260    let Some(statement) = statements.into_iter().next() else {
1261        return Err(empty_expression_error());
1262    };
1263    let StatementKind::Select(select) = statement.kind else {
1264        return Err(empty_expression_error());
1265    };
1266    let Some(crate::ast::SelectItem::Expr { expr, .. }) = select.projection.into_iter().next()
1267    else {
1268        return Err(empty_expression_error());
1269    };
1270    Ok(expr)
1271}
1272
1273fn empty_expression_error() -> ParserError {
1274    ParserError::UnexpectedToken {
1275        line: 0,
1276        column: 0,
1277        expected: "expression".to_string(),
1278        found: "empty parser result".to_string(),
1279    }
1280}
1281
1282fn checked_total_input_len(
1283    input_len: usize,
1284    wrapper_len: usize,
1285) -> std::result::Result<usize, InputPreflightError> {
1286    let total_len = input_len
1287        .checked_add(wrapper_len)
1288        .ok_or(InputPreflightError::LengthOverflow)?;
1289    if total_len > MAX_SQL_INPUT_BYTES {
1290        return Err(InputPreflightError::TooLarge);
1291    }
1292    Ok(total_len)
1293}
1294
1295fn preflight_input(
1296    sql: &str,
1297    wrapper_len: usize,
1298) -> std::result::Result<usize, InputPreflightError> {
1299    let total_len = checked_total_input_len(sql.len(), wrapper_len)?;
1300    if sql.as_bytes().contains(&0) {
1301        return Err(InputPreflightError::InteriorNul);
1302    }
1303    Ok(total_len)
1304}
1305
1306fn parser_error_from_preflight(error: InputPreflightError) -> ParserError {
1307    match error {
1308        InputPreflightError::TooLarge | InputPreflightError::LengthOverflow => {
1309            input_too_large_error()
1310        }
1311        InputPreflightError::InteriorNul => interior_nul_error(),
1312    }
1313}
1314
1315fn parser_error_from_ffi_input(error: nim_ffi::ParseInputError) -> ParserError {
1316    match error {
1317        nim_ffi::ParseInputError::LengthOutOfRange => input_too_large_error(),
1318        nim_ffi::ParseInputError::InteriorNul => interior_nul_error(),
1319    }
1320}
1321
1322fn input_too_large_error() -> ParserError {
1323    ParserError::UnexpectedToken {
1324        line: 0,
1325        column: 0,
1326        expected: "SQL input at most 1048576 UTF-8 bytes".to_string(),
1327        found: "SQL input exceeds byte limit".to_string(),
1328    }
1329}
1330
1331fn interior_nul_error() -> ParserError {
1332    ParserError::UnexpectedToken {
1333        line: 0,
1334        column: 0,
1335        expected: "valid SQL without interior NUL bytes".to_string(),
1336        found: "interior NUL byte".to_string(),
1337    }
1338}
1339
1340// nim-sql-parser/src/alopex_sql_parser.nim の `internalDefectPrefix` と
1341// 一致させる。Nim 側の `except Defect` 節が付与する接頭辞で、パーサー
1342// 内部の不変条件違反 (通常の構文エラーではない) を機械的に区別するための
1343// マーカー。ワイヤ契約 (MessagePack AST) には影響しない、エラー文言のみの
1344// 合意。
1345const INTERNAL_DEFECT_PREFIX: &str =
1346    "internal parser defect (this is a parser bug, not invalid SQL): ";
1347
1348fn parser_error_from_nim(message: &str) -> ParserError {
1349    if let Some(defect_message) = message.strip_prefix(INTERNAL_DEFECT_PREFIX) {
1350        return ParserError::InternalParserDefect {
1351            message: defect_message.to_string(),
1352        };
1353    }
1354    let (line, column) = parse_nim_line_col(message).unwrap_or((0, 0));
1355    ParserError::UnexpectedToken {
1356        line,
1357        column,
1358        expected: "valid SQL".to_string(),
1359        found: message.to_string(),
1360    }
1361}
1362
1363fn parse_nim_line_col(message: &str) -> Option<(u64, u64)> {
1364    let after_line = message.strip_prefix("Parse error at line ")?;
1365    let (line, rest) = after_line.split_once(", col ")?;
1366    let (col, _) = rest.split_once(':')?;
1367    Some((line.parse().ok()?, col.parse().ok()?))
1368}
1369
1370#[cfg(test)]
1371mod input_preflight_tests {
1372    use super::*;
1373
1374    #[test]
1375    fn relabeled_v040_parser_is_rejected_by_exported_contract_before_decode() {
1376        // The value checked here is the library export, not CONTRACT_VERSION.
1377        // Rewriting a sidecar therefore cannot change this runtime decision.
1378        let error = ensure_linked_parser_contract("0.4.0")
1379            .expect_err("sidecar labels cannot make a pre-frame producer compatible");
1380        let rendered = error.to_string();
1381
1382        assert!(rendered.contains("linked Nim parser contract 0.15.0"));
1383        assert!(rendered.contains("linked Nim parser contract 0.4.0"));
1384    }
1385
1386    #[test]
1387    fn relabeled_v050_parser_is_rejected_by_exported_contract_before_decode() {
1388        let error = ensure_linked_parser_contract("0.5.0")
1389            .expect_err("a 0.5.0 producer cannot satisfy the current named-window contract");
1390        let rendered = error.to_string();
1391
1392        assert!(rendered.contains("linked Nim parser contract 0.15.0"));
1393        assert!(rendered.contains("linked Nim parser contract 0.5.0"));
1394    }
1395
1396    #[test]
1397    fn legacy_v040_consumer_rejects_the_current_producer_before_decode() {
1398        let linked_producer_contract = nim_ffi::parser_contract_version();
1399        assert_eq!(linked_producer_contract, "0.15.0");
1400        let error = ensure_parser_contract("0.4.0", &linked_producer_contract)
1401            .expect_err("legacy consumer must reject a producer with frame semantics");
1402        let rendered = error.to_string();
1403
1404        assert!(rendered.contains("linked Nim parser contract 0.4.0"));
1405        assert!(rendered.contains("linked Nim parser contract 0.15.0"));
1406    }
1407
1408    #[test]
1409    fn legacy_v050_consumer_rejects_a_v060_named_window_producer_before_decode() {
1410        let linked_producer_contract = nim_ffi::parser_contract_version();
1411        assert_eq!(linked_producer_contract, "0.15.0");
1412        let error = ensure_parser_contract("0.5.0", &linked_producer_contract)
1413            .expect_err("legacy consumer must not ignore QUALIFY or named-window fields");
1414        let rendered = error.to_string();
1415
1416        assert!(rendered.contains("linked Nim parser contract 0.5.0"));
1417        assert!(rendered.contains("linked Nim parser contract 0.15.0"));
1418    }
1419
1420    #[test]
1421    fn raw_sql_guard_accepts_boundary_minus_and_exact_but_rejects_plus() {
1422        assert_eq!(
1423            checked_total_input_len(MAX_SQL_INPUT_BYTES - 1, 0),
1424            Ok(MAX_SQL_INPUT_BYTES - 1)
1425        );
1426        assert_eq!(
1427            checked_total_input_len(MAX_SQL_INPUT_BYTES, 0),
1428            Ok(MAX_SQL_INPUT_BYTES)
1429        );
1430        assert_eq!(
1431            checked_total_input_len(MAX_SQL_INPUT_BYTES + 1, 0),
1432            Err(InputPreflightError::TooLarge)
1433        );
1434    }
1435
1436    #[test]
1437    fn guard_counts_utf8_bytes_instead_of_characters() {
1438        let exact = "é".repeat(MAX_SQL_INPUT_BYTES / "é".len());
1439        assert_eq!(exact.chars().count(), MAX_SQL_INPUT_BYTES / 2);
1440        assert_eq!(exact.len(), MAX_SQL_INPUT_BYTES);
1441        assert_eq!(preflight_input(&exact, 0), Ok(MAX_SQL_INPUT_BYTES));
1442
1443        let plus = format!("{exact}é");
1444        assert_eq!(
1445            preflight_input(&plus, 0),
1446            Err(InputPreflightError::TooLarge)
1447        );
1448    }
1449
1450    #[test]
1451    fn expression_guard_includes_wrapper_and_detects_length_overflow() {
1452        assert_eq!(
1453            checked_total_input_len(
1454                MAX_SQL_INPUT_BYTES - SELECT_WRAPPER_PREFIX.len(),
1455                SELECT_WRAPPER_PREFIX.len(),
1456            ),
1457            Ok(MAX_SQL_INPUT_BYTES)
1458        );
1459        assert_eq!(
1460            checked_total_input_len(
1461                MAX_SQL_INPUT_BYTES - SELECT_WRAPPER_PREFIX.len() + 1,
1462                SELECT_WRAPPER_PREFIX.len(),
1463            ),
1464            Err(InputPreflightError::TooLarge)
1465        );
1466        assert_eq!(
1467            checked_total_input_len(usize::MAX, SELECT_WRAPPER_PREFIX.len()),
1468            Err(InputPreflightError::LengthOverflow)
1469        );
1470    }
1471
1472    #[test]
1473    fn preflight_rejects_nul_before_any_ffi_work() {
1474        assert_eq!(
1475            preflight_input("SELECT \0 1", 0),
1476            Err(InputPreflightError::InteriorNul)
1477        );
1478        assert_eq!(
1479            preflight_input("1 \0 2", SELECT_WRAPPER_PREFIX.len()),
1480            Err(InputPreflightError::InteriorNul)
1481        );
1482    }
1483}