1use crate::db::DbClient;
15use crate::dialect::DialectKind;
16use crate::error::{Result, WaypointError};
17
18const MAX_PARSE_DEPTH: usize = 50;
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub enum OnRequireFail {
28 #[default]
30 Error,
31 Warn,
33 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#[derive(Debug, Clone)]
52pub struct GuardsConfig {
53 pub enabled: bool,
55 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#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum ComparisonOp {
75 Lt,
77 Gt,
79 Le,
81 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#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum GuardExpr {
99 FunctionCall {
101 name: String,
103 args: Vec<GuardExpr>,
105 },
106 And(Box<GuardExpr>, Box<GuardExpr>),
108 Or(Box<GuardExpr>, Box<GuardExpr>),
110 Not(Box<GuardExpr>),
112 Comparison {
114 left: Box<GuardExpr>,
116 op: ComparisonOp,
118 right: Box<GuardExpr>,
120 },
121 StringLiteral(String),
123 NumberLiteral(i64),
125 BoolLiteral(bool),
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum GuardValue {
132 Bool(bool),
134 Number(i64),
136 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#[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
192fn 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 if ch.is_ascii_whitespace() {
204 i += 1;
205 continue;
206 }
207
208 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; } 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; continue;
228 }
229
230 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 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 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 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
316struct 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 fn parse_expr(&mut self, depth: usize) -> Result<GuardExpr> {
361 self.parse_or_expr(depth)
362 }
363
364 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(); 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 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(); 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 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(); 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 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(); 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 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 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 if self.pos + 1 < self.tokens.len() && self.tokens[self.pos + 1] == Token::LParen {
463 self.advance(); self.advance(); 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(); 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 fn parse_args(&mut self, depth: usize) -> Result<Vec<GuardExpr>> {
499 let mut args = Vec::new();
500
501 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(); args.push(self.parse_expr(depth)?);
511 }
512
513 Ok(args)
514 }
515}
516
517pub 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 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#[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#[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 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 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
822fn 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
833fn 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#[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#[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, ¶ms).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
995pub 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
1100async 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, ¶ms)
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 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 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#[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(_, _) => {} 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 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 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 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 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(_, _) => {} 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 match expr {
1511 GuardExpr::And(left, right) => {
1512 match *left {
1513 GuardExpr::And(_, _) => {} 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(_, _) => {} 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 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 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}