Skip to main content

mz_testdrive/action/
sql.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::ascii;
11use std::error::Error;
12use std::fmt::{self, Display, Formatter, Write as _};
13use std::time::SystemTime;
14
15use anyhow::{Context, bail};
16use itertools::Itertools;
17use md5::{Digest, Md5};
18use mz_ore::collections::CollectionExt;
19use mz_ore::retry::Retry;
20use mz_ore::str::StrExt;
21use mz_pgrepr::{Interval, Jsonb, Numeric, UInt2, UInt4, UInt8};
22use mz_postgres_util::query_prepared;
23use mz_repr::adt::range::Range;
24use mz_sql_parser::ast::{Raw, Statement};
25use postgres_array::Array;
26use regex::Regex;
27use tokio_postgres::error::DbError;
28use tokio_postgres::row::Row;
29use tokio_postgres::types::{FromSql, Type};
30
31use crate::action::{ControlFlow, Rewrite, State};
32use crate::parser::{FailSqlCommand, SqlCommand, SqlExpectedError, SqlOutput};
33
34pub async fn run_sql(mut cmd: SqlCommand, state: &mut State) -> Result<ControlFlow, anyhow::Error> {
35    use Statement::*;
36
37    state.rewrite_pos_start = cmd.expected_start;
38    state.rewrite_pos_end = cmd.expected_end;
39
40    let stmts = mz_sql_parser::parser::parse_statements(&cmd.query)
41        .with_context(|| format!("unable to parse SQL: {}", cmd.query))?;
42    if stmts.len() != 1 {
43        bail!("expected one statement, but got {}", stmts.len());
44    }
45    let stmt = stmts.into_element().ast;
46    if let SqlOutput::Full { expected_rows, .. } = &mut cmd.expected_output {
47        // TODO(benesch): one day we'll support SQL queries where order matters.
48        expected_rows.sort();
49    }
50
51    let should_retry = match &stmt {
52        // Do not retry FETCH statements as subsequent executions are likely
53        // to return an empty result. The original result would thus be lost.
54        Fetch(_) => false,
55        // EXPLAIN ... PLAN statements should always provide the expected result
56        // on the first try
57        ExplainPlan(_) => false,
58        // DDL statements should always provide the expected result on the first try
59        CreateConnection(_)
60        | CreateCluster(_)
61        | CreateClusterReplica(_)
62        | CreateDatabase(_)
63        | CreateSchema(_)
64        | CreateSource(_)
65        | CreateWebhookSource(_)
66        | CreateSink(_)
67        | CreateMaterializedView(_)
68        | CreateView(_)
69        | CreateTable(_)
70        | CreateTableFromSource(_)
71        | CreateIndex(_)
72        | CreateType(_)
73        | CreateRole(_)
74        | AlterObjectRename(_)
75        | AlterIndex(_)
76        | AlterSink(_)
77        | Discard(_)
78        | DropObjects(_)
79        | SetVariable(_) => false,
80        _ => true,
81    };
82
83    let query = &cmd.query;
84    print_query(query, Some(&stmt));
85    let expected_output = &cmd.expected_output;
86    let raw_output = cmd.raw_output;
87    state.error_line_count = 0;
88    state.error_string = "".to_string();
89    let (state, res) = match should_retry {
90        true => Retry::default()
91            .initial_backoff(state.initial_backoff)
92            .factor(state.backoff_factor)
93            .max_duration(state.timeout)
94            .max_tries(state.max_tries),
95        false => Retry::default().max_duration(state.timeout).max_tries(1),
96    }
97    .retry_async_with_state(state, |retry_state, state| async move {
98        // `next_backoff` is `None` exactly on the final attempt, whether the
99        // retry budget is bounded by `max_tries` or by `max_duration`. Result
100        // rewriting must only happen on the final attempt, so key off that
101        // rather than `max_tries` (which is usize::MAX by default and would
102        // never mark an attempt as final).
103        let should_continue = retry_state.next_backoff.is_some() && should_retry;
104        let start = SystemTime::now();
105        match try_run_sql(state, query, expected_output, raw_output, should_continue).await {
106            Ok(()) => {
107                let now = SystemTime::now();
108                let epoch = SystemTime::UNIX_EPOCH;
109                let ts = now.duration_since(epoch).unwrap().as_secs_f64();
110                let delay = now.duration_since(start).unwrap().as_secs_f64();
111                println!("rows match; continuing at ts {ts}, took {delay}s");
112                (state, Ok(()))
113            }
114            Err(e) => {
115                if let Some(backoff) = retry_state.next_backoff {
116                    if !backoff.is_zero()
117                        && (retry_state.i == 0 || !mz_ore::env::is_var_truthy("CI"))
118                    {
119                        let error_string = format!("{:?}", e);
120                        if error_string != state.error_string {
121                            // Remove old status lines so as not to spam the output
122                            for _ in 0..state.error_line_count {
123                                print!("\x1B[1A\x1B[2K");
124                            }
125                            state.error_line_count = 0;
126                            state.error_string = error_string;
127                            state.error_line_count = state.error_string.lines().count() + 1;
128                            if state.error_string.ends_with('\n') {
129                                print!("{}", state.error_string);
130                            } else {
131                                println!("{}", state.error_string);
132                                state.error_line_count += 1;
133                            }
134                            println!(
135                                "rows didn't match; sleeping to see if dataflow catches up 🕑 {:.0?}",
136                                retry_state.next_backoff.unwrap_or_default()
137                            );
138                        }
139                    }
140                } else {
141                    for _ in 0..state.error_line_count {
142                        print!("\x1B[1A\x1B[2K");
143                    }
144                    state.error_line_count = 0;
145                }
146                (state, Err(e))
147            }
148        }
149    })
150    .await;
151    if let Err(e) = res {
152        return Err(e);
153    }
154    if state.consistency_checks == super::consistency::Level::Statement {
155        super::consistency::run_consistency_checks(state).await?;
156    }
157
158    Ok(ControlFlow::Continue)
159}
160
161/// Quote and escape `value` using only the escape sequences that
162/// `parser::split_line` recognises (`\\`, `\"`, `\n`, `\t`, `\r`, `\0`).
163///
164/// Avoids `format!("{:?}", …)` because Rust's Debug impl also emits `\u{N}`
165/// for non-printable Unicode, which the parser does not understand and would
166/// silently corrupt on the next read.
167fn escape_for_parser(value: &str) -> String {
168    let mut out = String::with_capacity(value.len() + 2);
169    out.push('"');
170    for c in value.chars() {
171        match c {
172            '\\' => out.push_str("\\\\"),
173            '"' => out.push_str("\\\""),
174            '\n' => out.push_str("\\n"),
175            '\t' => out.push_str("\\t"),
176            '\r' => out.push_str("\\r"),
177            '\0' => out.push_str("\\0"),
178            c => out.push(c),
179        }
180    }
181    out.push('"');
182    out
183}
184
185fn rewrite_result(
186    state: &mut State,
187    columns: Vec<&str>,
188    content: Vec<Vec<String>>,
189    raw_output: bool,
190) -> Result<(), anyhow::Error> {
191    // A line starting with a sigil would terminate the expected block and be
192    // parsed as a new command (e.g. a `?column?` header would read as a `?`
193    // command). The parser strips one leading backslash, so escape with that.
194    fn escape_line_start(line: String) -> String {
195        match line.chars().next() {
196            Some('$' | '>' | '!' | '?' | '#' | '\\') => format!("\\{line}"),
197            _ => line,
198        }
199    }
200
201    let mut buf = String::new();
202    if raw_output {
203        // `?` commands expect the raw result text with no column header and no
204        // escaping, so emit the values verbatim.
205        for row in content {
206            for value in row {
207                buf.push_str(&value);
208                if !value.ends_with('\n') {
209                    buf.push('\n');
210                }
211            }
212        }
213    } else {
214        writeln!(buf, "{}", escape_line_start(columns.join(" ")))?;
215        writeln!(buf, "----")?;
216        for row in content {
217            let mut formatted_row = Vec::<String>::new();
218            for value in row {
219                if value.is_empty()
220                    || value.contains(|x: char| char::is_ascii_whitespace(&x))
221                    || value.contains('"')
222                    || value.contains('\\')
223                {
224                    formatted_row.push(escape_for_parser(&value));
225                } else {
226                    formatted_row.push(value);
227                }
228            }
229            writeln!(buf, "{}", escape_line_start(formatted_row.join(" ")))?;
230        }
231    }
232    state.rewrites.push(Rewrite {
233        content: buf,
234        start: state.rewrite_pos_start,
235        end: state.rewrite_pos_end,
236    });
237
238    Ok(())
239}
240
241async fn try_run_sql(
242    state: &mut State,
243    query: &str,
244    expected_output: &SqlOutput,
245    raw_output: bool,
246    should_retry: bool,
247) -> Result<(), anyhow::Error> {
248    let stmt = state
249        .materialize
250        .pgclient
251        .prepare(query)
252        .await
253        .context("preparing query failed")?;
254
255    let query_with_timeout = tokio::time::timeout(
256        state.timeout.clone(),
257        query_prepared(&state.materialize.pgclient, &stmt, &[]),
258    )
259    .await;
260
261    if query_with_timeout.is_err() {
262        bail!("query timed out\n")
263    }
264
265    let rows: Vec<_> = query_with_timeout
266        .unwrap()
267        .context("executing query failed")?
268        .into_iter()
269        .map(|row| decode_row(state, row))
270        .collect::<Result<_, _>>()?;
271
272    let (mut actual, raw_actual): (Vec<_>, Vec<_>) = rows.into_iter().unzip();
273
274    let raw_actual: Option<Vec<_>> = if raw_actual.iter().any(|r| r.is_some()) {
275        // TODO(guswynn): Note we don't sort the raw rows, because
276        // there is no easy way of ensuring they sort the same way as actual.
277        Some(
278            actual
279                .iter()
280                .zip_eq(raw_actual)
281                .map(|(actual, unreplaced)| match unreplaced {
282                    Some(raw_row) => raw_row,
283                    None => actual.clone(),
284                })
285                .collect(),
286        )
287    } else {
288        None
289    };
290
291    actual.sort();
292    let actual_columns: Vec<_> = stmt.columns().iter().map(|c| c.name()).collect();
293
294    match expected_output {
295        SqlOutput::Full {
296            expected_rows,
297            column_names,
298        } => {
299            if let Some(column_names) = column_names {
300                if actual_columns.iter().ne(column_names) {
301                    if state.rewrite_results && !should_retry {
302                        rewrite_result(state, actual_columns, actual, raw_output)?;
303                        return Ok(());
304                    } else {
305                        bail!(
306                            "column name mismatch\nexpected: {:?}\nactual:   {:?}\n",
307                            column_names,
308                            actual_columns
309                        );
310                    }
311                }
312            }
313            if &actual == expected_rows {
314                Ok(())
315            } else if state.rewrite_results && !should_retry {
316                rewrite_result(state, actual_columns, actual, raw_output)?;
317                Ok(())
318            } else {
319                let (mut left, mut right) = (0, 0);
320                let mut buf = String::new();
321                while let (Some(e), Some(a)) = (expected_rows.get(left), actual.get(right)) {
322                    match e.cmp(a) {
323                        std::cmp::Ordering::Less => {
324                            writeln!(buf, "- {}", TestdriveRow(e)).unwrap();
325                            left += 1;
326                        }
327                        std::cmp::Ordering::Equal => {
328                            left += 1;
329                            right += 1;
330                        }
331                        std::cmp::Ordering::Greater => {
332                            writeln!(buf, "+ {}", TestdriveRow(a)).unwrap();
333                            right += 1;
334                        }
335                    }
336                }
337                while let Some(e) = expected_rows.get(left) {
338                    writeln!(buf, "- {}", TestdriveRow(e)).unwrap();
339                    left += 1;
340                }
341                while let Some(a) = actual.get(right) {
342                    writeln!(buf, "+ {}", TestdriveRow(a)).unwrap();
343                    right += 1;
344                }
345                if state.rewrite_results && !should_retry {
346                    rewrite_result(state, actual_columns, actual, raw_output)?;
347                    Ok(())
348                } else if let Some(raw_actual) = raw_actual {
349                    bail!(
350                        "non-matching rows: expected:\n{:?}\ngot:\n{:?}\ngot raw rows:\n{:?}\nPoor diff:\n{}",
351                        expected_rows,
352                        actual,
353                        raw_actual,
354                        buf,
355                    )
356                } else {
357                    bail!(
358                        "non-matching rows: expected:\n{:?}\ngot:\n{:?}\nPoor diff:\n{}",
359                        expected_rows,
360                        actual,
361                        buf
362                    )
363                }
364            }
365        }
366        SqlOutput::Hashed { num_values, md5 } => {
367            if &actual.len() != num_values {
368                bail!(
369                    "wrong row count: expected:\n{:?}\ngot:\n{:?}\n",
370                    num_values,
371                    actual.len(),
372                )
373            } else {
374                let mut hasher = Md5::new();
375                for row in &actual {
376                    for entry in row {
377                        hasher.update(entry);
378                    }
379                }
380                let actual = format!("{:x}", hasher.finalize());
381                if &actual != md5 {
382                    bail!("wrong hash value: expected:{:?} got:{:?}", md5, actual)
383                } else {
384                    Ok(())
385                }
386            }
387        }
388    }
389}
390
391#[derive(Clone)]
392enum ErrorMatcher {
393    Contains(String),
394    Exact(String),
395    Regex(Regex),
396    Timeout,
397}
398
399impl ErrorMatcher {
400    fn is_match(&self, err: &String) -> bool {
401        match self {
402            ErrorMatcher::Contains(s) => err.contains(s),
403            ErrorMatcher::Exact(s) => err == s,
404            ErrorMatcher::Regex(r) => r.is_match(err),
405            // Timeouts never match errors directly. If we are matching an error
406            // message, it means the query returned a result (i.e., an error
407            // result), which means the query did not time out as expected.
408            ErrorMatcher::Timeout => false,
409        }
410    }
411}
412
413impl fmt::Display for ErrorMatcher {
414    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
415        match self {
416            ErrorMatcher::Contains(s) => write!(f, "error containing {}", s.quoted()),
417            ErrorMatcher::Exact(s) => write!(f, "exact error {}", s.quoted()),
418            ErrorMatcher::Regex(s) => write!(f, "error matching regex {}", s.as_str().quoted()),
419            ErrorMatcher::Timeout => f.write_str("timeout"),
420        }
421    }
422}
423
424impl ErrorMatcher {
425    fn fmt_with_type(&self, type_: &str) -> String {
426        match self {
427            ErrorMatcher::Contains(s) => format!("{} containing {}", type_, s.quoted()),
428            ErrorMatcher::Exact(s) => format!("exact {} {}", type_, s.quoted()),
429            ErrorMatcher::Regex(s) => format!("{} matching regex {}", type_, s.as_str().quoted()),
430            ErrorMatcher::Timeout => "timeout".to_string(),
431        }
432    }
433}
434
435pub async fn run_fail_sql(
436    cmd: FailSqlCommand,
437    state: &mut State,
438) -> Result<ControlFlow, anyhow::Error> {
439    use Statement::{AlterSink, Commit, CreateConnection, Fetch, Rollback};
440
441    let stmts = mz_sql_parser::parser::parse_statements(&cmd.query)
442        .map_err(|e| format!("unable to parse SQL: {}: {}", cmd.query, e));
443
444    // Allow for statements that could not be parsed.
445    // This way such statements can be used for negative testing in .td files
446    let stmt = match stmts {
447        Ok(s) => {
448            if s.len() != 1 {
449                bail!("expected one statement, but got {}", s.len());
450            }
451            Some(s.into_element().ast)
452        }
453        Err(_) => None,
454    };
455
456    let expected_error = match cmd.expected_error {
457        SqlExpectedError::Contains(s) => ErrorMatcher::Contains(s),
458        SqlExpectedError::Exact(s) => ErrorMatcher::Exact(s),
459        SqlExpectedError::Regex(s) => ErrorMatcher::Regex(s.parse()?),
460        SqlExpectedError::Timeout => ErrorMatcher::Timeout,
461    };
462    let expected_detail = cmd.expected_detail.map(ErrorMatcher::Contains);
463    let expected_hint = cmd.expected_hint.map(ErrorMatcher::Contains);
464
465    let query = &cmd.query;
466    print_query(query, stmt.as_ref());
467
468    let should_retry = match &stmt {
469        // Do not retry statements that could not be parsed
470        None => false,
471        // Do not retry COMMIT and ROLLBACK. Once the transaction has errored out and has
472        // been aborted, retrying COMMIT or ROLLBACK will actually start succeeding, which
473        // causes testdrive to emit a confusing "query succeded but expected error" message.
474        Some(Commit(_)) | Some(Rollback(_)) => false,
475        // FETCH should not be retried because it consumes data on each response.
476        Some(Fetch(_)) => false,
477        Some(AlterSink(_)) => false,
478        Some(CreateConnection(_)) => false,
479        Some(_) => true,
480    };
481
482    state.error_line_count = 0;
483    state.error_string = "".to_string();
484    let res = match should_retry {
485        true => Retry::default()
486            .initial_backoff(state.initial_backoff)
487            .factor(state.backoff_factor)
488            .max_duration(state.timeout)
489            .max_tries(state.max_tries),
490        false => Retry::default().max_duration(state.timeout).max_tries(1),
491    }
492    .retry_async_with_state_canceling(state, |retry_state, state| {
493        let expected_error = expected_error.clone();
494        let expected_detail = expected_detail.clone();
495        let expected_hint = expected_hint.clone();
496        async move {
497            match try_run_fail_sql(
498                state,
499                query,
500                &expected_error,
501                expected_detail.as_ref(),
502                expected_hint.as_ref(),
503            )
504            .await
505            {
506                Ok(()) => {
507                    println!("query error matches; continuing");
508                    (state, Ok(()))
509                }
510                Err(e) => {
511                    if let Some(backoff) = retry_state.next_backoff {
512                        if !backoff.is_zero() && (retry_state.i == 0 || !mz_ore::env::is_var_truthy("CI")) {
513                            let error_string = format!("{:?}", e);
514                            if error_string != state.error_string {
515                                // Remove old status lines so as not to spam the output
516                                for _ in 0..state.error_line_count {
517                                    print!("\x1B[1A\x1B[2K");
518                                }
519                                state.error_line_count = 0;
520                                state.error_string = error_string;
521                                state.error_line_count = state.error_string.lines().count() + 1;
522                                if state.error_string.ends_with('\n') {
523                                    print!("{}", state.error_string);
524                                } else {
525                                    println!("{}", state.error_string);
526                                    state.error_line_count += 1;
527                                }
528                                println!("query error didn't match; sleeping to see if dataflow produces error shortly 🕑 {:.0?}", retry_state.next_backoff.unwrap_or_default());
529                            }
530                        }
531                    } else {
532                        for _ in 0..state.error_line_count {
533                            print!("\x1B[1A\x1B[2K");
534                        }
535                        state.error_line_count = 0;
536                    }
537                    (state, Err(e))
538                }
539            }
540        }})
541    .await;
542
543    // If a timeout was expected, check whether the retry operation timed
544    // out, which indicates that the test passed.
545    if let ErrorMatcher::Timeout = expected_error {
546        if let Err(e) = &res {
547            if e.is::<tokio::time::error::Elapsed>() {
548                println!("query timed out as expected");
549                return Ok(ControlFlow::Continue);
550            }
551        }
552    }
553
554    // Otherwise, return the error if any. Note that this is the error
555    // returned by the retry operation (e.g., "expected timeout, but query
556    // succeeded"), *not* an error returned from Materialize itself.
557    res?;
558    Ok(ControlFlow::Continue)
559}
560
561async fn try_run_fail_sql(
562    state: &State,
563    query: &str,
564    expected_error: &ErrorMatcher,
565    expected_detail: Option<&ErrorMatcher>,
566    expected_hint: Option<&ErrorMatcher>,
567) -> Result<(), anyhow::Error> {
568    // `query` is raw SQL from testdrive input and may include statements that
569    // cannot be represented as composable `Sql`.
570    #[allow(clippy::disallowed_methods)]
571    match state.materialize.pgclient.query(query, &[]).await {
572        Ok(_) => bail!("query succeeded, but expected {}", expected_error),
573        Err(err) => match err.source().and_then(|err| err.downcast_ref::<DbError>()) {
574            Some(err) => {
575                let mut err_string = err.message().to_string();
576                if let Some(regex) = &state.regex {
577                    err_string = regex
578                        .replace_all(&err_string, state.regex_replacement.as_str())
579                        .to_string();
580                }
581                if !expected_error.is_match(&err_string) {
582                    bail!("expected {}, got {}", expected_error, err_string.quoted());
583                }
584
585                let check_additional =
586                    |extra: Option<&str>, matcher: Option<&ErrorMatcher>, type_| {
587                        let extra = extra.map(|s| s.to_string());
588                        match (extra, matcher) {
589                            (Some(extra), Some(expected)) => {
590                                if !expected.is_match(&extra) {
591                                    bail!(
592                                        "expected {}, got {}",
593                                        expected.fmt_with_type(type_),
594                                        extra.quoted()
595                                    );
596                                }
597                            }
598                            (None, Some(expected)) => {
599                                bail!("expected {}, but found none", expected.fmt_with_type(type_));
600                            }
601                            _ => {}
602                        }
603                        Ok(())
604                    };
605
606                check_additional(err.detail(), expected_detail, "DETAIL")?;
607                check_additional(err.hint(), expected_hint, "HINT")?;
608
609                Ok(())
610            }
611            None => Err(err.into()),
612        },
613    }
614}
615
616pub fn print_query(query: &str, stmt: Option<&Statement<Raw>>) {
617    use Statement::*;
618    if let Some(CreateSecret(_)) = stmt {
619        println!(
620            "> CREATE SECRET [query truncated on purpose so as to not reveal the secret in the log]"
621        );
622    } else {
623        println!("> {}", query)
624    }
625}
626
627// Returns the row after regex replacments, and the before, if its different
628pub fn decode_row(
629    state: &State,
630    row: Row,
631) -> Result<(Vec<String>, Option<Vec<String>>), anyhow::Error> {
632    enum ArrayElement<T> {
633        Null,
634        NonNull(T),
635    }
636
637    impl<T> fmt::Display for ArrayElement<T>
638    where
639        T: fmt::Display,
640    {
641        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
642            match self {
643                ArrayElement::Null => f.write_str("NULL"),
644                ArrayElement::NonNull(t) => t.fmt(f),
645            }
646        }
647    }
648
649    impl<'a, T> FromSql<'a> for ArrayElement<T>
650    where
651        T: FromSql<'a>,
652    {
653        fn from_sql(
654            ty: &Type,
655            raw: &'a [u8],
656        ) -> Result<ArrayElement<T>, Box<dyn Error + Sync + Send>> {
657            T::from_sql(ty, raw).map(ArrayElement::NonNull)
658        }
659
660        fn from_sql_null(_: &Type) -> Result<ArrayElement<T>, Box<dyn Error + Sync + Send>> {
661            Ok(ArrayElement::Null)
662        }
663
664        fn accepts(ty: &Type) -> bool {
665            T::accepts(ty)
666        }
667    }
668
669    /// This lets us:
670    /// - Continue using the default method of printing array elements while
671    /// preserving SQL-looking output w/ `dec::to_standard_notation_string`.
672    /// - Avoid upstreaming a complicated change to `rust-postgres-array`.
673    struct NumericStandardNotation(Numeric);
674
675    impl fmt::Display for NumericStandardNotation {
676        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
677            write!(f, "{}", self.0.0.0.to_standard_notation_string())
678        }
679    }
680
681    impl<'a> FromSql<'a> for NumericStandardNotation {
682        fn from_sql(
683            ty: &Type,
684            raw: &'a [u8],
685        ) -> Result<NumericStandardNotation, Box<dyn Error + Sync + Send>> {
686            Ok(NumericStandardNotation(Numeric::from_sql(ty, raw)?))
687        }
688
689        fn from_sql_null(
690            ty: &Type,
691        ) -> Result<NumericStandardNotation, Box<dyn Error + Sync + Send>> {
692            Ok(NumericStandardNotation(Numeric::from_sql_null(ty)?))
693        }
694
695        fn accepts(ty: &Type) -> bool {
696            Numeric::accepts(ty)
697        }
698    }
699
700    let mut out = vec![];
701    let mut raw_out = vec![];
702    for (i, col) in row.columns().iter().enumerate() {
703        let ty = col.type_();
704        let mut value: String = match *ty {
705            Type::ACLITEM => row.get::<_, Option<AclItem>>(i).map(|x| x.0),
706            Type::BOOL => row.get::<_, Option<bool>>(i).map(|x| x.to_string()),
707            Type::BPCHAR | Type::TEXT | Type::VARCHAR => row.get::<_, Option<String>>(i),
708            Type::TEXT_ARRAY => row
709                .get::<_, Option<Array<ArrayElement<String>>>>(i)
710                .map(|a| a.to_string()),
711            Type::BYTEA => row.get::<_, Option<Vec<u8>>>(i).map(|x| {
712                let s = x.into_iter().map(ascii::escape_default).flatten().collect();
713                String::from_utf8(s).unwrap()
714            }),
715            Type::CHAR => row.get::<_, Option<i8>>(i).map(|x| x.to_string()),
716            Type::INT2 => row.get::<_, Option<i16>>(i).map(|x| x.to_string()),
717            Type::INT4 => row.get::<_, Option<i32>>(i).map(|x| x.to_string()),
718            Type::INT8 => row.get::<_, Option<i64>>(i).map(|x| x.to_string()),
719            Type::OID => row.get::<_, Option<u32>>(i).map(|x| x.to_string()),
720            Type::NUMERIC => row
721                .get::<_, Option<NumericStandardNotation>>(i)
722                .map(|x| x.to_string()),
723            Type::FLOAT4 => row.get::<_, Option<f32>>(i).map(|x| x.to_string()),
724            Type::FLOAT8 => row.get::<_, Option<f64>>(i).map(|x| x.to_string()),
725            Type::TIMESTAMP => row
726                .get::<_, Option<chrono::NaiveDateTime>>(i)
727                .map(|x| x.to_string()),
728            Type::TIMESTAMPTZ => row
729                .get::<_, Option<chrono::DateTime<chrono::Utc>>>(i)
730                .map(|x| x.to_string()),
731            Type::DATE => row
732                .get::<_, Option<chrono::NaiveDate>>(i)
733                .map(|x| x.to_string()),
734            Type::TIME => row
735                .get::<_, Option<chrono::NaiveTime>>(i)
736                .map(|x| x.to_string()),
737            Type::INTERVAL => row.get::<_, Option<Interval>>(i).map(|x| x.to_string()),
738            Type::JSONB => row.get::<_, Option<Jsonb>>(i).map(|v| v.0.to_string()),
739            Type::UUID => row.get::<_, Option<uuid::Uuid>>(i).map(|v| v.to_string()),
740            Type::BOOL_ARRAY => row
741                .get::<_, Option<Array<ArrayElement<bool>>>>(i)
742                .map(|a| a.to_string()),
743            Type::INT2_ARRAY | Type::INT2_VECTOR => row
744                .get::<_, Option<Array<ArrayElement<i16>>>>(i)
745                .map(|a| a.to_string()),
746            Type::INT4_ARRAY => row
747                .get::<_, Option<Array<ArrayElement<i32>>>>(i)
748                .map(|a| a.to_string()),
749            Type::INT8_ARRAY => row
750                .get::<_, Option<Array<ArrayElement<i64>>>>(i)
751                .map(|a| a.to_string()),
752            Type::OID_ARRAY => row
753                .get::<_, Option<Array<ArrayElement<u32>>>>(i)
754                .map(|x| x.to_string()),
755            Type::NUMERIC_ARRAY => row
756                .get::<_, Option<Array<ArrayElement<NumericStandardNotation>>>>(i)
757                .map(|x| x.to_string()),
758            Type::FLOAT4_ARRAY => row
759                .get::<_, Option<Array<ArrayElement<f32>>>>(i)
760                .map(|x| x.to_string()),
761            Type::FLOAT8_ARRAY => row
762                .get::<_, Option<Array<ArrayElement<f64>>>>(i)
763                .map(|x| x.to_string()),
764            Type::TIMESTAMP_ARRAY => row
765                .get::<_, Option<Array<ArrayElement<chrono::NaiveDateTime>>>>(i)
766                .map(|x| x.to_string()),
767            Type::TIMESTAMPTZ_ARRAY => row
768                .get::<_, Option<Array<ArrayElement<chrono::DateTime<chrono::Utc>>>>>(i)
769                .map(|x| x.to_string()),
770            Type::DATE_ARRAY => row
771                .get::<_, Option<Array<ArrayElement<chrono::NaiveDate>>>>(i)
772                .map(|x| x.to_string()),
773            Type::TIME_ARRAY => row
774                .get::<_, Option<Array<ArrayElement<chrono::NaiveTime>>>>(i)
775                .map(|x| x.to_string()),
776            Type::INTERVAL_ARRAY => row
777                .get::<_, Option<Array<ArrayElement<Interval>>>>(i)
778                .map(|x| x.to_string()),
779            Type::JSONB_ARRAY => row
780                .get::<_, Option<Array<ArrayElement<Jsonb>>>>(i)
781                .map(|v| v.to_string()),
782            Type::UUID_ARRAY => row
783                .get::<_, Option<Array<ArrayElement<uuid::Uuid>>>>(i)
784                .map(|v| v.to_string()),
785            Type::INT4_RANGE => row.get::<_, Option<Range<i32>>>(i).map(|v| v.to_string()),
786            Type::INT4_RANGE_ARRAY => row
787                .get::<_, Option<Array<ArrayElement<Range<i32>>>>>(i)
788                .map(|v| v.to_string()),
789            Type::INT8_RANGE => row.get::<_, Option<Range<i64>>>(i).map(|v| v.to_string()),
790            Type::INT8_RANGE_ARRAY => row
791                .get::<_, Option<Array<ArrayElement<Range<i64>>>>>(i)
792                .map(|v| v.to_string()),
793            Type::NUM_RANGE => row
794                .get::<_, Option<Range<NumericStandardNotation>>>(i)
795                .map(|v| v.to_string()),
796            Type::NUM_RANGE_ARRAY => row
797                .get::<_, Option<Array<ArrayElement<Range<NumericStandardNotation>>>>>(i)
798                .map(|v| v.to_string()),
799            Type::DATE_RANGE => row
800                .get::<_, Option<Range<chrono::NaiveDate>>>(i)
801                .map(|v| v.to_string()),
802            Type::DATE_RANGE_ARRAY => row
803                .get::<_, Option<Array<ArrayElement<Range<chrono::NaiveDate>>>>>(i)
804                .map(|v| v.to_string()),
805            Type::TS_RANGE => row
806                .get::<_, Option<Range<chrono::NaiveDateTime>>>(i)
807                .map(|v| v.to_string()),
808            Type::TS_RANGE_ARRAY => row
809                .get::<_, Option<Array<ArrayElement<Range<chrono::NaiveDateTime>>>>>(i)
810                .map(|v| v.to_string()),
811            Type::TSTZ_RANGE => row
812                .get::<_, Option<Range<chrono::DateTime<chrono::Utc>>>>(i)
813                .map(|v| v.to_string()),
814            Type::TSTZ_RANGE_ARRAY => row
815                .get::<_, Option<Array<ArrayElement<Range<chrono::DateTime<chrono::Utc>>>>>>(i)
816                .map(|v| v.to_string()),
817            _ => match ty.oid() {
818                mz_pgrepr::oid::TYPE_UINT2_OID => {
819                    row.get::<_, Option<UInt2>>(i).map(|x| x.0.to_string())
820                }
821                mz_pgrepr::oid::TYPE_UINT4_OID => {
822                    row.get::<_, Option<UInt4>>(i).map(|x| x.0.to_string())
823                }
824                mz_pgrepr::oid::TYPE_UINT8_OID => {
825                    row.get::<_, Option<UInt8>>(i).map(|x| x.0.to_string())
826                }
827                mz_pgrepr::oid::TYPE_MZ_TIMESTAMP_OID => {
828                    row.get::<_, Option<MzTimestamp>>(i).map(|x| x.0)
829                }
830                _ => bail!("unsupported SQL type in testdrive: {:?}", ty),
831            },
832        }
833        .unwrap_or_else(|| "<null>".into());
834
835        raw_out.push(value.clone());
836        if let Some(regex) = &state.regex {
837            value = regex
838                .replace_all(&value, state.regex_replacement.as_str())
839                .to_string();
840        }
841
842        out.push(value);
843    }
844    let raw_out = if out != raw_out { Some(raw_out) } else { None };
845    Ok((out, raw_out))
846}
847
848struct MzTimestamp(String);
849
850impl<'a> FromSql<'a> for MzTimestamp {
851    fn from_sql(_: &Type, raw: &'a [u8]) -> Result<MzTimestamp, Box<dyn Error + Sync + Send>> {
852        Ok(MzTimestamp(std::str::from_utf8(raw)?.to_string()))
853    }
854
855    fn accepts(ty: &Type) -> bool {
856        ty.oid() == mz_pgrepr::oid::TYPE_MZ_TIMESTAMP_OID
857    }
858}
859
860#[allow(dead_code)]
861struct MzAclItem(String);
862
863impl<'a> FromSql<'a> for MzAclItem {
864    fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
865        Ok(MzAclItem(std::str::from_utf8(raw)?.to_string()))
866    }
867
868    fn accepts(ty: &Type) -> bool {
869        ty.oid() == mz_pgrepr::oid::TYPE_MZ_ACL_ITEM_OID
870    }
871}
872
873struct AclItem(String);
874
875impl<'a> FromSql<'a> for AclItem {
876    fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
877        Ok(AclItem(std::str::from_utf8(raw)?.to_string()))
878    }
879
880    fn accepts(ty: &Type) -> bool {
881        ty.oid() == 1033
882    }
883}
884
885struct TestdriveRow<'a>(&'a Vec<String>);
886
887impl Display for TestdriveRow<'_> {
888    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
889        let mut cols = Vec::<String>::new();
890
891        for col_str in &self.0[0..self.0.len()] {
892            if col_str.contains(' ') || col_str.contains('"') || col_str.is_empty() {
893                cols.push(escape_for_parser(col_str));
894            } else {
895                cols.push(col_str.to_string());
896            }
897        }
898
899        write!(f, "{}", cols.join(" "))
900    }
901}