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
971 .query_opt(&sql, ¶ms)
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
990pub 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
1095async 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 let row = pg
1117 .query_opt(&sql, ¶ms)
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 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 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
1179fn 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#[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(_, _) => {} 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 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 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 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 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(_, _) => {} 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 match expr {
1535 GuardExpr::And(left, right) => {
1536 match *left {
1537 GuardExpr::And(_, _) => {} 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(_, _) => {} 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 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 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}