Skip to main content

waypoint_core/
guard.rs

1//! Guard expression parser and evaluator for migration pre/post conditions.
2//!
3//! Guard expressions are declared in migration file headers using directives:
4//! ```sql
5//! -- waypoint:require table_exists("users")
6//! -- waypoint:require NOT column_exists("users", "email")
7//! -- waypoint:ensure column_exists("users", "email")
8//! ```
9//!
10//! Expressions support boolean operators (`AND`, `OR`, `NOT`), comparison
11//! operators (`<`, `>`, `<=`, `>=`), and built-in assertion functions that
12//! query the database schema.
13
14use crate::db::DbClient;
15use crate::dialect::DialectKind;
16use crate::error::{Result, WaypointError};
17
18/// Maximum nesting depth for guard expression parsing.
19const MAX_PARSE_DEPTH: usize = 50;
20
21// ---------------------------------------------------------------------------
22// Configuration
23// ---------------------------------------------------------------------------
24
25/// Behavior when a `-- waypoint:require` precondition fails.
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub enum OnRequireFail {
28    /// Abort the migration with an error (default).
29    #[default]
30    Error,
31    /// Log a warning but continue with the migration.
32    Warn,
33    /// Silently skip the migration.
34    Skip,
35}
36
37impl std::str::FromStr for OnRequireFail {
38    type Err = String;
39
40    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
41        match s.to_lowercase().as_str() {
42            "error" => Ok(Self::Error),
43            "warn" => Ok(Self::Warn),
44            "skip" => Ok(Self::Skip),
45            other => Err(format!("unknown on_require_fail value: '{other}'")),
46        }
47    }
48}
49
50/// Configuration for guard (pre/post condition) evaluation.
51#[derive(Debug, Clone)]
52pub struct GuardsConfig {
53    /// Whether guard conditions are evaluated before/after migrations.
54    pub enabled: bool,
55    /// What to do when a precondition (`-- waypoint:require`) fails.
56    pub on_require_fail: OnRequireFail,
57}
58
59impl Default for GuardsConfig {
60    fn default() -> Self {
61        Self {
62            enabled: true,
63            on_require_fail: OnRequireFail::default(),
64        }
65    }
66}
67
68// ---------------------------------------------------------------------------
69// AST
70// ---------------------------------------------------------------------------
71
72/// A comparison operator in a guard expression.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum ComparisonOp {
75    /// `<`
76    Lt,
77    /// `>`
78    Gt,
79    /// `<=`
80    Le,
81    /// `>=`
82    Ge,
83}
84
85impl std::fmt::Display for ComparisonOp {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            ComparisonOp::Lt => write!(f, "<"),
89            ComparisonOp::Gt => write!(f, ">"),
90            ComparisonOp::Le => write!(f, "<="),
91            ComparisonOp::Ge => write!(f, ">="),
92        }
93    }
94}
95
96/// A node in the guard expression abstract syntax tree.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum GuardExpr {
99    /// A call to a built-in assertion function, e.g. `table_exists("users")`.
100    FunctionCall {
101        /// Function name (e.g. `table_exists`, `column_exists`).
102        name: String,
103        /// Argument expressions.
104        args: Vec<GuardExpr>,
105    },
106    /// Logical AND of two expressions.
107    And(Box<GuardExpr>, Box<GuardExpr>),
108    /// Logical OR of two expressions.
109    Or(Box<GuardExpr>, Box<GuardExpr>),
110    /// Logical NOT of an expression.
111    Not(Box<GuardExpr>),
112    /// A comparison between two expressions.
113    Comparison {
114        /// Left-hand operand.
115        left: Box<GuardExpr>,
116        /// Comparison operator.
117        op: ComparisonOp,
118        /// Right-hand operand.
119        right: Box<GuardExpr>,
120    },
121    /// A string literal (double-quoted).
122    StringLiteral(String),
123    /// A numeric literal.
124    NumberLiteral(i64),
125    /// A boolean literal (`true` / `false`).
126    BoolLiteral(bool),
127}
128
129/// The runtime value produced by evaluating a guard expression.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum GuardValue {
132    /// A boolean value.
133    Bool(bool),
134    /// A numeric (integer) value.
135    Number(i64),
136    /// A string value.
137    Str(String),
138}
139
140impl std::fmt::Display for GuardValue {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            GuardValue::Bool(b) => write!(f, "{b}"),
144            GuardValue::Number(n) => write!(f, "{n}"),
145            GuardValue::Str(s) => write!(f, "\"{s}\""),
146        }
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Tokenizer
152// ---------------------------------------------------------------------------
153
154/// A token produced by the lexer.
155#[derive(Debug, Clone, PartialEq, Eq)]
156enum Token {
157    Ident(String),
158    StringLit(String),
159    NumberLit(i64),
160    And,
161    Or,
162    Not,
163    Lt,
164    Gt,
165    Le,
166    Ge,
167    LParen,
168    RParen,
169    Comma,
170}
171
172impl std::fmt::Display for Token {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            Token::Ident(s) => write!(f, "{s}"),
176            Token::StringLit(s) => write!(f, "\"{s}\""),
177            Token::NumberLit(n) => write!(f, "{n}"),
178            Token::And => write!(f, "AND"),
179            Token::Or => write!(f, "OR"),
180            Token::Not => write!(f, "NOT"),
181            Token::Lt => write!(f, "<"),
182            Token::Gt => write!(f, ">"),
183            Token::Le => write!(f, "<="),
184            Token::Ge => write!(f, ">="),
185            Token::LParen => write!(f, "("),
186            Token::RParen => write!(f, ")"),
187            Token::Comma => write!(f, ","),
188        }
189    }
190}
191
192/// Tokenize a guard expression string into a sequence of tokens.
193fn tokenize(input: &str) -> Result<Vec<Token>> {
194    let mut tokens = Vec::new();
195    let chars: Vec<char> = input.chars().collect();
196    let len = chars.len();
197    let mut i = 0;
198
199    while i < len {
200        let ch = chars[i];
201
202        // Skip whitespace
203        if ch.is_ascii_whitespace() {
204            i += 1;
205            continue;
206        }
207
208        // String literal (double-quoted)
209        if ch == '"' {
210            i += 1;
211            let start = i;
212            while i < len && chars[i] != '"' {
213                if chars[i] == '\\' && i + 1 < len {
214                    i += 2; // skip escaped character
215                } else {
216                    i += 1;
217                }
218            }
219            if i >= len {
220                return Err(WaypointError::ConfigError(
221                    "Guard expression: unterminated string literal".to_string(),
222                ));
223            }
224            let s: String = chars[start..i].iter().collect();
225            tokens.push(Token::StringLit(s));
226            i += 1; // skip closing quote
227            continue;
228        }
229
230        // Parentheses and comma
231        if ch == '(' {
232            tokens.push(Token::LParen);
233            i += 1;
234            continue;
235        }
236        if ch == ')' {
237            tokens.push(Token::RParen);
238            i += 1;
239            continue;
240        }
241        if ch == ',' {
242            tokens.push(Token::Comma);
243            i += 1;
244            continue;
245        }
246
247        // Comparison operators
248        if ch == '<' {
249            if i + 1 < len && chars[i + 1] == '=' {
250                tokens.push(Token::Le);
251                i += 2;
252            } else {
253                tokens.push(Token::Lt);
254                i += 1;
255            }
256            continue;
257        }
258        if ch == '>' {
259            if i + 1 < len && chars[i + 1] == '=' {
260                tokens.push(Token::Ge);
261                i += 2;
262            } else {
263                tokens.push(Token::Gt);
264                i += 1;
265            }
266            continue;
267        }
268
269        // Numbers
270        if ch.is_ascii_digit() {
271            let start = i;
272            while i < len && chars[i].is_ascii_digit() {
273                i += 1;
274            }
275            let num_str: String = chars[start..i].iter().collect();
276            let n = num_str.parse::<i64>().map_err(|e| {
277                WaypointError::ConfigError(format!(
278                    "Guard expression: invalid number '{num_str}': {e}"
279                ))
280            })?;
281            tokens.push(Token::NumberLit(n));
282            continue;
283        }
284
285        // Identifiers and keywords (AND, OR, NOT, true, false)
286        if ch.is_ascii_alphabetic() || ch == '_' {
287            let start = i;
288            while i < len && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
289                i += 1;
290            }
291            let word: String = chars[start..i].iter().collect();
292            if word.eq_ignore_ascii_case("AND") {
293                tokens.push(Token::And);
294            } else if word.eq_ignore_ascii_case("OR") {
295                tokens.push(Token::Or);
296            } else if word.eq_ignore_ascii_case("NOT") {
297                tokens.push(Token::Not);
298            } else if word.eq_ignore_ascii_case("TRUE") {
299                tokens.push(Token::Ident("true".to_string()));
300            } else if word.eq_ignore_ascii_case("FALSE") {
301                tokens.push(Token::Ident("false".to_string()));
302            } else {
303                tokens.push(Token::Ident(word));
304            }
305            continue;
306        }
307
308        return Err(WaypointError::ConfigError(format!(
309            "Guard expression: unexpected character '{ch}'"
310        )));
311    }
312
313    Ok(tokens)
314}
315
316// ---------------------------------------------------------------------------
317// Parser
318// ---------------------------------------------------------------------------
319
320/// Recursive descent parser state.
321struct Parser {
322    tokens: Vec<Token>,
323    pos: usize,
324}
325
326impl Parser {
327    fn new(tokens: Vec<Token>) -> Self {
328        Self { tokens, pos: 0 }
329    }
330
331    fn peek(&self) -> Option<&Token> {
332        self.tokens.get(self.pos)
333    }
334
335    fn advance(&mut self) -> Option<Token> {
336        if self.pos < self.tokens.len() {
337            let tok = self.tokens[self.pos].clone();
338            self.pos += 1;
339            Some(tok)
340        } else {
341            None
342        }
343    }
344
345    fn expect(&mut self, expected: &Token) -> Result<()> {
346        match self.advance() {
347            Some(ref tok) if tok == expected => Ok(()),
348            Some(tok) => Err(WaypointError::ConfigError(format!(
349                "Guard expression: expected '{expected}', found '{tok}'"
350            ))),
351            None => Err(WaypointError::ConfigError(format!(
352                "Guard expression: expected '{expected}', found end of input"
353            ))),
354        }
355    }
356
357    /// Parse a complete expression.
358    ///
359    /// Grammar: `expr → or_expr`
360    fn parse_expr(&mut self, depth: usize) -> Result<GuardExpr> {
361        self.parse_or_expr(depth)
362    }
363
364    /// `or_expr → and_expr (OR and_expr)*`
365    fn parse_or_expr(&mut self, depth: usize) -> Result<GuardExpr> {
366        if depth > MAX_PARSE_DEPTH {
367            return Err(WaypointError::ConfigError(
368                "Guard expression: maximum nesting depth exceeded".to_string(),
369            ));
370        }
371        let mut left = self.parse_and_expr(depth + 1)?;
372        while self.peek() == Some(&Token::Or) {
373            self.advance(); // consume OR
374            let right = self.parse_and_expr(depth + 1)?;
375            left = GuardExpr::Or(Box::new(left), Box::new(right));
376        }
377        Ok(left)
378    }
379
380    /// `and_expr → not_expr (AND not_expr)*`
381    fn parse_and_expr(&mut self, depth: usize) -> Result<GuardExpr> {
382        if depth > MAX_PARSE_DEPTH {
383            return Err(WaypointError::ConfigError(
384                "Guard expression: maximum nesting depth exceeded".to_string(),
385            ));
386        }
387        let mut left = self.parse_not_expr(depth + 1)?;
388        while self.peek() == Some(&Token::And) {
389            self.advance(); // consume AND
390            let right = self.parse_not_expr(depth + 1)?;
391            left = GuardExpr::And(Box::new(left), Box::new(right));
392        }
393        Ok(left)
394    }
395
396    /// `not_expr → NOT not_expr | comparison`
397    fn parse_not_expr(&mut self, depth: usize) -> Result<GuardExpr> {
398        if depth > MAX_PARSE_DEPTH {
399            return Err(WaypointError::ConfigError(
400                "Guard expression: maximum nesting depth exceeded".to_string(),
401            ));
402        }
403        if self.peek() == Some(&Token::Not) {
404            self.advance(); // consume NOT
405            let inner = self.parse_not_expr(depth + 1)?;
406            Ok(GuardExpr::Not(Box::new(inner)))
407        } else {
408            self.parse_comparison(depth + 1)
409        }
410    }
411
412    /// `comparison → primary ((< | > | <= | >=) primary)?`
413    fn parse_comparison(&mut self, depth: usize) -> Result<GuardExpr> {
414        if depth > MAX_PARSE_DEPTH {
415            return Err(WaypointError::ConfigError(
416                "Guard expression: maximum nesting depth exceeded".to_string(),
417            ));
418        }
419        let left = self.parse_primary(depth + 1)?;
420
421        let op = match self.peek() {
422            Some(Token::Lt) => Some(ComparisonOp::Lt),
423            Some(Token::Gt) => Some(ComparisonOp::Gt),
424            Some(Token::Le) => Some(ComparisonOp::Le),
425            Some(Token::Ge) => Some(ComparisonOp::Ge),
426            _ => None,
427        };
428
429        if let Some(op) = op {
430            self.advance(); // consume operator
431            let right = self.parse_primary(depth + 1)?;
432            Ok(GuardExpr::Comparison {
433                left: Box::new(left),
434                op,
435                right: Box::new(right),
436            })
437        } else {
438            Ok(left)
439        }
440    }
441
442    /// `primary → function_call | '(' expr ')' | literal`
443    fn parse_primary(&mut self, depth: usize) -> Result<GuardExpr> {
444        if depth > MAX_PARSE_DEPTH {
445            return Err(WaypointError::ConfigError(
446                "Guard expression: maximum nesting depth exceeded".to_string(),
447            ));
448        }
449        match self.peek().cloned() {
450            Some(Token::Ident(name)) => {
451                // Check if it's a boolean literal
452                if name == "true" {
453                    self.advance();
454                    return Ok(GuardExpr::BoolLiteral(true));
455                }
456                if name == "false" {
457                    self.advance();
458                    return Ok(GuardExpr::BoolLiteral(false));
459                }
460
461                // Check if it's a function call (ident followed by '(')
462                if self.pos + 1 < self.tokens.len() && self.tokens[self.pos + 1] == Token::LParen {
463                    self.advance(); // consume ident
464                    self.advance(); // consume '('
465                    let args = self.parse_args(depth + 1)?;
466                    self.expect(&Token::RParen)?;
467                    Ok(GuardExpr::FunctionCall { name, args })
468                } else {
469                    Err(WaypointError::ConfigError(format!(
470                        "Guard expression: unexpected identifier '{name}' (expected function call)"
471                    )))
472                }
473            }
474            Some(Token::LParen) => {
475                self.advance(); // consume '('
476                let expr = self.parse_expr(depth + 1)?;
477                self.expect(&Token::RParen)?;
478                Ok(expr)
479            }
480            Some(Token::StringLit(s)) => {
481                self.advance();
482                Ok(GuardExpr::StringLiteral(s))
483            }
484            Some(Token::NumberLit(n)) => {
485                self.advance();
486                Ok(GuardExpr::NumberLiteral(n))
487            }
488            Some(tok) => Err(WaypointError::ConfigError(format!(
489                "Guard expression: unexpected token '{tok}'"
490            ))),
491            None => Err(WaypointError::ConfigError(
492                "Guard expression: unexpected end of input".to_string(),
493            )),
494        }
495    }
496
497    /// `args → expr (',' expr)* | ε`
498    fn parse_args(&mut self, depth: usize) -> Result<Vec<GuardExpr>> {
499        let mut args = Vec::new();
500
501        // Empty argument list
502        if self.peek() == Some(&Token::RParen) {
503            return Ok(args);
504        }
505
506        args.push(self.parse_expr(depth)?);
507
508        while self.peek() == Some(&Token::Comma) {
509            self.advance(); // consume ','
510            args.push(self.parse_expr(depth)?);
511        }
512
513        Ok(args)
514    }
515}
516
517/// Parse a guard expression string into an AST.
518///
519/// # Errors
520///
521/// Returns `WaypointError::ConfigError` if the expression has invalid syntax.
522///
523/// # Examples
524///
525/// ```
526/// use waypoint_core::guard::parse;
527///
528/// let expr = parse("table_exists(\"users\")").unwrap();
529/// let expr = parse("table_exists(\"users\") AND column_exists(\"users\", \"email\")").unwrap();
530/// let expr = parse("NOT table_exists(\"legacy\")").unwrap();
531/// let expr = parse("row_count(\"users\") < 1000").unwrap();
532/// ```
533pub fn parse(input: &str) -> Result<GuardExpr> {
534    let tokens = tokenize(input)?;
535    if tokens.is_empty() {
536        return Err(WaypointError::ConfigError(
537            "Guard expression: empty expression".to_string(),
538        ));
539    }
540    let mut parser = Parser::new(tokens);
541    let expr = parser.parse_expr(0)?;
542
543    // Ensure all tokens were consumed
544    if parser.pos < parser.tokens.len() {
545        let remaining = &parser.tokens[parser.pos];
546        return Err(WaypointError::ConfigError(format!(
547            "Guard expression: unexpected token '{remaining}' after complete expression"
548        )));
549    }
550
551    Ok(expr)
552}
553
554// ---------------------------------------------------------------------------
555// Built-in function SQL generation
556// ---------------------------------------------------------------------------
557
558/// Generate the SQL query for a built-in guard function (PostgreSQL).
559///
560/// Returns `(sql, params, is_boolean)` — `params` contains the parameter values
561/// in order ($1, $2, $3...), and `is_boolean` is `true` when the query returns
562/// a single boolean, `false` when it returns a count (Number).
563#[cfg(feature = "postgres")]
564fn builtin_sql(name: &str, args: &[String], schema: &str) -> Result<(String, Vec<String>, bool)> {
565    match name {
566        "table_exists" => {
567            require_args(name, args, 1)?;
568            let table = &args[0];
569            Ok((
570                "SELECT EXISTS(SELECT 1 FROM information_schema.tables \
571                 WHERE table_schema = $1 AND table_name = $2)"
572                    .to_string(),
573                vec![schema.to_string(), table.to_string()],
574                true,
575            ))
576        }
577        "column_exists" => {
578            require_args(name, args, 2)?;
579            let table = &args[0];
580            let column = &args[1];
581            Ok((
582                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
583                 WHERE table_schema = $1 AND table_name = $2 \
584                 AND column_name = $3)"
585                    .to_string(),
586                vec![schema.to_string(), table.to_string(), column.to_string()],
587                true,
588            ))
589        }
590        "column_type" => {
591            require_args(name, args, 3)?;
592            let table = &args[0];
593            let column = &args[1];
594            let expected_type = &args[2];
595            Ok((
596                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
597                 WHERE table_schema = $1 AND table_name = $2 \
598                 AND column_name = $3 AND data_type = $4)"
599                    .to_string(),
600                vec![
601                    schema.to_string(),
602                    table.to_string(),
603                    column.to_string(),
604                    expected_type.to_string(),
605                ],
606                true,
607            ))
608        }
609        "column_nullable" => {
610            require_args(name, args, 2)?;
611            let table = &args[0];
612            let column = &args[1];
613            Ok((
614                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
615                 WHERE table_schema = $1 AND table_name = $2 \
616                 AND column_name = $3 AND is_nullable = 'YES')"
617                    .to_string(),
618                vec![schema.to_string(), table.to_string(), column.to_string()],
619                true,
620            ))
621        }
622        "index_exists" => {
623            require_args(name, args, 1)?;
624            let index = &args[0];
625            Ok((
626                "SELECT EXISTS(SELECT 1 FROM pg_indexes \
627                 WHERE schemaname = $1 AND indexname = $2)"
628                    .to_string(),
629                vec![schema.to_string(), index.to_string()],
630                true,
631            ))
632        }
633        "constraint_exists" => {
634            require_args(name, args, 2)?;
635            let table = &args[0];
636            let constraint = &args[1];
637            Ok((
638                "SELECT EXISTS(SELECT 1 FROM information_schema.table_constraints \
639                 WHERE table_schema = $1 AND table_name = $2 \
640                 AND constraint_name = $3)"
641                    .to_string(),
642                vec![
643                    schema.to_string(),
644                    table.to_string(),
645                    constraint.to_string(),
646                ],
647                true,
648            ))
649        }
650        "function_exists" => {
651            require_args(name, args, 1)?;
652            let func = &args[0];
653            Ok((
654                "SELECT EXISTS(SELECT 1 FROM pg_proc p \
655                 JOIN pg_namespace n ON n.oid = p.pronamespace \
656                 WHERE n.nspname = $1 AND p.proname = $2)"
657                    .to_string(),
658                vec![schema.to_string(), func.to_string()],
659                true,
660            ))
661        }
662        "enum_exists" => {
663            require_args(name, args, 1)?;
664            let enum_name = &args[0];
665            Ok((
666                "SELECT EXISTS(SELECT 1 FROM pg_type t \
667                 JOIN pg_namespace n ON n.oid = t.typnamespace \
668                 WHERE n.nspname = $1 AND t.typname = $2 \
669                 AND t.typtype = 'e')"
670                    .to_string(),
671                vec![schema.to_string(), enum_name.to_string()],
672                true,
673            ))
674        }
675        "row_count" => {
676            require_args(name, args, 1)?;
677            let table = &args[0];
678            Ok((
679                "SELECT COALESCE(n_live_tup, 0)::bigint FROM pg_stat_user_tables \
680                 WHERE schemaname = $1 AND relname = $2"
681                    .to_string(),
682                vec![schema.to_string(), table.to_string()],
683                false,
684            ))
685        }
686        "sql" => {
687            require_args(name, args, 1)?;
688            let query = &args[0];
689            Ok((query.to_string(), vec![], true))
690        }
691        _ => Err(WaypointError::ConfigError(format!(
692            "Guard expression: unknown function '{name}'"
693        ))),
694    }
695}
696
697/// Generate the SQL query for a built-in guard function (MySQL 8.0+).
698///
699/// Mirrors [`builtin_sql`] but emits `?` placeholders and uses MySQL system
700/// tables (`information_schema.*`). The `enum_exists` builtin is rejected
701/// because MySQL has no enum *type* — ENUM is a column type modifier and
702/// can't exist independently in the schema.
703#[cfg(feature = "mysql")]
704fn builtin_sql_mysql(
705    name: &str,
706    args: &[String],
707    schema: &str,
708) -> Result<(String, Vec<String>, bool)> {
709    match name {
710        "table_exists" => {
711            require_args(name, args, 1)?;
712            Ok((
713                "SELECT EXISTS(SELECT 1 FROM information_schema.tables \
714                 WHERE table_schema = ? AND table_name = ?)"
715                    .to_string(),
716                vec![schema.to_string(), args[0].clone()],
717                true,
718            ))
719        }
720        "column_exists" => {
721            require_args(name, args, 2)?;
722            Ok((
723                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
724                 WHERE table_schema = ? AND table_name = ? AND column_name = ?)"
725                    .to_string(),
726                vec![schema.to_string(), args[0].clone(), args[1].clone()],
727                true,
728            ))
729        }
730        "column_type" => {
731            require_args(name, args, 3)?;
732            // MySQL stores the base type in DATA_TYPE (e.g. "varchar", "int")
733            // and the full declaration in COLUMN_TYPE (e.g. "varchar(255)").
734            // We match DATA_TYPE for consistency with the PG behaviour where
735            // `column_type("t","c","character varying")` matches the type name.
736            Ok((
737                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
738                 WHERE table_schema = ? AND table_name = ? \
739                 AND column_name = ? AND data_type = ?)"
740                    .to_string(),
741                vec![
742                    schema.to_string(),
743                    args[0].clone(),
744                    args[1].clone(),
745                    args[2].clone(),
746                ],
747                true,
748            ))
749        }
750        "column_nullable" => {
751            require_args(name, args, 2)?;
752            Ok((
753                "SELECT EXISTS(SELECT 1 FROM information_schema.columns \
754                 WHERE table_schema = ? AND table_name = ? \
755                 AND column_name = ? AND is_nullable = 'YES')"
756                    .to_string(),
757                vec![schema.to_string(), args[0].clone(), args[1].clone()],
758                true,
759            ))
760        }
761        "index_exists" => {
762            require_args(name, args, 1)?;
763            Ok((
764                "SELECT EXISTS(SELECT 1 FROM information_schema.statistics \
765                 WHERE table_schema = ? AND index_name = ?)"
766                    .to_string(),
767                vec![schema.to_string(), args[0].clone()],
768                true,
769            ))
770        }
771        "constraint_exists" => {
772            require_args(name, args, 2)?;
773            Ok((
774                "SELECT EXISTS(SELECT 1 FROM information_schema.table_constraints \
775                 WHERE table_schema = ? AND table_name = ? AND constraint_name = ?)"
776                    .to_string(),
777                vec![schema.to_string(), args[0].clone(), args[1].clone()],
778                true,
779            ))
780        }
781        "function_exists" => {
782            require_args(name, args, 1)?;
783            Ok((
784                "SELECT EXISTS(SELECT 1 FROM information_schema.routines \
785                 WHERE routine_schema = ? AND routine_name = ? \
786                 AND routine_type = 'FUNCTION')"
787                    .to_string(),
788                vec![schema.to_string(), args[0].clone()],
789                true,
790            ))
791        }
792        "enum_exists" => Err(WaypointError::ConfigError(
793            "Guard expression: enum_exists() is not supported on MySQL — \
794             MySQL has no enum *type* (ENUM is a column type modifier, not a \
795             schema object). Use column_type(..., \"enum\") instead."
796                .into(),
797        )),
798        "row_count" => {
799            require_args(name, args, 1)?;
800            // information_schema.tables.table_rows is an approximate count
801            // (storage-engine dependent). InnoDB returns NULL for empty/new
802            // tables in some cases — COALESCE so callers get 0 rather than
803            // a NULL surfacing as a type-conversion error.
804            Ok((
805                "SELECT COALESCE(table_rows, 0) FROM information_schema.tables \
806                 WHERE table_schema = ? AND table_name = ?"
807                    .to_string(),
808                vec![schema.to_string(), args[0].clone()],
809                false,
810            ))
811        }
812        "sql" => {
813            require_args(name, args, 1)?;
814            Ok((args[0].clone(), vec![], true))
815        }
816        _ => Err(WaypointError::ConfigError(format!(
817            "Guard expression: unknown function '{name}'"
818        ))),
819    }
820}
821
822/// Validate that a function received the expected number of string arguments.
823fn require_args(name: &str, args: &[String], expected: usize) -> Result<()> {
824    if args.len() != expected {
825        return Err(WaypointError::ConfigError(format!(
826            "Guard expression: {name}() expects {expected} argument(s), got {}",
827            args.len()
828        )));
829    }
830    Ok(())
831}
832
833// ---------------------------------------------------------------------------
834// Evaluator
835// ---------------------------------------------------------------------------
836
837/// Extract string values from evaluated argument expressions.
838///
839/// This resolves each argument expression; only `StringLiteral` nodes are
840/// accepted as function arguments for built-in functions.
841fn extract_string_args(args: &[GuardExpr]) -> Result<Vec<String>> {
842    let mut result = Vec::with_capacity(args.len());
843    for arg in args {
844        match arg {
845            GuardExpr::StringLiteral(s) => result.push(s.clone()),
846            other => {
847                return Err(WaypointError::ConfigError(format!(
848                    "Guard expression: expected string argument, found {other:?}"
849                )));
850            }
851        }
852    }
853    Ok(result)
854}
855
856/// Evaluate a guard expression tree against a live database.
857///
858/// Built-in functions are translated to SQL queries and executed against the
859/// given `schema`. Boolean operators are short-circuit evaluated.
860///
861/// # Errors
862///
863/// Returns `WaypointError::GuardFailed` when a function execution fails, or
864/// `WaypointError::ConfigError` for type mismatches and unknown functions.
865#[cfg(feature = "postgres")]
866pub async fn evaluate(
867    client: &tokio_postgres::Client,
868    schema: &str,
869    expr: &GuardExpr,
870) -> Result<bool> {
871    let value = eval_expr(client, schema, expr).await?;
872    match value {
873        GuardValue::Bool(b) => Ok(b),
874        other => Err(WaypointError::ConfigError(format!(
875            "Guard expression: expected boolean result, got {other}"
876        ))),
877    }
878}
879
880/// Recursively evaluate an expression node, returning its value (PostgreSQL).
881#[cfg(feature = "postgres")]
882fn eval_expr<'a>(
883    client: &'a tokio_postgres::Client,
884    schema: &'a str,
885    expr: &'a GuardExpr,
886) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<GuardValue>> + Send + 'a>> {
887    Box::pin(async move {
888        match expr {
889            GuardExpr::BoolLiteral(b) => Ok(GuardValue::Bool(*b)),
890            GuardExpr::NumberLiteral(n) => Ok(GuardValue::Number(*n)),
891            GuardExpr::StringLiteral(s) => Ok(GuardValue::Str(s.clone())),
892
893            GuardExpr::Not(inner) => {
894                let val = eval_expr(client, schema, inner).await?;
895                match val {
896                    GuardValue::Bool(b) => Ok(GuardValue::Bool(!b)),
897                    other => Err(WaypointError::ConfigError(format!(
898                        "Guard expression: NOT requires boolean, got {other}"
899                    ))),
900                }
901            }
902
903            GuardExpr::And(left, right) => {
904                let lval = eval_expr(client, schema, left).await?;
905                match lval {
906                    GuardValue::Bool(false) => Ok(GuardValue::Bool(false)),
907                    GuardValue::Bool(true) => {
908                        let rval = eval_expr(client, schema, right).await?;
909                        match rval {
910                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
911                            other => Err(WaypointError::ConfigError(format!(
912                                "Guard expression: AND requires boolean operands, got {other}"
913                            ))),
914                        }
915                    }
916                    other => Err(WaypointError::ConfigError(format!(
917                        "Guard expression: AND requires boolean operands, got {other}"
918                    ))),
919                }
920            }
921
922            GuardExpr::Or(left, right) => {
923                let lval = eval_expr(client, schema, left).await?;
924                match lval {
925                    GuardValue::Bool(true) => Ok(GuardValue::Bool(true)),
926                    GuardValue::Bool(false) => {
927                        let rval = eval_expr(client, schema, right).await?;
928                        match rval {
929                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
930                            other => Err(WaypointError::ConfigError(format!(
931                                "Guard expression: OR requires boolean operands, got {other}"
932                            ))),
933                        }
934                    }
935                    other => Err(WaypointError::ConfigError(format!(
936                        "Guard expression: OR requires boolean operands, got {other}"
937                    ))),
938                }
939            }
940
941            GuardExpr::Comparison { left, op, right } => {
942                let lval = eval_expr(client, schema, left).await?;
943                let rval = eval_expr(client, schema, right).await?;
944                match (&lval, &rval) {
945                    (GuardValue::Number(a), GuardValue::Number(b)) => {
946                        let result = match op {
947                            ComparisonOp::Lt => a < b,
948                            ComparisonOp::Gt => a > b,
949                            ComparisonOp::Le => a <= b,
950                            ComparisonOp::Ge => a >= b,
951                        };
952                        Ok(GuardValue::Bool(result))
953                    }
954                    _ => Err(WaypointError::ConfigError(format!(
955                        "Guard expression: comparison requires numeric operands, got {lval} {op} {rval}"
956                    ))),
957                }
958            }
959
960            GuardExpr::FunctionCall { name, args } => {
961                let string_args = extract_string_args(args)?;
962                let (sql, param_values, is_boolean) = builtin_sql(name, &string_args, schema)?;
963                let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = param_values
964                    .iter()
965                    .map(|s| s as &(dyn tokio_postgres::types::ToSql + Sync))
966                    .collect();
967
968                let row = client.query_one(&sql, &params).await.map_err(|e| {
969                    WaypointError::GuardFailed {
970                        kind: "evaluation".to_string(),
971                        script: String::new(),
972                        expression: format!(
973                            "{name}({}) failed: {e}",
974                            string_args
975                                .iter()
976                                .map(|a| format!("\"{a}\""))
977                                .collect::<Vec<_>>()
978                                .join(", ")
979                        ),
980                    }
981                })?;
982
983                if is_boolean {
984                    let val: bool = row.get(0);
985                    Ok(GuardValue::Bool(val))
986                } else {
987                    let val: i64 = row.get(0);
988                    Ok(GuardValue::Number(val))
989                }
990            }
991        }
992    })
993}
994
995// ---------------------------------------------------------------------------
996// Dialect-aware evaluator
997// ---------------------------------------------------------------------------
998
999/// Evaluate a guard expression against a [`DbClient`] (dialect-aware entry).
1000///
1001/// Dispatches to the PostgreSQL or MySQL implementation based on the connection
1002/// kind. Recursion shape mirrors the legacy [`evaluate`] function; only the
1003/// leaf `FunctionCall` arm differs per engine.
1004pub async fn evaluate_db(client: &DbClient, schema: &str, expr: &GuardExpr) -> Result<bool> {
1005    let value = eval_expr_db(client, schema, expr).await?;
1006    match value {
1007        GuardValue::Bool(b) => Ok(b),
1008        other => Err(WaypointError::ConfigError(format!(
1009            "Guard expression: expected boolean result, got {other}"
1010        ))),
1011    }
1012}
1013
1014fn eval_expr_db<'a>(
1015    client: &'a DbClient,
1016    schema: &'a str,
1017    expr: &'a GuardExpr,
1018) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<GuardValue>> + Send + 'a>> {
1019    Box::pin(async move {
1020        match expr {
1021            GuardExpr::BoolLiteral(b) => Ok(GuardValue::Bool(*b)),
1022            GuardExpr::NumberLiteral(n) => Ok(GuardValue::Number(*n)),
1023            GuardExpr::StringLiteral(s) => Ok(GuardValue::Str(s.clone())),
1024
1025            GuardExpr::Not(inner) => {
1026                let val = eval_expr_db(client, schema, inner).await?;
1027                match val {
1028                    GuardValue::Bool(b) => Ok(GuardValue::Bool(!b)),
1029                    other => Err(WaypointError::ConfigError(format!(
1030                        "Guard expression: NOT requires boolean, got {other}"
1031                    ))),
1032                }
1033            }
1034
1035            GuardExpr::And(left, right) => {
1036                let lval = eval_expr_db(client, schema, left).await?;
1037                match lval {
1038                    GuardValue::Bool(false) => Ok(GuardValue::Bool(false)),
1039                    GuardValue::Bool(true) => {
1040                        let rval = eval_expr_db(client, schema, right).await?;
1041                        match rval {
1042                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
1043                            other => Err(WaypointError::ConfigError(format!(
1044                                "Guard expression: AND requires boolean operands, got {other}"
1045                            ))),
1046                        }
1047                    }
1048                    other => Err(WaypointError::ConfigError(format!(
1049                        "Guard expression: AND requires boolean operands, got {other}"
1050                    ))),
1051                }
1052            }
1053
1054            GuardExpr::Or(left, right) => {
1055                let lval = eval_expr_db(client, schema, left).await?;
1056                match lval {
1057                    GuardValue::Bool(true) => Ok(GuardValue::Bool(true)),
1058                    GuardValue::Bool(false) => {
1059                        let rval = eval_expr_db(client, schema, right).await?;
1060                        match rval {
1061                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
1062                            other => Err(WaypointError::ConfigError(format!(
1063                                "Guard expression: OR requires boolean operands, got {other}"
1064                            ))),
1065                        }
1066                    }
1067                    other => Err(WaypointError::ConfigError(format!(
1068                        "Guard expression: OR requires boolean operands, got {other}"
1069                    ))),
1070                }
1071            }
1072
1073            GuardExpr::Comparison { left, op, right } => {
1074                let lval = eval_expr_db(client, schema, left).await?;
1075                let rval = eval_expr_db(client, schema, right).await?;
1076                match (&lval, &rval) {
1077                    (GuardValue::Number(a), GuardValue::Number(b)) => {
1078                        let result = match op {
1079                            ComparisonOp::Lt => a < b,
1080                            ComparisonOp::Gt => a > b,
1081                            ComparisonOp::Le => a <= b,
1082                            ComparisonOp::Ge => a >= b,
1083                        };
1084                        Ok(GuardValue::Bool(result))
1085                    }
1086                    _ => Err(WaypointError::ConfigError(format!(
1087                        "Guard expression: comparison requires numeric operands, got {lval} {op} {rval}"
1088                    ))),
1089                }
1090            }
1091
1092            GuardExpr::FunctionCall { name, args } => {
1093                let string_args = extract_string_args(args)?;
1094                exec_builtin(client, schema, name, &string_args).await
1095            }
1096        }
1097    })
1098}
1099
1100/// Execute a built-in guard function against the configured backend.
1101async fn exec_builtin(
1102    client: &DbClient,
1103    schema: &str,
1104    name: &str,
1105    string_args: &[String],
1106) -> Result<GuardValue> {
1107    match client.dialect_kind() {
1108        #[cfg(feature = "postgres")]
1109        DialectKind::Postgres => {
1110            let (sql, param_values, is_boolean) = builtin_sql(name, string_args, schema)?;
1111            let pg = client.as_postgres()?;
1112            let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = param_values
1113                .iter()
1114                .map(|s| s as &(dyn tokio_postgres::types::ToSql + Sync))
1115                .collect();
1116            let row = pg
1117                .query_one(&sql, &params)
1118                .await
1119                .map_err(|e| guard_failed(name, string_args, &e.to_string()))?;
1120            if is_boolean {
1121                Ok(GuardValue::Bool(row.get(0)))
1122            } else {
1123                Ok(GuardValue::Number(row.get(0)))
1124            }
1125        }
1126        #[cfg(not(feature = "postgres"))]
1127        DialectKind::Postgres => Err(WaypointError::ConfigError(
1128            "PostgreSQL support is not compiled in".into(),
1129        )),
1130        #[cfg(feature = "mysql")]
1131        DialectKind::Mysql => {
1132            use mysql_async::prelude::*;
1133            let (sql, param_values, is_boolean) = builtin_sql_mysql(name, string_args, schema)?;
1134            let pool = client.as_mysql()?;
1135            let mut conn = pool
1136                .get_conn()
1137                .await
1138                .map_err(|e| guard_failed(name, string_args, &e.to_string()))?;
1139
1140            // information_schema EXISTS(...) and COUNT(*) both return a single
1141            // i64 column on MySQL — read as Option<i64> to share the param-
1142            // binding path between the boolean and numeric builtins (avoids the
1143            // chrono-feature ambiguity around bool decoding).
1144            let result: Option<i64> = if param_values.is_empty() {
1145                conn.query_first(&sql).await
1146            } else {
1147                let params: Vec<mysql_async::Value> = param_values
1148                    .iter()
1149                    .map(|s| mysql_async::Value::Bytes(s.as_bytes().to_vec()))
1150                    .collect();
1151                conn.exec_first(&sql, params).await
1152            }
1153            .map_err(|e| guard_failed(name, string_args, &e.to_string()))?;
1154
1155            if is_boolean {
1156                Ok(GuardValue::Bool(matches!(result, Some(n) if n != 0)))
1157            } else {
1158                // COUNT(*) always returns a row, so None here means a malformed
1159                // builtin query — treat as 0 rather than panic.
1160                Ok(GuardValue::Number(result.unwrap_or(0)))
1161            }
1162        }
1163        #[cfg(not(feature = "mysql"))]
1164        DialectKind::Mysql => Err(WaypointError::ConfigError(
1165            "MySQL support is not compiled in".into(),
1166        )),
1167    }
1168}
1169
1170fn guard_failed(name: &str, args: &[String], reason: &str) -> WaypointError {
1171    WaypointError::GuardFailed {
1172        kind: "evaluation".to_string(),
1173        script: String::new(),
1174        expression: format!(
1175            "{name}({}) failed: {reason}",
1176            args.iter()
1177                .map(|a| format!("\"{a}\""))
1178                .collect::<Vec<_>>()
1179                .join(", ")
1180        ),
1181    }
1182}
1183
1184// ---------------------------------------------------------------------------
1185// Tests
1186// ---------------------------------------------------------------------------
1187
1188// Many of these reference the PG-specific `builtin_sql`. They cover the
1189// engine-agnostic parser too, but gating individual tests would be noisier
1190// than gating the module; the parser is covered under both the default
1191// (postgres) and `--features mysql` (postgres+mysql) builds.
1192#[cfg(all(test, feature = "postgres"))]
1193mod tests {
1194    use super::*;
1195
1196    #[test]
1197    fn test_parse_simple_function_call() {
1198        let expr = parse("table_exists(\"users\")").unwrap();
1199        match expr {
1200            GuardExpr::FunctionCall { name, args } => {
1201                assert_eq!(name, "table_exists");
1202                assert_eq!(args.len(), 1);
1203                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1204            }
1205            other => panic!("Expected FunctionCall, got {other:?}"),
1206        }
1207    }
1208
1209    #[test]
1210    fn test_parse_function_with_multiple_args() {
1211        let expr = parse("column_exists(\"users\", \"email\")").unwrap();
1212        match expr {
1213            GuardExpr::FunctionCall { name, args } => {
1214                assert_eq!(name, "column_exists");
1215                assert_eq!(args.len(), 2);
1216                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1217                assert_eq!(args[1], GuardExpr::StringLiteral("email".to_string()));
1218            }
1219            other => panic!("Expected FunctionCall, got {other:?}"),
1220        }
1221    }
1222
1223    #[test]
1224    fn test_parse_function_with_three_args() {
1225        let expr = parse("column_type(\"users\", \"age\", \"integer\")").unwrap();
1226        match expr {
1227            GuardExpr::FunctionCall { name, args } => {
1228                assert_eq!(name, "column_type");
1229                assert_eq!(args.len(), 3);
1230                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1231                assert_eq!(args[1], GuardExpr::StringLiteral("age".to_string()));
1232                assert_eq!(args[2], GuardExpr::StringLiteral("integer".to_string()));
1233            }
1234            other => panic!("Expected FunctionCall, got {other:?}"),
1235        }
1236    }
1237
1238    #[test]
1239    fn test_parse_and_expression() {
1240        let expr =
1241            parse("table_exists(\"users\") AND column_exists(\"users\", \"email\")").unwrap();
1242        match expr {
1243            GuardExpr::And(left, right) => {
1244                match *left {
1245                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1246                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1247                }
1248                match *right {
1249                    GuardExpr::FunctionCall { ref name, .. } => {
1250                        assert_eq!(name, "column_exists")
1251                    }
1252                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1253                }
1254            }
1255            other => panic!("Expected And, got {other:?}"),
1256        }
1257    }
1258
1259    #[test]
1260    fn test_parse_or_expression() {
1261        let expr = parse("table_exists(\"users\") OR table_exists(\"accounts\")").unwrap();
1262        match expr {
1263            GuardExpr::Or(left, right) => {
1264                match *left {
1265                    GuardExpr::FunctionCall { ref name, ref args } => {
1266                        assert_eq!(name, "table_exists");
1267                        assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1268                    }
1269                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1270                }
1271                match *right {
1272                    GuardExpr::FunctionCall { ref name, ref args } => {
1273                        assert_eq!(name, "table_exists");
1274                        assert_eq!(args[0], GuardExpr::StringLiteral("accounts".to_string()));
1275                    }
1276                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1277                }
1278            }
1279            other => panic!("Expected Or, got {other:?}"),
1280        }
1281    }
1282
1283    #[test]
1284    fn test_parse_not_expression() {
1285        let expr = parse("NOT table_exists(\"legacy\")").unwrap();
1286        match expr {
1287            GuardExpr::Not(inner) => match *inner {
1288                GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1289                ref other => panic!("Expected FunctionCall inside NOT, got {other:?}"),
1290            },
1291            other => panic!("Expected Not, got {other:?}"),
1292        }
1293    }
1294
1295    #[test]
1296    fn test_parse_double_not() {
1297        let expr = parse("NOT NOT table_exists(\"t\")").unwrap();
1298        match expr {
1299            GuardExpr::Not(inner) => match *inner {
1300                GuardExpr::Not(inner2) => match *inner2 {
1301                    GuardExpr::FunctionCall { ref name, .. } => {
1302                        assert_eq!(name, "table_exists")
1303                    }
1304                    ref other => panic!("Expected FunctionCall, got {other:?}"),
1305                },
1306                ref other => panic!("Expected Not, got {other:?}"),
1307            },
1308            other => panic!("Expected Not, got {other:?}"),
1309        }
1310    }
1311
1312    #[test]
1313    fn test_parse_nested_parentheses() {
1314        let expr =
1315            parse("(table_exists(\"a\") AND table_exists(\"b\")) OR table_exists(\"c\")").unwrap();
1316        match expr {
1317            GuardExpr::Or(left, right) => {
1318                match *left {
1319                    GuardExpr::And(_, _) => {} // good
1320                    ref other => panic!("Expected And on left, got {other:?}"),
1321                }
1322                match *right {
1323                    GuardExpr::FunctionCall { ref name, .. } => {
1324                        assert_eq!(name, "table_exists")
1325                    }
1326                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1327                }
1328            }
1329            other => panic!("Expected Or, got {other:?}"),
1330        }
1331    }
1332
1333    #[test]
1334    fn test_parse_deeply_nested_parentheses() {
1335        let expr = parse("((table_exists(\"a\")))").unwrap();
1336        match expr {
1337            GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1338            other => panic!("Expected FunctionCall, got {other:?}"),
1339        }
1340    }
1341
1342    #[test]
1343    fn test_parse_comparison_less_than() {
1344        let expr = parse("row_count(\"users\") < 1000").unwrap();
1345        match expr {
1346            GuardExpr::Comparison { left, op, right } => {
1347                match *left {
1348                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "row_count"),
1349                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1350                }
1351                assert_eq!(op, ComparisonOp::Lt);
1352                assert_eq!(*right, GuardExpr::NumberLiteral(1000));
1353            }
1354            other => panic!("Expected Comparison, got {other:?}"),
1355        }
1356    }
1357
1358    #[test]
1359    fn test_parse_comparison_greater_than() {
1360        let expr = parse("row_count(\"orders\") > 0").unwrap();
1361        match expr {
1362            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Gt),
1363            other => panic!("Expected Comparison, got {other:?}"),
1364        }
1365    }
1366
1367    #[test]
1368    fn test_parse_comparison_le_ge() {
1369        let expr = parse("row_count(\"t\") <= 500").unwrap();
1370        match expr {
1371            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Le),
1372            other => panic!("Expected Comparison, got {other:?}"),
1373        }
1374
1375        let expr = parse("row_count(\"t\") >= 10").unwrap();
1376        match expr {
1377            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Ge),
1378            other => panic!("Expected Comparison, got {other:?}"),
1379        }
1380    }
1381
1382    #[test]
1383    fn test_parse_error_empty() {
1384        let result = parse("");
1385        assert!(result.is_err());
1386        let err = result.unwrap_err().to_string();
1387        assert!(err.contains("empty expression"), "got: {err}");
1388    }
1389
1390    #[test]
1391    fn test_parse_error_unterminated_string() {
1392        let result = parse("table_exists(\"users)");
1393        assert!(result.is_err());
1394        let err = result.unwrap_err().to_string();
1395        assert!(err.contains("unterminated string"), "got: {err}");
1396    }
1397
1398    #[test]
1399    fn test_parse_error_unexpected_token() {
1400        let result = parse("AND");
1401        assert!(result.is_err());
1402    }
1403
1404    #[test]
1405    fn test_parse_error_missing_closing_paren() {
1406        let result = parse("table_exists(\"users\"");
1407        assert!(result.is_err());
1408        let err = result.unwrap_err().to_string();
1409        assert!(err.contains("expected ')'"), "got: {err}");
1410    }
1411
1412    #[test]
1413    fn test_parse_error_trailing_tokens() {
1414        let result = parse("table_exists(\"users\") table_exists(\"orders\")");
1415        assert!(result.is_err());
1416        let err = result.unwrap_err().to_string();
1417        assert!(err.contains("unexpected"), "got: {err}");
1418    }
1419
1420    #[test]
1421    fn test_parse_error_unexpected_character() {
1422        let result = parse("table_exists(\"users\") @ foo");
1423        assert!(result.is_err());
1424        let err = result.unwrap_err().to_string();
1425        assert!(err.contains("unexpected character"), "got: {err}");
1426    }
1427
1428    #[test]
1429    fn test_parse_complex_expression() {
1430        // (table_exists("users") AND NOT column_exists("users", "deleted_at"))
1431        //   OR (enum_exists("status") AND row_count("users") < 10000)
1432        let input = "(table_exists(\"users\") AND NOT column_exists(\"users\", \"deleted_at\")) \
1433                      OR (enum_exists(\"status\") AND row_count(\"users\") < 10000)";
1434        let expr = parse(input).unwrap();
1435        match expr {
1436            GuardExpr::Or(left, right) => {
1437                // Left: AND with NOT
1438                match *left {
1439                    GuardExpr::And(ref a, ref b) => {
1440                        match **a {
1441                            GuardExpr::FunctionCall { ref name, .. } => {
1442                                assert_eq!(name, "table_exists")
1443                            }
1444                            ref other => panic!("Expected FunctionCall, got {other:?}"),
1445                        }
1446                        match **b {
1447                            GuardExpr::Not(ref inner) => match **inner {
1448                                GuardExpr::FunctionCall { ref name, .. } => {
1449                                    assert_eq!(name, "column_exists")
1450                                }
1451                                ref other => panic!("Expected FunctionCall, got {other:?}"),
1452                            },
1453                            ref other => panic!("Expected Not, got {other:?}"),
1454                        }
1455                    }
1456                    ref other => panic!("Expected And, got {other:?}"),
1457                }
1458                // Right: AND with comparison
1459                match *right {
1460                    GuardExpr::And(ref a, ref b) => {
1461                        match **a {
1462                            GuardExpr::FunctionCall { ref name, .. } => {
1463                                assert_eq!(name, "enum_exists")
1464                            }
1465                            ref other => panic!("Expected FunctionCall, got {other:?}"),
1466                        }
1467                        match **b {
1468                            GuardExpr::Comparison {
1469                                ref op, ref right, ..
1470                            } => {
1471                                assert_eq!(*op, ComparisonOp::Lt);
1472                                assert_eq!(**right, GuardExpr::NumberLiteral(10000));
1473                            }
1474                            ref other => panic!("Expected Comparison, got {other:?}"),
1475                        }
1476                    }
1477                    ref other => panic!("Expected And, got {other:?}"),
1478                }
1479            }
1480            other => panic!("Expected Or, got {other:?}"),
1481        }
1482    }
1483
1484    #[test]
1485    fn test_parse_and_or_precedence() {
1486        // AND binds tighter than OR:
1487        // a OR b AND c  =>  a OR (b AND c)
1488        let expr =
1489            parse("table_exists(\"a\") OR table_exists(\"b\") AND table_exists(\"c\")").unwrap();
1490        match expr {
1491            GuardExpr::Or(left, right) => {
1492                match *left {
1493                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1494                    ref other => panic!("Expected FunctionCall, got {other:?}"),
1495                }
1496                match *right {
1497                    GuardExpr::And(_, _) => {} // correct: AND grouped first
1498                    ref other => panic!("Expected And on right, got {other:?}"),
1499                }
1500            }
1501            other => panic!("Expected Or, got {other:?}"),
1502        }
1503    }
1504
1505    #[test]
1506    fn test_parse_chained_and() {
1507        let expr =
1508            parse("table_exists(\"a\") AND table_exists(\"b\") AND table_exists(\"c\")").unwrap();
1509        // Should be left-associative: (a AND b) AND c
1510        match expr {
1511            GuardExpr::And(left, right) => {
1512                match *left {
1513                    GuardExpr::And(_, _) => {} // left is itself an AND
1514                    ref other => panic!("Expected And on left (left-assoc), got {other:?}"),
1515                }
1516                match *right {
1517                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1518                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1519                }
1520            }
1521            other => panic!("Expected And, got {other:?}"),
1522        }
1523    }
1524
1525    #[test]
1526    fn test_parse_sql_function() {
1527        let expr = parse("sql(\"SELECT true\")").unwrap();
1528        match expr {
1529            GuardExpr::FunctionCall { name, args } => {
1530                assert_eq!(name, "sql");
1531                assert_eq!(args.len(), 1);
1532                assert_eq!(args[0], GuardExpr::StringLiteral("SELECT true".to_string()));
1533            }
1534            other => panic!("Expected FunctionCall, got {other:?}"),
1535        }
1536    }
1537
1538    #[test]
1539    fn test_parse_not_with_parentheses() {
1540        let expr = parse("NOT (table_exists(\"a\") OR table_exists(\"b\"))").unwrap();
1541        match expr {
1542            GuardExpr::Not(inner) => match *inner {
1543                GuardExpr::Or(_, _) => {} // correct
1544                ref other => panic!("Expected Or inside Not, got {other:?}"),
1545            },
1546            other => panic!("Expected Not, got {other:?}"),
1547        }
1548    }
1549
1550    #[test]
1551    fn test_parse_boolean_literals() {
1552        let expr = parse("true").unwrap();
1553        assert_eq!(expr, GuardExpr::BoolLiteral(true));
1554
1555        let expr = parse("false").unwrap();
1556        assert_eq!(expr, GuardExpr::BoolLiteral(false));
1557    }
1558
1559    #[test]
1560    fn test_tokenize_all_operators() {
1561        let tokens = tokenize("< > <= >= AND OR NOT ( ) ,").unwrap();
1562        assert_eq!(
1563            tokens,
1564            vec![
1565                Token::Lt,
1566                Token::Gt,
1567                Token::Le,
1568                Token::Ge,
1569                Token::And,
1570                Token::Or,
1571                Token::Not,
1572                Token::LParen,
1573                Token::RParen,
1574                Token::Comma,
1575            ]
1576        );
1577    }
1578
1579    #[test]
1580    fn test_builtin_sql_table_exists() {
1581        let (sql, params, is_bool) =
1582            builtin_sql("table_exists", &["users".to_string()], "public").unwrap();
1583        assert!(is_bool);
1584        assert!(sql.contains("information_schema.tables"));
1585        assert!(sql.contains("$1"));
1586        assert!(sql.contains("$2"));
1587        assert_eq!(params, vec!["public", "users"]);
1588    }
1589
1590    #[test]
1591    fn test_builtin_sql_column_exists() {
1592        let (sql, params, is_bool) = builtin_sql(
1593            "column_exists",
1594            &["users".to_string(), "email".to_string()],
1595            "public",
1596        )
1597        .unwrap();
1598        assert!(is_bool);
1599        assert!(sql.contains("information_schema.columns"));
1600        assert!(sql.contains("$3"));
1601        assert_eq!(params, vec!["public", "users", "email"]);
1602    }
1603
1604    #[test]
1605    fn test_builtin_sql_row_count() {
1606        let (sql, params, is_bool) =
1607            builtin_sql("row_count", &["users".to_string()], "public").unwrap();
1608        assert!(!is_bool);
1609        assert!(sql.contains("pg_stat_user_tables"));
1610        assert!(sql.contains("n_live_tup"));
1611        assert_eq!(params, vec!["public", "users"]);
1612    }
1613
1614    #[test]
1615    fn test_builtin_sql_unknown_function() {
1616        let result = builtin_sql("unknown_fn", &[], "public");
1617        assert!(result.is_err());
1618        let err = result.unwrap_err().to_string();
1619        assert!(err.contains("unknown function"), "got: {err}");
1620    }
1621
1622    #[test]
1623    fn test_builtin_sql_wrong_arg_count() {
1624        let result = builtin_sql("table_exists", &[], "public");
1625        assert!(result.is_err());
1626        let err = result.unwrap_err().to_string();
1627        assert!(err.contains("expects 1 argument"), "got: {err}");
1628    }
1629
1630    #[test]
1631    fn test_parse_depth_limit() {
1632        // Build a deeply nested expression: NOT NOT NOT ... NOT true
1633        let mut expr = String::new();
1634        for _ in 0..100 {
1635            expr.push_str("NOT ");
1636        }
1637        expr.push_str("true");
1638        let result = parse(&expr);
1639        assert!(result.is_err());
1640        let err = result.unwrap_err().to_string();
1641        assert!(err.contains("maximum nesting depth exceeded"), "got: {err}");
1642    }
1643
1644    #[test]
1645    fn test_builtin_sql_column_type() {
1646        let (sql, params, is_bool) = builtin_sql(
1647            "column_type",
1648            &[
1649                "users".to_string(),
1650                "age".to_string(),
1651                "integer".to_string(),
1652            ],
1653            "myschema",
1654        )
1655        .unwrap();
1656        assert!(is_bool);
1657        assert!(sql.contains("data_type = $4"));
1658        assert_eq!(params, vec!["myschema", "users", "age", "integer"]);
1659    }
1660
1661    #[test]
1662    fn test_builtin_sql_column_nullable() {
1663        let (sql, params, is_bool) = builtin_sql(
1664            "column_nullable",
1665            &["users".to_string(), "name".to_string()],
1666            "public",
1667        )
1668        .unwrap();
1669        assert!(is_bool);
1670        assert!(sql.contains("is_nullable = 'YES'"));
1671        assert_eq!(params, vec!["public", "users", "name"]);
1672    }
1673
1674    #[test]
1675    fn test_builtin_sql_index_exists() {
1676        let (sql, params, is_bool) =
1677            builtin_sql("index_exists", &["idx_users_email".to_string()], "public").unwrap();
1678        assert!(is_bool);
1679        assert!(sql.contains("pg_indexes"));
1680        assert!(sql.contains("$2"));
1681        assert_eq!(params, vec!["public", "idx_users_email"]);
1682    }
1683
1684    #[test]
1685    fn test_builtin_sql_constraint_exists() {
1686        let (sql, params, is_bool) = builtin_sql(
1687            "constraint_exists",
1688            &["users".to_string(), "users_pkey".to_string()],
1689            "public",
1690        )
1691        .unwrap();
1692        assert!(is_bool);
1693        assert!(sql.contains("table_constraints"));
1694        assert!(sql.contains("$3"));
1695        assert_eq!(params, vec!["public", "users", "users_pkey"]);
1696    }
1697
1698    #[test]
1699    fn test_builtin_sql_function_exists() {
1700        let (sql, params, is_bool) =
1701            builtin_sql("function_exists", &["my_func".to_string()], "public").unwrap();
1702        assert!(is_bool);
1703        assert!(sql.contains("pg_proc"));
1704        assert!(sql.contains("pg_namespace"));
1705        assert!(sql.contains("$2"));
1706        assert_eq!(params, vec!["public", "my_func"]);
1707    }
1708
1709    #[test]
1710    fn test_builtin_sql_enum_exists() {
1711        let (sql, params, is_bool) =
1712            builtin_sql("enum_exists", &["status_type".to_string()], "public").unwrap();
1713        assert!(is_bool);
1714        assert!(sql.contains("pg_type"));
1715        assert!(sql.contains("typtype = 'e'"));
1716        assert!(sql.contains("$2"));
1717        assert_eq!(params, vec!["public", "status_type"]);
1718    }
1719
1720    #[test]
1721    fn test_builtin_sql_custom_sql() {
1722        let (sql, params, is_bool) = builtin_sql(
1723            "sql",
1724            &["SELECT count(*) = 0 FROM old_table".to_string()],
1725            "public",
1726        )
1727        .unwrap();
1728        assert!(is_bool);
1729        assert_eq!(sql, "SELECT count(*) = 0 FROM old_table");
1730        assert!(params.is_empty());
1731    }
1732
1733    #[test]
1734    fn test_builtin_sql_params_order_table_exists() {
1735        let (sql, params, is_bool) =
1736            builtin_sql("table_exists", &["users".to_string()], "myschema").unwrap();
1737        assert!(is_bool);
1738        assert_eq!(params.len(), 2);
1739        assert_eq!(params[0], "myschema");
1740        assert_eq!(params[1], "users");
1741        assert!(sql.contains("$1"));
1742        assert!(sql.contains("$2"));
1743    }
1744
1745    #[test]
1746    fn test_builtin_sql_sql_function_empty_params() {
1747        let (sql, params, is_bool) =
1748            builtin_sql("sql", &["SELECT 1".to_string()], "public").unwrap();
1749        assert!(is_bool);
1750        assert!(params.is_empty());
1751        assert_eq!(sql, "SELECT 1");
1752    }
1753
1754    #[test]
1755    fn test_parse_empty_function_args() {
1756        // table_exists() with no args should be parsed OK but builtin_sql should reject it
1757        let expr = parse("table_exists()").unwrap();
1758        match expr {
1759            GuardExpr::FunctionCall { name, args } => {
1760                assert_eq!(name, "table_exists");
1761                assert!(args.is_empty());
1762            }
1763            other => panic!("Expected FunctionCall, got {other:?}"),
1764        }
1765    }
1766}