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                // See the dialect-aware twin below: `query_opt` so a `row_count`
969                // against something that is not a plain table reports *why*.
970                let row = client
971                    .query_opt(&sql, &params)
972                    .await
973                    .map_err(|e| guard_failed(name, &string_args, &e.to_string()))?
974                    .ok_or_else(|| {
975                        guard_failed(name, &string_args, &missing_row_reason(&string_args))
976                    })?;
977
978                if is_boolean {
979                    let val: bool = row.get(0);
980                    Ok(GuardValue::Bool(val))
981                } else {
982                    let val: i64 = row.get(0);
983                    Ok(GuardValue::Number(val))
984                }
985            }
986        }
987    })
988}
989
990// ---------------------------------------------------------------------------
991// Dialect-aware evaluator
992// ---------------------------------------------------------------------------
993
994/// Evaluate a guard expression against a [`DbClient`] (dialect-aware entry).
995///
996/// Dispatches to the PostgreSQL or MySQL implementation based on the connection
997/// kind. Recursion shape mirrors the legacy [`evaluate`] function; only the
998/// leaf `FunctionCall` arm differs per engine.
999pub async fn evaluate_db(client: &DbClient, schema: &str, expr: &GuardExpr) -> Result<bool> {
1000    let value = eval_expr_db(client, schema, expr).await?;
1001    match value {
1002        GuardValue::Bool(b) => Ok(b),
1003        other => Err(WaypointError::ConfigError(format!(
1004            "Guard expression: expected boolean result, got {other}"
1005        ))),
1006    }
1007}
1008
1009fn eval_expr_db<'a>(
1010    client: &'a DbClient,
1011    schema: &'a str,
1012    expr: &'a GuardExpr,
1013) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<GuardValue>> + Send + 'a>> {
1014    Box::pin(async move {
1015        match expr {
1016            GuardExpr::BoolLiteral(b) => Ok(GuardValue::Bool(*b)),
1017            GuardExpr::NumberLiteral(n) => Ok(GuardValue::Number(*n)),
1018            GuardExpr::StringLiteral(s) => Ok(GuardValue::Str(s.clone())),
1019
1020            GuardExpr::Not(inner) => {
1021                let val = eval_expr_db(client, schema, inner).await?;
1022                match val {
1023                    GuardValue::Bool(b) => Ok(GuardValue::Bool(!b)),
1024                    other => Err(WaypointError::ConfigError(format!(
1025                        "Guard expression: NOT requires boolean, got {other}"
1026                    ))),
1027                }
1028            }
1029
1030            GuardExpr::And(left, right) => {
1031                let lval = eval_expr_db(client, schema, left).await?;
1032                match lval {
1033                    GuardValue::Bool(false) => Ok(GuardValue::Bool(false)),
1034                    GuardValue::Bool(true) => {
1035                        let rval = eval_expr_db(client, schema, right).await?;
1036                        match rval {
1037                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
1038                            other => Err(WaypointError::ConfigError(format!(
1039                                "Guard expression: AND requires boolean operands, got {other}"
1040                            ))),
1041                        }
1042                    }
1043                    other => Err(WaypointError::ConfigError(format!(
1044                        "Guard expression: AND requires boolean operands, got {other}"
1045                    ))),
1046                }
1047            }
1048
1049            GuardExpr::Or(left, right) => {
1050                let lval = eval_expr_db(client, schema, left).await?;
1051                match lval {
1052                    GuardValue::Bool(true) => Ok(GuardValue::Bool(true)),
1053                    GuardValue::Bool(false) => {
1054                        let rval = eval_expr_db(client, schema, right).await?;
1055                        match rval {
1056                            GuardValue::Bool(b) => Ok(GuardValue::Bool(b)),
1057                            other => Err(WaypointError::ConfigError(format!(
1058                                "Guard expression: OR requires boolean operands, got {other}"
1059                            ))),
1060                        }
1061                    }
1062                    other => Err(WaypointError::ConfigError(format!(
1063                        "Guard expression: OR requires boolean operands, got {other}"
1064                    ))),
1065                }
1066            }
1067
1068            GuardExpr::Comparison { left, op, right } => {
1069                let lval = eval_expr_db(client, schema, left).await?;
1070                let rval = eval_expr_db(client, schema, right).await?;
1071                match (&lval, &rval) {
1072                    (GuardValue::Number(a), GuardValue::Number(b)) => {
1073                        let result = match op {
1074                            ComparisonOp::Lt => a < b,
1075                            ComparisonOp::Gt => a > b,
1076                            ComparisonOp::Le => a <= b,
1077                            ComparisonOp::Ge => a >= b,
1078                        };
1079                        Ok(GuardValue::Bool(result))
1080                    }
1081                    _ => Err(WaypointError::ConfigError(format!(
1082                        "Guard expression: comparison requires numeric operands, got {lval} {op} {rval}"
1083                    ))),
1084                }
1085            }
1086
1087            GuardExpr::FunctionCall { name, args } => {
1088                let string_args = extract_string_args(args)?;
1089                exec_builtin(client, schema, name, &string_args).await
1090            }
1091        }
1092    })
1093}
1094
1095/// Execute a built-in guard function against the configured backend.
1096async fn exec_builtin(
1097    client: &DbClient,
1098    schema: &str,
1099    name: &str,
1100    string_args: &[String],
1101) -> Result<GuardValue> {
1102    match client.dialect_kind() {
1103        #[cfg(feature = "postgres")]
1104        DialectKind::Postgres => {
1105            let (sql, param_values, is_boolean) = builtin_sql(name, string_args, schema)?;
1106            let pg = client.as_postgres()?;
1107            let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = param_values
1108                .iter()
1109                .map(|s| s as &(dyn tokio_postgres::types::ToSql + Sync))
1110                .collect();
1111            // `query_opt`, not `query_one`: `row_count` reads
1112            // `pg_stat_user_tables`, which has no row for a name that is not a
1113            // plain table. `query_one` turned that into tokio-postgres's
1114            // "query returned an unexpected number of rows", which tells the
1115            // operator nothing about what is actually wrong.
1116            let row = pg
1117                .query_opt(&sql, &params)
1118                .await
1119                .map_err(|e| guard_failed(name, string_args, &e.to_string()))?
1120                .ok_or_else(|| guard_failed(name, string_args, &missing_row_reason(string_args)))?;
1121            if is_boolean {
1122                Ok(GuardValue::Bool(row.get(0)))
1123            } else {
1124                Ok(GuardValue::Number(row.get(0)))
1125            }
1126        }
1127        #[cfg(not(feature = "postgres"))]
1128        DialectKind::Postgres => Err(WaypointError::ConfigError(
1129            "PostgreSQL support is not compiled in".into(),
1130        )),
1131        #[cfg(feature = "mysql")]
1132        DialectKind::Mysql => {
1133            use mysql_async::prelude::*;
1134            let (sql, param_values, is_boolean) = builtin_sql_mysql(name, string_args, schema)?;
1135            let pool = client.as_mysql()?;
1136            let mut conn = pool
1137                .get_conn()
1138                .await
1139                .map_err(|e| guard_failed(name, string_args, &e.to_string()))?;
1140
1141            // information_schema EXISTS(...) and COUNT(*) both return a single
1142            // i64 column on MySQL — read as Option<i64> to share the param-
1143            // binding path between the boolean and numeric builtins (avoids the
1144            // chrono-feature ambiguity around bool decoding).
1145            let result: Option<i64> = if param_values.is_empty() {
1146                conn.query_first(&sql).await
1147            } else {
1148                let params: Vec<mysql_async::Value> = param_values
1149                    .iter()
1150                    .map(|s| mysql_async::Value::Bytes(s.as_bytes().to_vec()))
1151                    .collect();
1152                conn.exec_first(&sql, params).await
1153            }
1154            .map_err(|e| guard_failed(name, string_args, &e.to_string()))?;
1155
1156            if is_boolean {
1157                Ok(GuardValue::Bool(matches!(result, Some(n) if n != 0)))
1158            } else {
1159                // `row_count` selects from `information_schema.tables`, which
1160                // returns **no row at all** for a name that does not exist —
1161                // the old code's `unwrap_or(0)` therefore made
1162                // `row_count("typo") < 1000000` quietly *true*, so a guard the
1163                // operator wrote as a safety check passed vacuously. (The
1164                // comment here used to claim "COUNT(*) always returns a row",
1165                // which was never what this query does.) PostgreSQL errors on
1166                // the same input, so this also keeps the two engines in step.
1167                result.map(GuardValue::Number).ok_or_else(|| {
1168                    guard_failed(name, string_args, &missing_row_reason(string_args))
1169                })
1170            }
1171        }
1172        #[cfg(not(feature = "mysql"))]
1173        DialectKind::Mysql => Err(WaypointError::ConfigError(
1174            "MySQL support is not compiled in".into(),
1175        )),
1176    }
1177}
1178
1179/// Explain a numeric builtin that matched no catalog row.
1180///
1181/// The only numeric builtin is `row_count`, and it reads a statistics/catalog
1182/// view rather than the table itself. "No row" therefore means the name is not
1183/// a plain table there — most often a typo, but also a view or a partitioned
1184/// parent. Saying so is the difference between an operator fixing the guard and
1185/// staring at "query returned an unexpected number of rows".
1186fn missing_row_reason(args: &[String]) -> String {
1187    let target = args.first().map(String::as_str).unwrap_or("<unknown>");
1188    format!(
1189        "no row-count statistics for '{target}' — it may not exist in this schema, \
1190         or may be a view or partitioned parent rather than a plain table"
1191    )
1192}
1193
1194fn guard_failed(name: &str, args: &[String], reason: &str) -> WaypointError {
1195    WaypointError::GuardFailed {
1196        kind: "evaluation".to_string(),
1197        script: String::new(),
1198        expression: format!(
1199            "{name}({}) failed: {reason}",
1200            args.iter()
1201                .map(|a| format!("\"{a}\""))
1202                .collect::<Vec<_>>()
1203                .join(", ")
1204        ),
1205    }
1206}
1207
1208// ---------------------------------------------------------------------------
1209// Tests
1210// ---------------------------------------------------------------------------
1211
1212// Many of these reference the PG-specific `builtin_sql`. They cover the
1213// engine-agnostic parser too, but gating individual tests would be noisier
1214// than gating the module; the parser is covered under both the default
1215// (postgres) and `--features mysql` (postgres+mysql) builds.
1216#[cfg(all(test, feature = "postgres"))]
1217mod tests {
1218    use super::*;
1219
1220    #[test]
1221    fn test_parse_simple_function_call() {
1222        let expr = parse("table_exists(\"users\")").unwrap();
1223        match expr {
1224            GuardExpr::FunctionCall { name, args } => {
1225                assert_eq!(name, "table_exists");
1226                assert_eq!(args.len(), 1);
1227                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1228            }
1229            other => panic!("Expected FunctionCall, got {other:?}"),
1230        }
1231    }
1232
1233    #[test]
1234    fn test_parse_function_with_multiple_args() {
1235        let expr = parse("column_exists(\"users\", \"email\")").unwrap();
1236        match expr {
1237            GuardExpr::FunctionCall { name, args } => {
1238                assert_eq!(name, "column_exists");
1239                assert_eq!(args.len(), 2);
1240                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1241                assert_eq!(args[1], GuardExpr::StringLiteral("email".to_string()));
1242            }
1243            other => panic!("Expected FunctionCall, got {other:?}"),
1244        }
1245    }
1246
1247    #[test]
1248    fn test_parse_function_with_three_args() {
1249        let expr = parse("column_type(\"users\", \"age\", \"integer\")").unwrap();
1250        match expr {
1251            GuardExpr::FunctionCall { name, args } => {
1252                assert_eq!(name, "column_type");
1253                assert_eq!(args.len(), 3);
1254                assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1255                assert_eq!(args[1], GuardExpr::StringLiteral("age".to_string()));
1256                assert_eq!(args[2], GuardExpr::StringLiteral("integer".to_string()));
1257            }
1258            other => panic!("Expected FunctionCall, got {other:?}"),
1259        }
1260    }
1261
1262    #[test]
1263    fn test_parse_and_expression() {
1264        let expr =
1265            parse("table_exists(\"users\") AND column_exists(\"users\", \"email\")").unwrap();
1266        match expr {
1267            GuardExpr::And(left, right) => {
1268                match *left {
1269                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1270                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1271                }
1272                match *right {
1273                    GuardExpr::FunctionCall { ref name, .. } => {
1274                        assert_eq!(name, "column_exists")
1275                    }
1276                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1277                }
1278            }
1279            other => panic!("Expected And, got {other:?}"),
1280        }
1281    }
1282
1283    #[test]
1284    fn test_parse_or_expression() {
1285        let expr = parse("table_exists(\"users\") OR table_exists(\"accounts\")").unwrap();
1286        match expr {
1287            GuardExpr::Or(left, right) => {
1288                match *left {
1289                    GuardExpr::FunctionCall { ref name, ref args } => {
1290                        assert_eq!(name, "table_exists");
1291                        assert_eq!(args[0], GuardExpr::StringLiteral("users".to_string()));
1292                    }
1293                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1294                }
1295                match *right {
1296                    GuardExpr::FunctionCall { ref name, ref args } => {
1297                        assert_eq!(name, "table_exists");
1298                        assert_eq!(args[0], GuardExpr::StringLiteral("accounts".to_string()));
1299                    }
1300                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1301                }
1302            }
1303            other => panic!("Expected Or, got {other:?}"),
1304        }
1305    }
1306
1307    #[test]
1308    fn test_parse_not_expression() {
1309        let expr = parse("NOT table_exists(\"legacy\")").unwrap();
1310        match expr {
1311            GuardExpr::Not(inner) => match *inner {
1312                GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1313                ref other => panic!("Expected FunctionCall inside NOT, got {other:?}"),
1314            },
1315            other => panic!("Expected Not, got {other:?}"),
1316        }
1317    }
1318
1319    #[test]
1320    fn test_parse_double_not() {
1321        let expr = parse("NOT NOT table_exists(\"t\")").unwrap();
1322        match expr {
1323            GuardExpr::Not(inner) => match *inner {
1324                GuardExpr::Not(inner2) => match *inner2 {
1325                    GuardExpr::FunctionCall { ref name, .. } => {
1326                        assert_eq!(name, "table_exists")
1327                    }
1328                    ref other => panic!("Expected FunctionCall, got {other:?}"),
1329                },
1330                ref other => panic!("Expected Not, got {other:?}"),
1331            },
1332            other => panic!("Expected Not, got {other:?}"),
1333        }
1334    }
1335
1336    #[test]
1337    fn test_parse_nested_parentheses() {
1338        let expr =
1339            parse("(table_exists(\"a\") AND table_exists(\"b\")) OR table_exists(\"c\")").unwrap();
1340        match expr {
1341            GuardExpr::Or(left, right) => {
1342                match *left {
1343                    GuardExpr::And(_, _) => {} // good
1344                    ref other => panic!("Expected And on left, got {other:?}"),
1345                }
1346                match *right {
1347                    GuardExpr::FunctionCall { ref name, .. } => {
1348                        assert_eq!(name, "table_exists")
1349                    }
1350                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1351                }
1352            }
1353            other => panic!("Expected Or, got {other:?}"),
1354        }
1355    }
1356
1357    #[test]
1358    fn test_parse_deeply_nested_parentheses() {
1359        let expr = parse("((table_exists(\"a\")))").unwrap();
1360        match expr {
1361            GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1362            other => panic!("Expected FunctionCall, got {other:?}"),
1363        }
1364    }
1365
1366    #[test]
1367    fn test_parse_comparison_less_than() {
1368        let expr = parse("row_count(\"users\") < 1000").unwrap();
1369        match expr {
1370            GuardExpr::Comparison { left, op, right } => {
1371                match *left {
1372                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "row_count"),
1373                    ref other => panic!("Expected FunctionCall on left, got {other:?}"),
1374                }
1375                assert_eq!(op, ComparisonOp::Lt);
1376                assert_eq!(*right, GuardExpr::NumberLiteral(1000));
1377            }
1378            other => panic!("Expected Comparison, got {other:?}"),
1379        }
1380    }
1381
1382    #[test]
1383    fn test_parse_comparison_greater_than() {
1384        let expr = parse("row_count(\"orders\") > 0").unwrap();
1385        match expr {
1386            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Gt),
1387            other => panic!("Expected Comparison, got {other:?}"),
1388        }
1389    }
1390
1391    #[test]
1392    fn test_parse_comparison_le_ge() {
1393        let expr = parse("row_count(\"t\") <= 500").unwrap();
1394        match expr {
1395            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Le),
1396            other => panic!("Expected Comparison, got {other:?}"),
1397        }
1398
1399        let expr = parse("row_count(\"t\") >= 10").unwrap();
1400        match expr {
1401            GuardExpr::Comparison { op, .. } => assert_eq!(op, ComparisonOp::Ge),
1402            other => panic!("Expected Comparison, got {other:?}"),
1403        }
1404    }
1405
1406    #[test]
1407    fn test_parse_error_empty() {
1408        let result = parse("");
1409        assert!(result.is_err());
1410        let err = result.unwrap_err().to_string();
1411        assert!(err.contains("empty expression"), "got: {err}");
1412    }
1413
1414    #[test]
1415    fn test_parse_error_unterminated_string() {
1416        let result = parse("table_exists(\"users)");
1417        assert!(result.is_err());
1418        let err = result.unwrap_err().to_string();
1419        assert!(err.contains("unterminated string"), "got: {err}");
1420    }
1421
1422    #[test]
1423    fn test_parse_error_unexpected_token() {
1424        let result = parse("AND");
1425        assert!(result.is_err());
1426    }
1427
1428    #[test]
1429    fn test_parse_error_missing_closing_paren() {
1430        let result = parse("table_exists(\"users\"");
1431        assert!(result.is_err());
1432        let err = result.unwrap_err().to_string();
1433        assert!(err.contains("expected ')'"), "got: {err}");
1434    }
1435
1436    #[test]
1437    fn test_parse_error_trailing_tokens() {
1438        let result = parse("table_exists(\"users\") table_exists(\"orders\")");
1439        assert!(result.is_err());
1440        let err = result.unwrap_err().to_string();
1441        assert!(err.contains("unexpected"), "got: {err}");
1442    }
1443
1444    #[test]
1445    fn test_parse_error_unexpected_character() {
1446        let result = parse("table_exists(\"users\") @ foo");
1447        assert!(result.is_err());
1448        let err = result.unwrap_err().to_string();
1449        assert!(err.contains("unexpected character"), "got: {err}");
1450    }
1451
1452    #[test]
1453    fn test_parse_complex_expression() {
1454        // (table_exists("users") AND NOT column_exists("users", "deleted_at"))
1455        //   OR (enum_exists("status") AND row_count("users") < 10000)
1456        let input = "(table_exists(\"users\") AND NOT column_exists(\"users\", \"deleted_at\")) \
1457                      OR (enum_exists(\"status\") AND row_count(\"users\") < 10000)";
1458        let expr = parse(input).unwrap();
1459        match expr {
1460            GuardExpr::Or(left, right) => {
1461                // Left: AND with NOT
1462                match *left {
1463                    GuardExpr::And(ref a, ref b) => {
1464                        match **a {
1465                            GuardExpr::FunctionCall { ref name, .. } => {
1466                                assert_eq!(name, "table_exists")
1467                            }
1468                            ref other => panic!("Expected FunctionCall, got {other:?}"),
1469                        }
1470                        match **b {
1471                            GuardExpr::Not(ref inner) => match **inner {
1472                                GuardExpr::FunctionCall { ref name, .. } => {
1473                                    assert_eq!(name, "column_exists")
1474                                }
1475                                ref other => panic!("Expected FunctionCall, got {other:?}"),
1476                            },
1477                            ref other => panic!("Expected Not, got {other:?}"),
1478                        }
1479                    }
1480                    ref other => panic!("Expected And, got {other:?}"),
1481                }
1482                // Right: AND with comparison
1483                match *right {
1484                    GuardExpr::And(ref a, ref b) => {
1485                        match **a {
1486                            GuardExpr::FunctionCall { ref name, .. } => {
1487                                assert_eq!(name, "enum_exists")
1488                            }
1489                            ref other => panic!("Expected FunctionCall, got {other:?}"),
1490                        }
1491                        match **b {
1492                            GuardExpr::Comparison {
1493                                ref op, ref right, ..
1494                            } => {
1495                                assert_eq!(*op, ComparisonOp::Lt);
1496                                assert_eq!(**right, GuardExpr::NumberLiteral(10000));
1497                            }
1498                            ref other => panic!("Expected Comparison, got {other:?}"),
1499                        }
1500                    }
1501                    ref other => panic!("Expected And, got {other:?}"),
1502                }
1503            }
1504            other => panic!("Expected Or, got {other:?}"),
1505        }
1506    }
1507
1508    #[test]
1509    fn test_parse_and_or_precedence() {
1510        // AND binds tighter than OR:
1511        // a OR b AND c  =>  a OR (b AND c)
1512        let expr =
1513            parse("table_exists(\"a\") OR table_exists(\"b\") AND table_exists(\"c\")").unwrap();
1514        match expr {
1515            GuardExpr::Or(left, right) => {
1516                match *left {
1517                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1518                    ref other => panic!("Expected FunctionCall, got {other:?}"),
1519                }
1520                match *right {
1521                    GuardExpr::And(_, _) => {} // correct: AND grouped first
1522                    ref other => panic!("Expected And on right, got {other:?}"),
1523                }
1524            }
1525            other => panic!("Expected Or, got {other:?}"),
1526        }
1527    }
1528
1529    #[test]
1530    fn test_parse_chained_and() {
1531        let expr =
1532            parse("table_exists(\"a\") AND table_exists(\"b\") AND table_exists(\"c\")").unwrap();
1533        // Should be left-associative: (a AND b) AND c
1534        match expr {
1535            GuardExpr::And(left, right) => {
1536                match *left {
1537                    GuardExpr::And(_, _) => {} // left is itself an AND
1538                    ref other => panic!("Expected And on left (left-assoc), got {other:?}"),
1539                }
1540                match *right {
1541                    GuardExpr::FunctionCall { ref name, .. } => assert_eq!(name, "table_exists"),
1542                    ref other => panic!("Expected FunctionCall on right, got {other:?}"),
1543                }
1544            }
1545            other => panic!("Expected And, got {other:?}"),
1546        }
1547    }
1548
1549    #[test]
1550    fn test_parse_sql_function() {
1551        let expr = parse("sql(\"SELECT true\")").unwrap();
1552        match expr {
1553            GuardExpr::FunctionCall { name, args } => {
1554                assert_eq!(name, "sql");
1555                assert_eq!(args.len(), 1);
1556                assert_eq!(args[0], GuardExpr::StringLiteral("SELECT true".to_string()));
1557            }
1558            other => panic!("Expected FunctionCall, got {other:?}"),
1559        }
1560    }
1561
1562    #[test]
1563    fn test_parse_not_with_parentheses() {
1564        let expr = parse("NOT (table_exists(\"a\") OR table_exists(\"b\"))").unwrap();
1565        match expr {
1566            GuardExpr::Not(inner) => match *inner {
1567                GuardExpr::Or(_, _) => {} // correct
1568                ref other => panic!("Expected Or inside Not, got {other:?}"),
1569            },
1570            other => panic!("Expected Not, got {other:?}"),
1571        }
1572    }
1573
1574    #[test]
1575    fn test_parse_boolean_literals() {
1576        let expr = parse("true").unwrap();
1577        assert_eq!(expr, GuardExpr::BoolLiteral(true));
1578
1579        let expr = parse("false").unwrap();
1580        assert_eq!(expr, GuardExpr::BoolLiteral(false));
1581    }
1582
1583    #[test]
1584    fn test_tokenize_all_operators() {
1585        let tokens = tokenize("< > <= >= AND OR NOT ( ) ,").unwrap();
1586        assert_eq!(
1587            tokens,
1588            vec![
1589                Token::Lt,
1590                Token::Gt,
1591                Token::Le,
1592                Token::Ge,
1593                Token::And,
1594                Token::Or,
1595                Token::Not,
1596                Token::LParen,
1597                Token::RParen,
1598                Token::Comma,
1599            ]
1600        );
1601    }
1602
1603    #[test]
1604    fn test_builtin_sql_table_exists() {
1605        let (sql, params, is_bool) =
1606            builtin_sql("table_exists", &["users".to_string()], "public").unwrap();
1607        assert!(is_bool);
1608        assert!(sql.contains("information_schema.tables"));
1609        assert!(sql.contains("$1"));
1610        assert!(sql.contains("$2"));
1611        assert_eq!(params, vec!["public", "users"]);
1612    }
1613
1614    #[test]
1615    fn test_builtin_sql_column_exists() {
1616        let (sql, params, is_bool) = builtin_sql(
1617            "column_exists",
1618            &["users".to_string(), "email".to_string()],
1619            "public",
1620        )
1621        .unwrap();
1622        assert!(is_bool);
1623        assert!(sql.contains("information_schema.columns"));
1624        assert!(sql.contains("$3"));
1625        assert_eq!(params, vec!["public", "users", "email"]);
1626    }
1627
1628    #[test]
1629    fn test_builtin_sql_row_count() {
1630        let (sql, params, is_bool) =
1631            builtin_sql("row_count", &["users".to_string()], "public").unwrap();
1632        assert!(!is_bool);
1633        assert!(sql.contains("pg_stat_user_tables"));
1634        assert!(sql.contains("n_live_tup"));
1635        assert_eq!(params, vec!["public", "users"]);
1636    }
1637
1638    #[test]
1639    fn test_builtin_sql_unknown_function() {
1640        let result = builtin_sql("unknown_fn", &[], "public");
1641        assert!(result.is_err());
1642        let err = result.unwrap_err().to_string();
1643        assert!(err.contains("unknown function"), "got: {err}");
1644    }
1645
1646    #[test]
1647    fn test_builtin_sql_wrong_arg_count() {
1648        let result = builtin_sql("table_exists", &[], "public");
1649        assert!(result.is_err());
1650        let err = result.unwrap_err().to_string();
1651        assert!(err.contains("expects 1 argument"), "got: {err}");
1652    }
1653
1654    #[test]
1655    fn test_parse_depth_limit() {
1656        // Build a deeply nested expression: NOT NOT NOT ... NOT true
1657        let mut expr = String::new();
1658        for _ in 0..100 {
1659            expr.push_str("NOT ");
1660        }
1661        expr.push_str("true");
1662        let result = parse(&expr);
1663        assert!(result.is_err());
1664        let err = result.unwrap_err().to_string();
1665        assert!(err.contains("maximum nesting depth exceeded"), "got: {err}");
1666    }
1667
1668    #[test]
1669    fn test_builtin_sql_column_type() {
1670        let (sql, params, is_bool) = builtin_sql(
1671            "column_type",
1672            &[
1673                "users".to_string(),
1674                "age".to_string(),
1675                "integer".to_string(),
1676            ],
1677            "myschema",
1678        )
1679        .unwrap();
1680        assert!(is_bool);
1681        assert!(sql.contains("data_type = $4"));
1682        assert_eq!(params, vec!["myschema", "users", "age", "integer"]);
1683    }
1684
1685    #[test]
1686    fn test_builtin_sql_column_nullable() {
1687        let (sql, params, is_bool) = builtin_sql(
1688            "column_nullable",
1689            &["users".to_string(), "name".to_string()],
1690            "public",
1691        )
1692        .unwrap();
1693        assert!(is_bool);
1694        assert!(sql.contains("is_nullable = 'YES'"));
1695        assert_eq!(params, vec!["public", "users", "name"]);
1696    }
1697
1698    #[test]
1699    fn test_builtin_sql_index_exists() {
1700        let (sql, params, is_bool) =
1701            builtin_sql("index_exists", &["idx_users_email".to_string()], "public").unwrap();
1702        assert!(is_bool);
1703        assert!(sql.contains("pg_indexes"));
1704        assert!(sql.contains("$2"));
1705        assert_eq!(params, vec!["public", "idx_users_email"]);
1706    }
1707
1708    #[test]
1709    fn test_builtin_sql_constraint_exists() {
1710        let (sql, params, is_bool) = builtin_sql(
1711            "constraint_exists",
1712            &["users".to_string(), "users_pkey".to_string()],
1713            "public",
1714        )
1715        .unwrap();
1716        assert!(is_bool);
1717        assert!(sql.contains("table_constraints"));
1718        assert!(sql.contains("$3"));
1719        assert_eq!(params, vec!["public", "users", "users_pkey"]);
1720    }
1721
1722    #[test]
1723    fn test_builtin_sql_function_exists() {
1724        let (sql, params, is_bool) =
1725            builtin_sql("function_exists", &["my_func".to_string()], "public").unwrap();
1726        assert!(is_bool);
1727        assert!(sql.contains("pg_proc"));
1728        assert!(sql.contains("pg_namespace"));
1729        assert!(sql.contains("$2"));
1730        assert_eq!(params, vec!["public", "my_func"]);
1731    }
1732
1733    #[test]
1734    fn test_builtin_sql_enum_exists() {
1735        let (sql, params, is_bool) =
1736            builtin_sql("enum_exists", &["status_type".to_string()], "public").unwrap();
1737        assert!(is_bool);
1738        assert!(sql.contains("pg_type"));
1739        assert!(sql.contains("typtype = 'e'"));
1740        assert!(sql.contains("$2"));
1741        assert_eq!(params, vec!["public", "status_type"]);
1742    }
1743
1744    #[test]
1745    fn test_builtin_sql_custom_sql() {
1746        let (sql, params, is_bool) = builtin_sql(
1747            "sql",
1748            &["SELECT count(*) = 0 FROM old_table".to_string()],
1749            "public",
1750        )
1751        .unwrap();
1752        assert!(is_bool);
1753        assert_eq!(sql, "SELECT count(*) = 0 FROM old_table");
1754        assert!(params.is_empty());
1755    }
1756
1757    #[test]
1758    fn test_builtin_sql_params_order_table_exists() {
1759        let (sql, params, is_bool) =
1760            builtin_sql("table_exists", &["users".to_string()], "myschema").unwrap();
1761        assert!(is_bool);
1762        assert_eq!(params.len(), 2);
1763        assert_eq!(params[0], "myschema");
1764        assert_eq!(params[1], "users");
1765        assert!(sql.contains("$1"));
1766        assert!(sql.contains("$2"));
1767    }
1768
1769    #[test]
1770    fn test_builtin_sql_sql_function_empty_params() {
1771        let (sql, params, is_bool) =
1772            builtin_sql("sql", &["SELECT 1".to_string()], "public").unwrap();
1773        assert!(is_bool);
1774        assert!(params.is_empty());
1775        assert_eq!(sql, "SELECT 1");
1776    }
1777
1778    #[test]
1779    fn test_parse_empty_function_args() {
1780        // table_exists() with no args should be parsed OK but builtin_sql should reject it
1781        let expr = parse("table_exists()").unwrap();
1782        match expr {
1783            GuardExpr::FunctionCall { name, args } => {
1784                assert_eq!(name, "table_exists");
1785                assert!(args.is_empty());
1786            }
1787            other => panic!("Expected FunctionCall, got {other:?}"),
1788        }
1789    }
1790}