1use std::borrow::Cow;
52use std::collections::BTreeMap;
53use std::path::PathBuf;
54
55#[derive(Debug, Clone)]
57pub struct UnresolvedVariable {
58 pub name: String,
60 pub byte_offset: usize,
62 pub byte_len: usize,
64}
65
66#[derive(Debug, Clone)]
68pub(crate) struct Substitution {
69 pub original_start: usize,
71 pub original_len: usize,
73 pub resolved_len: usize,
75}
76
77#[derive(Debug)]
79pub struct VariableError {
80 pub unresolved: Vec<UnresolvedVariable>,
81 pub path: PathBuf,
82 pub profile_set: bool,
86}
87
88#[derive(Debug)]
90pub(crate) struct ResolvedSql<'a> {
91 pub sql: Cow<'a, str>,
93 pub unresolved: Vec<UnresolvedVariable>,
95 pub substitutions: Vec<Substitution>,
97 pub has_warn_pragma: bool,
99}
100
101#[allow(clippy::as_conversions)]
108pub(crate) fn resolved_to_original(offset: usize, substitutions: &[Substitution]) -> usize {
109 let mut delta: isize = 0; for sub in substitutions {
112 let resolved_start = (sub.original_start as isize - delta) as usize;
114
115 if offset < resolved_start {
116 return (offset as isize + delta) as usize;
118 }
119
120 let resolved_end = resolved_start + sub.resolved_len;
121 if offset < resolved_end {
122 return sub.original_start;
124 }
125
126 delta += sub.original_len as isize - sub.resolved_len as isize;
128 }
129
130 (offset as isize + delta) as usize
132}
133
134const PRAGMA: &str = "PRAGMA WARN_ON_MISSING_VARIABLES;";
135
136fn detect_warn_pragma(sql: &str) -> bool {
139 let trimmed = sql.trim_start();
140
141 if let Some(rest) = trimmed.strip_prefix("--") {
142 let line = match rest.find('\n') {
144 Some(pos) => &rest[..pos],
145 None => rest,
146 };
147 line.contains(PRAGMA)
148 } else if let Some(rest) = trimmed.strip_prefix("/*") {
149 match rest.find("*/") {
151 Some(pos) => rest[..pos].contains(PRAGMA),
152 None => rest.contains(PRAGMA),
153 }
154 } else {
155 false
156 }
157}
158
159enum VarKind {
161 Raw,
163 SqlLiteral,
165 SqlIdentifier,
167}
168
169fn starts_with(bytes: &[u8], i: usize, needle: &[u8]) -> bool {
171 bytes[i..].starts_with(needle)
172}
173
174fn push_sql_escaped(out: &mut String, value: &str, quote: char) {
176 for ch in value.chars() {
177 if ch == quote {
178 out.push(ch);
179 }
180 out.push(ch);
181 }
182}
183
184fn try_read_variable<'a>(
188 sql: &'a str,
189 bytes: &[u8],
190 i: usize,
191) -> Option<(&'a str, VarKind, usize)> {
192 let len = bytes.len();
193 if i + 1 >= len {
194 return None;
195 }
196
197 if bytes[i + 1] == b':' {
199 return None;
200 }
201
202 if bytes[i + 1] == b'\'' {
204 let name_start = i + 2;
205 let mut j = name_start;
206 while j < len && bytes[j] != b'\'' {
207 j += 1;
208 }
209 if j >= len {
210 return None; }
212 let name = &sql[name_start..j];
213 return Some((name, VarKind::SqlLiteral, j + 1));
214 }
215
216 if bytes[i + 1] == b'"' {
218 let name_start = i + 2;
219 let mut j = name_start;
220 while j < len && bytes[j] != b'"' {
221 j += 1;
222 }
223 if j >= len {
224 return None; }
226 let name = &sql[name_start..j];
227 return Some((name, VarKind::SqlIdentifier, j + 1));
228 }
229
230 if bytes[i + 1].is_ascii_alphabetic() || bytes[i + 1] == b'_' {
232 let name_start = i + 1;
233 let mut j = name_start;
234 while j < len && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
235 j += 1;
236 }
237 let name = &sql[name_start..j];
238 return Some((name, VarKind::Raw, j));
239 }
240
241 None
242}
243
244fn consume_single_quoted(bytes: &[u8], mut i: usize, len: usize) -> usize {
247 while i < len {
248 if bytes[i] == b'\'' {
249 if i + 1 < len && bytes[i + 1] == b'\'' {
250 i += 2; } else {
252 return i + 1; }
254 } else {
255 i += 1;
256 }
257 }
258 i
259}
260
261fn consume_double_quoted(bytes: &[u8], mut i: usize, len: usize) -> usize {
264 while i < len {
265 if bytes[i] == b'"' {
266 return i + 1;
267 }
268 i += 1;
269 }
270 i
271}
272
273fn consume_line_comment(bytes: &[u8], mut i: usize, len: usize) -> usize {
276 while i < len {
277 if bytes[i] == b'\n' {
278 return i + 1;
279 }
280 i += 1;
281 }
282 i
283}
284
285fn consume_block_comment(bytes: &[u8], mut i: usize, len: usize) -> usize {
288 let mut depth: u32 = 1;
289 while i < len && depth > 0 {
290 if starts_with(bytes, i, b"/*") {
291 depth += 1;
292 i += 2;
293 } else if starts_with(bytes, i, b"*/") {
294 depth -= 1;
295 i += 2;
296 } else {
297 i += 1;
298 }
299 }
300 i
301}
302
303fn try_dollar_tag<'a>(bytes: &'a [u8], i: usize, len: usize) -> Option<(usize, &'a [u8])> {
306 if bytes[i] != b'$' {
308 return None;
309 }
310
311 if i + 1 < len && bytes[i + 1] == b'$' {
313 return Some((i + 2, &bytes[i..i + 2]));
314 }
315
316 let tag_start = i + 1;
318 if tag_start < len && (bytes[tag_start].is_ascii_alphabetic() || bytes[tag_start] == b'_') {
319 let mut j = tag_start + 1;
320 while j < len && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
321 j += 1;
322 }
323 if j < len && bytes[j] == b'$' {
324 return Some((j + 1, &bytes[i..j + 1]));
325 }
326 }
327
328 None
329}
330
331fn consume_dollar_quoted(bytes: &[u8], mut i: usize, len: usize, tag: &[u8]) -> usize {
334 while i < len {
335 if bytes[i] == b'$' && bytes[i..].starts_with(tag) {
336 return i + tag.len();
337 }
338 i += 1;
339 }
340 i
341}
342
343pub(crate) fn find_variable_at_position(
354 sql: &str,
355 offset: usize,
356) -> Option<(String, usize, usize)> {
357 let bytes = sql.as_bytes();
358 let len = bytes.len();
359 let mut i = 0;
360
361 while i < len {
362 if bytes[i] == b'\'' {
363 i = consume_single_quoted(bytes, i + 1, len);
364 } else if bytes[i] == b'"' {
365 i = consume_double_quoted(bytes, i + 1, len);
366 } else if starts_with(bytes, i, b"--") {
367 i = consume_line_comment(bytes, i + 2, len);
368 } else if starts_with(bytes, i, b"/*") {
369 i = consume_block_comment(bytes, i + 2, len);
370 } else if bytes[i] == b'$' {
371 if let Some((end, tag)) = try_dollar_tag(bytes, i, len) {
372 i = consume_dollar_quoted(bytes, end, len, tag);
373 } else {
374 i += 1;
375 }
376 } else if starts_with(bytes, i, b"::") {
377 i += 2;
378 } else if bytes[i] == b':' {
379 if let Some((name, _kind, end)) = try_read_variable(sql, bytes, i) {
380 let var_start = i;
381 let var_len = end - var_start;
382 if offset >= var_start && offset < end {
383 return Some((name.to_string(), var_start, var_len));
384 }
385 i = end;
386 } else {
387 i += 1;
388 }
389 } else {
390 i += 1;
391 }
392 }
393
394 None
395}
396
397pub(crate) fn resolve_variables<'a>(
403 sql: &'a str,
404 vars: &BTreeMap<String, String>,
405) -> ResolvedSql<'a> {
406 let bytes = sql.as_bytes();
407 let len = bytes.len();
408
409 if len == 0 {
410 return ResolvedSql {
411 sql: Cow::Borrowed(sql),
412 unresolved: Vec::new(),
413 substitutions: Vec::new(),
414 has_warn_pragma: false,
415 };
416 }
417
418 let has_warn_pragma = detect_warn_pragma(sql);
419
420 let mut i = 0;
421 let mut output: Option<String> = None;
422 let mut copy_from: usize = 0;
423 let mut unresolved: Vec<UnresolvedVariable> = Vec::new();
424 let mut substitutions: Vec<Substitution> = Vec::new();
425
426 while i < len {
427 if bytes[i] == b'\'' {
428 i = consume_single_quoted(bytes, i + 1, len);
429 } else if bytes[i] == b'"' {
430 i = consume_double_quoted(bytes, i + 1, len);
431 } else if starts_with(bytes, i, b"--") {
432 i = consume_line_comment(bytes, i + 2, len);
433 } else if starts_with(bytes, i, b"/*") {
434 i = consume_block_comment(bytes, i + 2, len);
435 } else if bytes[i] == b'$' {
436 if let Some((end, tag)) = try_dollar_tag(bytes, i, len) {
437 i = consume_dollar_quoted(bytes, end, len, tag);
438 } else {
439 i += 1;
440 }
441 } else if starts_with(bytes, i, b"::") {
442 i += 2;
444 } else if bytes[i] == b':' {
445 if let Some((name, kind, end)) = try_read_variable(sql, bytes, i) {
446 let var_start = i;
448 let buf = match output {
449 Some(ref mut buf) => {
450 buf.push_str(&sql[copy_from..var_start]);
451 buf
452 }
453 None => {
454 let mut buf = String::with_capacity(sql.len());
455 buf.push_str(&sql[copy_from..var_start]);
456 output = Some(buf);
457 output.as_mut().unwrap()
458 }
459 };
460
461 if let Some(value) = vars.get(name) {
462 let before = buf.len();
463 match kind {
464 VarKind::Raw => buf.push_str(value),
465 VarKind::SqlLiteral => {
466 buf.push('\'');
467 push_sql_escaped(buf, value, '\'');
468 buf.push('\'');
469 }
470 VarKind::SqlIdentifier => {
471 buf.push('"');
472 push_sql_escaped(buf, value, '"');
473 buf.push('"');
474 }
475 }
476 let after = buf.len();
477 substitutions.push(Substitution {
478 original_start: var_start,
479 original_len: end - var_start,
480 resolved_len: after - before,
481 });
482 } else {
483 unresolved.push(UnresolvedVariable {
484 name: name.to_string(),
485 byte_offset: var_start,
486 byte_len: end - var_start,
487 });
488 buf.push_str(&sql[var_start..end]);
489 }
490
491 copy_from = end;
492 i = end;
493 } else {
494 i += 1;
495 }
496 } else {
497 i += 1;
498 }
499 }
500
501 let sql = match output {
502 Some(mut buf) => {
503 buf.push_str(&sql[copy_from..]);
504 Cow::Owned(buf)
505 }
506 None => Cow::Borrowed(sql),
507 };
508
509 ResolvedSql {
510 sql,
511 unresolved,
512 substitutions,
513 has_warn_pragma,
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 fn vars(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
522 pairs
523 .iter()
524 .map(|(k, v)| (k.to_string(), v.to_string()))
525 .collect()
526 }
527
528 fn unresolved_names<'a>(result: &'a ResolvedSql<'a>) -> Vec<&'a str> {
530 result.unresolved.iter().map(|v| v.name.as_str()).collect()
531 }
532
533 #[mz_ore::test]
534 fn no_variables_returns_borrowed() {
535 let sql = "SELECT 1 FROM t WHERE x = 'hello'";
536 let result = resolve_variables(sql, &BTreeMap::new());
537 assert!(matches!(result.sql, Cow::Borrowed(_)));
538 assert_eq!(result.sql.as_ref(), sql);
539 assert!(result.unresolved.is_empty());
540 }
541
542 #[mz_ore::test]
543 fn bare_variable_substitution() {
544 let v = vars(&[("cluster", "analytics")]);
545 let result = resolve_variables(
546 "CREATE MATERIALIZED VIEW mv IN CLUSTER :cluster AS SELECT 1",
547 &v,
548 );
549 assert_eq!(
550 result.sql.as_ref(),
551 "CREATE MATERIALIZED VIEW mv IN CLUSTER analytics AS SELECT 1"
552 );
553 assert!(result.unresolved.is_empty());
554 }
555
556 #[mz_ore::test]
557 fn single_quoted_variable_with_escaping() {
558 let v = vars(&[("pg_host", "it's-a-host")]);
559 let result = resolve_variables("CREATE CONNECTION pg TO POSTGRES (HOST :'pg_host')", &v);
560 assert_eq!(
561 result.sql.as_ref(),
562 "CREATE CONNECTION pg TO POSTGRES (HOST 'it''s-a-host')"
563 );
564 assert!(result.unresolved.is_empty());
565 }
566
567 #[mz_ore::test]
568 fn double_quoted_variable_with_escaping() {
569 let v = vars(&[("col", "my\"col")]);
570 let result = resolve_variables("SELECT :\"col\" FROM t", &v);
571 assert_eq!(result.sql.as_ref(), "SELECT \"my\"\"col\" FROM t");
572 assert!(result.unresolved.is_empty());
573 }
574
575 #[mz_ore::test]
576 fn type_cast_preserved() {
577 let result = resolve_variables("SELECT x::int FROM t", &BTreeMap::new());
578 assert!(matches!(result.sql, Cow::Borrowed(_)));
579 assert_eq!(result.sql.as_ref(), "SELECT x::int FROM t");
580 assert!(result.unresolved.is_empty());
581 }
582
583 #[mz_ore::test]
584 fn variable_inside_string_literal_not_resolved() {
585 let v = vars(&[("foo", "bar")]);
586 let result = resolve_variables("SELECT ':foo' FROM t", &v);
587 assert!(matches!(result.sql, Cow::Borrowed(_)));
588 assert_eq!(result.sql.as_ref(), "SELECT ':foo' FROM t");
589 }
590
591 #[mz_ore::test]
592 fn variable_in_line_comment_not_resolved() {
593 let v = vars(&[("foo", "bar")]);
594 let result = resolve_variables("-- :foo\nSELECT 1", &v);
595 assert!(matches!(result.sql, Cow::Borrowed(_)));
596 assert_eq!(result.sql.as_ref(), "-- :foo\nSELECT 1");
597 }
598
599 #[mz_ore::test]
600 fn variable_in_block_comment_not_resolved() {
601 let v = vars(&[("foo", "bar")]);
602 let result = resolve_variables("/* :foo */ SELECT 1", &v);
603 assert!(matches!(result.sql, Cow::Borrowed(_)));
604 assert_eq!(result.sql.as_ref(), "/* :foo */ SELECT 1");
605 }
606
607 #[mz_ore::test]
608 fn nested_block_comment_not_resolved() {
609 let v = vars(&[("foo", "bar")]);
610 let result = resolve_variables("/* /* :foo */ */ SELECT 1", &v);
611 assert!(matches!(result.sql, Cow::Borrowed(_)));
612 assert_eq!(result.sql.as_ref(), "/* /* :foo */ */ SELECT 1");
613 }
614
615 #[mz_ore::test]
616 fn multiple_variables() {
617 let v = vars(&[("a", "1"), ("b", "2")]);
618 let result = resolve_variables("SELECT :a, :b", &v);
619 assert_eq!(result.sql.as_ref(), "SELECT 1, 2");
620 assert!(result.unresolved.is_empty());
621 }
622
623 #[mz_ore::test]
624 fn unresolved_variable_reported() {
625 let result = resolve_variables("SELECT :missing", &BTreeMap::new());
626 assert_eq!(unresolved_names(&result), vec!["missing"]);
627 assert_eq!(result.sql.as_ref(), "SELECT :missing");
628 }
629
630 #[mz_ore::test]
631 fn multiple_unresolved_lists_all() {
632 let result = resolve_variables("SELECT :a, :b, :a", &BTreeMap::new());
633 assert_eq!(unresolved_names(&result), vec!["a", "b", "a"]);
634 }
635
636 #[mz_ore::test]
637 fn empty_vars_no_syntax_borrowed() {
638 let result = resolve_variables("SELECT 1", &BTreeMap::new());
639 assert!(matches!(result.sql, Cow::Borrowed(_)));
640 }
641
642 #[mz_ore::test]
643 fn empty_vars_with_syntax_reports_unresolved() {
644 let result = resolve_variables("SELECT :foo", &BTreeMap::new());
645 assert!(!result.unresolved.is_empty());
646 }
647
648 #[mz_ore::test]
649 fn variable_at_end_of_input() {
650 let v = vars(&[("foo", "bar")]);
651 let result = resolve_variables("SELECT :foo", &v);
652 assert_eq!(result.sql.as_ref(), "SELECT bar");
653 }
654
655 #[mz_ore::test]
656 fn adjacent_syntax() {
657 let v = vars(&[("foo", "1"), ("bar", "2")]);
658 let result = resolve_variables("(:foo, :bar)", &v);
659 assert_eq!(result.sql.as_ref(), "(1, 2)");
660 }
661
662 #[mz_ore::test]
663 fn single_quoted_no_escaping_needed() {
664 let v = vars(&[("host", "simple-host")]);
665 let result = resolve_variables("HOST :'host'", &v);
666 assert_eq!(result.sql.as_ref(), "HOST 'simple-host'");
667 }
668
669 #[mz_ore::test]
670 fn double_quoted_no_escaping_needed() {
671 let v = vars(&[("col", "simple_col")]);
672 let result = resolve_variables("SELECT :\"col\"", &v);
673 assert_eq!(result.sql.as_ref(), "SELECT \"simple_col\"");
674 }
675
676 #[mz_ore::test]
677 fn double_quoted_identifier_not_resolved() {
678 let v = vars(&[("id", "bar")]);
679 let result = resolve_variables("SELECT \"user:id\" FROM t", &v);
680 assert!(matches!(result.sql, Cow::Borrowed(_)));
681 assert_eq!(result.sql.as_ref(), "SELECT \"user:id\" FROM t");
682 }
683
684 #[mz_ore::test]
685 fn dollar_quoted_not_resolved() {
686 let v = vars(&[("foo", "bar")]);
687 let result = resolve_variables("SELECT $$:foo$$ FROM t", &v);
688 assert!(matches!(result.sql, Cow::Borrowed(_)));
689 assert_eq!(result.sql.as_ref(), "SELECT $$:foo$$ FROM t");
690 }
691
692 #[mz_ore::test]
693 fn dollar_tagged_not_resolved() {
694 let v = vars(&[("foo", "bar")]);
695 let result = resolve_variables("SELECT $tag$:foo$tag$ FROM t", &v);
696 assert!(matches!(result.sql, Cow::Borrowed(_)));
697 assert_eq!(result.sql.as_ref(), "SELECT $tag$:foo$tag$ FROM t");
698 }
699
700 #[mz_ore::test]
701 fn dollar_sign_alone_no_crash() {
702 let result = resolve_variables("SELECT $ FROM t", &BTreeMap::new());
703 assert!(matches!(result.sql, Cow::Borrowed(_)));
704 assert_eq!(result.sql.as_ref(), "SELECT $ FROM t");
705 }
706
707 #[mz_ore::test]
710 fn pragma_line_comment() {
711 let result = resolve_variables(
712 "-- PRAGMA WARN_ON_MISSING_VARIABLES;\nSELECT :foo",
713 &BTreeMap::new(),
714 );
715 assert!(result.has_warn_pragma);
716 assert_eq!(unresolved_names(&result), vec!["foo"]);
717 }
718
719 #[mz_ore::test]
720 fn pragma_block_comment() {
721 let result = resolve_variables(
722 "/* PRAGMA WARN_ON_MISSING_VARIABLES; */\nSELECT :foo",
723 &BTreeMap::new(),
724 );
725 assert!(result.has_warn_pragma);
726 assert_eq!(unresolved_names(&result), vec!["foo"]);
727 }
728
729 #[mz_ore::test]
730 fn pragma_with_leading_whitespace() {
731 let result = resolve_variables(
732 " \t\n -- PRAGMA WARN_ON_MISSING_VARIABLES;\nSELECT :foo",
733 &BTreeMap::new(),
734 );
735 assert!(result.has_warn_pragma);
736 }
737
738 #[mz_ore::test]
739 fn pragma_not_on_first_comment() {
740 let result = resolve_variables(
741 "SELECT 1;\n-- PRAGMA WARN_ON_MISSING_VARIABLES;\nSELECT :foo",
742 &BTreeMap::new(),
743 );
744 assert!(!result.has_warn_pragma);
745 }
746
747 #[mz_ore::test]
748 fn pragma_missing() {
749 let result = resolve_variables("SELECT :foo", &BTreeMap::new());
750 assert!(!result.has_warn_pragma);
751 }
752
753 #[mz_ore::test]
754 fn pragma_partial_match() {
755 let result = resolve_variables("-- PRAGMA WARN_ON_MISSING\nSELECT :foo", &BTreeMap::new());
756 assert!(!result.has_warn_pragma);
757 }
758
759 #[mz_ore::test]
762 fn unresolved_variable_has_correct_offset() {
763 let result = resolve_variables("SELECT :missing", &BTreeMap::new());
765 assert_eq!(result.unresolved.len(), 1);
766 assert_eq!(result.unresolved[0].name, "missing");
767 assert_eq!(result.unresolved[0].byte_offset, 7);
768 assert_eq!(result.unresolved[0].byte_len, 8); }
770
771 #[mz_ore::test]
772 fn unresolved_quoted_variable_has_correct_len() {
773 let result = resolve_variables("SELECT :'missing'", &BTreeMap::new());
775 assert_eq!(result.unresolved.len(), 1);
776 assert_eq!(result.unresolved[0].byte_offset, 7);
777 assert_eq!(result.unresolved[0].byte_len, 10);
778 }
779
780 #[mz_ore::test]
781 fn multiple_unresolved_tracks_each_occurrence() {
782 let result = resolve_variables("SELECT :a, :b, :a", &BTreeMap::new());
783 assert_eq!(result.unresolved.len(), 3);
784 assert_eq!(result.unresolved[0].name, "a");
785 assert_eq!(result.unresolved[0].byte_offset, 7);
786 assert_eq!(result.unresolved[1].name, "b");
787 assert_eq!(result.unresolved[1].byte_offset, 11);
788 assert_eq!(result.unresolved[2].name, "a");
789 assert_eq!(result.unresolved[2].byte_offset, 15);
790 }
791
792 #[mz_ore::test]
795 fn find_var_bare_variable() {
796 let sql = "SELECT :foo FROM t";
798 let result = find_variable_at_position(sql, 7);
799 assert_eq!(result, Some(("foo".to_string(), 7, 4)));
800 assert_eq!(
802 find_variable_at_position(sql, 9),
803 Some(("foo".to_string(), 7, 4))
804 );
805 }
806
807 #[mz_ore::test]
808 fn find_var_single_quoted() {
809 let sql = "HOST :'host'";
811 let result = find_variable_at_position(sql, 5);
812 assert_eq!(result, Some(("host".to_string(), 5, 7)));
813 assert_eq!(
814 find_variable_at_position(sql, 11),
815 Some(("host".to_string(), 5, 7))
816 );
817 }
818
819 #[mz_ore::test]
820 fn find_var_double_quoted() {
821 let sql = "SELECT :\"col\"";
823 let result = find_variable_at_position(sql, 7);
824 assert_eq!(result, Some(("col".to_string(), 7, 6)));
825 }
826
827 #[mz_ore::test]
828 fn find_var_between_variables_returns_none() {
829 let sql = "SELECT :a, :b";
831 assert_eq!(find_variable_at_position(sql, 10), None);
832 }
833
834 #[mz_ore::test]
835 fn find_var_in_string_literal_returns_none() {
836 let sql = "SELECT ':foo' FROM t";
837 assert_eq!(find_variable_at_position(sql, 8), None);
839 }
840
841 #[mz_ore::test]
842 fn find_var_type_cast_returns_none() {
843 let sql = "SELECT x::int FROM t";
844 assert_eq!(find_variable_at_position(sql, 9), None);
846 assert_eq!(find_variable_at_position(sql, 8), None);
847 }
848
849 #[mz_ore::test]
850 fn find_var_in_comment_returns_none() {
851 let sql = "-- :foo\nSELECT 1";
852 assert_eq!(find_variable_at_position(sql, 3), None);
853 }
854
855 #[mz_ore::test]
856 fn find_var_offset_past_end_returns_none() {
857 let sql = "SELECT :foo";
858 assert_eq!(find_variable_at_position(sql, 11), None);
859 }
860
861 #[mz_ore::test]
864 fn resolved_variable_records_substitution() {
865 let v = vars(&[("cluster", "analytics")]);
866 let result = resolve_variables("IN CLUSTER :cluster AS", &v);
867 assert_eq!(result.substitutions.len(), 1);
868 assert_eq!(result.substitutions[0].original_start, 11); assert_eq!(result.substitutions[0].original_len, 8); assert_eq!(result.substitutions[0].resolved_len, 9); }
872
873 #[mz_ore::test]
876 fn resolved_to_original_no_substitutions() {
877 assert_eq!(resolved_to_original(5, &[]), 5);
878 assert_eq!(resolved_to_original(0, &[]), 0);
879 }
880
881 #[mz_ore::test]
882 fn resolved_to_original_shorter_replacement() {
883 let subs = vec![Substitution {
888 original_start: 11,
889 original_len: 8,
890 resolved_len: 2,
891 }];
892 assert_eq!(resolved_to_original(5, &subs), 5);
894 assert_eq!(resolved_to_original(11, &subs), 11);
896 assert_eq!(resolved_to_original(12, &subs), 11);
897 assert_eq!(resolved_to_original(13, &subs), 19);
899 }
900
901 #[mz_ore::test]
902 fn resolved_to_original_longer_replacement() {
903 let subs = vec![Substitution {
906 original_start: 2,
907 original_len: 2,
908 resolved_len: 9,
909 }];
910 assert_eq!(resolved_to_original(0, &subs), 0);
912 assert_eq!(resolved_to_original(5, &subs), 2);
914 assert_eq!(resolved_to_original(11, &subs), 4);
916 }
917
918 #[mz_ore::test]
919 fn resolved_to_original_multiple_substitutions() {
920 let subs = vec![
923 Substitution {
924 original_start: 2,
925 original_len: 2,
926 resolved_len: 4,
927 },
928 Substitution {
929 original_start: 7,
930 original_len: 2,
931 resolved_len: 4,
932 },
933 ];
934 assert_eq!(resolved_to_original(0, &subs), 0);
936 assert_eq!(resolved_to_original(3, &subs), 2);
938 assert_eq!(resolved_to_original(6, &subs), 4);
940 assert_eq!(resolved_to_original(10, &subs), 7);
944 assert_eq!(resolved_to_original(13, &subs), 9);
946 }
947}