Skip to main content

mz_sqllogictest/
runner.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
10//! The Materialize-specific runner for sqllogictest.
11//!
12//! slt tests expect a serialized execution of sql statements and queries.
13//! To get the same results in materialize we track current_timestamp and increment it whenever we execute a statement.
14//!
15//! The high-level workflow is:
16//!   for each record in the test file:
17//!     if record is a sql statement:
18//!       run sql in postgres, observe changes and copy them to materialize using LocalInput::Updates(..)
19//!       advance current_timestamp
20//!       promise to never send updates for times < current_timestamp using LocalInput::Watermark(..)
21//!       compare to expected results
22//!       if wrong, bail out and stop processing this file
23//!     if record is a sql query:
24//!       peek query at current_timestamp
25//!       compare to expected results
26//!       if wrong, record the error
27
28use std::collections::BTreeMap;
29use std::error::Error;
30use std::fs::{File, OpenOptions};
31use std::io::{Read, Seek, SeekFrom, Write};
32use std::net::{IpAddr, Ipv4Addr, SocketAddr};
33use std::path::Path;
34use std::sync::Arc;
35use std::sync::LazyLock;
36use std::time::Duration;
37use std::{env, fmt, ops, str, thread};
38
39use anyhow::{anyhow, bail};
40use bytes::BytesMut;
41use chrono::{DateTime, NaiveDateTime, NaiveTime, Utc};
42use fallible_iterator::FallibleIterator;
43use futures::sink::SinkExt;
44use itertools::Itertools;
45use maplit::btreemap;
46use md5::{Digest, Md5};
47use mz_adapter_types::bootstrap_builtin_cluster_config::{
48    ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
49    CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
50    SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR, SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
51};
52use mz_catalog::config::ClusterReplicaSizeMap;
53use mz_controller::{ControllerConfig, ReplicaHttpLocator};
54use mz_environmentd::CatalogConfig;
55use mz_license_keys::ValidatedLicenseKey;
56use mz_orchestrator_process::{ProcessOrchestrator, ProcessOrchestratorConfig};
57use mz_orchestrator_tracing::{TracingCliArgs, TracingOrchestrator};
58use mz_ore::cast::{CastFrom, ReinterpretCast};
59use mz_ore::channel::trigger;
60use mz_ore::error::ErrorExt;
61use mz_ore::metrics::MetricsRegistry;
62use mz_ore::now::SYSTEM_TIME;
63use mz_ore::retry::Retry;
64use mz_ore::sql;
65use mz_ore::sql::Sql;
66use mz_ore::task;
67use mz_ore::thread::{JoinHandleExt, JoinOnDropHandle};
68use mz_ore::tracing::TracingHandle;
69use mz_ore::url::SensitiveUrl;
70use mz_persist_client::PersistLocation;
71use mz_persist_client::cache::PersistClientCache;
72use mz_persist_client::cfg::PersistConfig;
73use mz_persist_client::rpc::{
74    MetricsSameProcessPubSubSender, PersistGrpcPubSubServer, PubSubClientConnection, PubSubSender,
75};
76use mz_pgrepr::{Interval, Jsonb, Numeric, UInt2, UInt4, UInt8, Value, oid};
77use mz_repr::ColumnName;
78use mz_repr::adt::date::Date;
79use mz_repr::adt::mz_acl_item::{AclItem, MzAclItem};
80use mz_repr::adt::numeric;
81use mz_secrets::SecretsController;
82use mz_server_core::listeners::v26_32_0::ListenersConfig;
83use mz_server_core::listeners::{
84    AllowedRoles, AuthenticatorKind, HttpListenerConfig, HttpRoutesEnabled, RouteGroup,
85    SqlListenerConfig,
86};
87use mz_sql::ast::{Expr, Raw, Statement};
88use mz_sql::catalog::EnvironmentId;
89use mz_sql_parser::ast::display::AstDisplay;
90use mz_sql_parser::ast::{
91    CreateIndexStatement, CreateViewStatement, CteBlock, Distinct, DropObjectsStatement, Ident,
92    IfExistsBehavior, ObjectType, OrderByExpr, Query, RawItemName, Select, SelectItem,
93    SelectStatement, SetExpr, Statement as AstStatement, TableFactor, TableWithJoins,
94    UnresolvedItemName, UnresolvedObjectName, ViewDefinition,
95};
96use mz_sql_parser::parser;
97use mz_storage_types::connections::ConnectionContext;
98use postgres_protocol::types;
99use regex::Regex;
100use tempfile::TempDir;
101use tokio::net::TcpListener;
102use tokio::runtime::Runtime;
103use tokio::sync::oneshot;
104use tokio_postgres::types::{FromSql, Kind as PgKind, Type as PgType};
105use tokio_postgres::{NoTls, Row, SimpleQueryMessage};
106use tokio_stream::wrappers::TcpListenerStream;
107use tower_http::cors::AllowOrigin;
108use tracing::{error, info};
109use uuid::Uuid;
110use uuid::fmt::Simple;
111
112use crate::ast::{Location, Mode, Output, QueryOutput, Record, Sort, Type};
113use crate::util;
114
115#[derive(Debug)]
116pub enum Outcome<'a> {
117    Unsupported {
118        error: anyhow::Error,
119        location: Location,
120    },
121    ParseFailure {
122        error: anyhow::Error,
123        location: Location,
124    },
125    PlanFailure {
126        error: anyhow::Error,
127        expected_error: Option<String>,
128        location: Location,
129    },
130    UnexpectedPlanSuccess {
131        expected_error: &'a str,
132        location: Location,
133    },
134    WrongNumberOfRowsInserted {
135        expected_count: u64,
136        actual_count: u64,
137        location: Location,
138    },
139    WrongColumnCount {
140        expected_count: usize,
141        actual_count: usize,
142        location: Location,
143    },
144    WrongColumnNames {
145        expected_column_names: &'a Vec<ColumnName>,
146        actual_column_names: Vec<ColumnName>,
147        actual_output: Output,
148        location: Location,
149    },
150    OutputFailure {
151        expected_output: &'a Output,
152        actual_raw_output: Vec<Row>,
153        actual_output: Output,
154        location: Location,
155    },
156    InconsistentViewOutcome {
157        query_outcome: Box<Outcome<'a>>,
158        view_outcome: Box<Outcome<'a>>,
159        location: Location,
160    },
161    Bail {
162        cause: Box<Outcome<'a>>,
163        location: Location,
164    },
165    Warning {
166        cause: Box<Outcome<'a>>,
167        location: Location,
168    },
169    Success,
170}
171
172const NUM_OUTCOMES: usize = 12;
173const WARNING_OUTCOME: usize = NUM_OUTCOMES - 2;
174const SUCCESS_OUTCOME: usize = NUM_OUTCOMES - 1;
175
176impl<'a> Outcome<'a> {
177    fn code(&self) -> usize {
178        match self {
179            Outcome::Unsupported { .. } => 0,
180            Outcome::ParseFailure { .. } => 1,
181            Outcome::PlanFailure { .. } => 2,
182            Outcome::UnexpectedPlanSuccess { .. } => 3,
183            Outcome::WrongNumberOfRowsInserted { .. } => 4,
184            Outcome::WrongColumnCount { .. } => 5,
185            Outcome::WrongColumnNames { .. } => 6,
186            Outcome::OutputFailure { .. } => 7,
187            Outcome::InconsistentViewOutcome { .. } => 8,
188            Outcome::Bail { .. } => 9,
189            Outcome::Warning { .. } => 10,
190            Outcome::Success => 11,
191        }
192    }
193
194    fn success(&self) -> bool {
195        matches!(self, Outcome::Success)
196    }
197
198    fn failure(&self) -> bool {
199        !matches!(self, Outcome::Success) && !matches!(self, Outcome::Warning { .. })
200    }
201
202    /// Returns an error message that will match self. Appropriate for
203    /// rewriting error messages (i.e. not inserting error messages where we
204    /// currently expect success).
205    fn err_msg(&self) -> Option<String> {
206        match self {
207            Outcome::Unsupported { error, .. }
208            | Outcome::ParseFailure { error, .. }
209            | Outcome::PlanFailure { error, .. } => {
210                // Take only the first line, which should be sufficient for
211                // meaningfully matching the error.
212                let err_str = error.to_string_with_causes();
213                let err_str = err_str.split('\n').next().unwrap();
214                // Strip the "db error: ERROR: " prefix added by the postgres
215                // client library, as it's noisy and not useful for matching.
216                let err_str = err_str.strip_prefix("db error: ERROR: ").unwrap_or(err_str);
217                // This value gets fed back into regex to check that it matches
218                // `self`, so escape its meta characters. We need to undo the
219                // escaping of #. `regex::escape` escapes this because it
220                // expects that we use the `x` flag when building a regex, but
221                // this is not the case, so \# would end up being an invalid
222                // escape sequence, which would choke the parsing of the slt
223                // file the next time around.
224                Some(regex::escape(err_str).replace(r"\#", "#"))
225            }
226            _ => None,
227        }
228    }
229}
230
231impl fmt::Display for Outcome<'_> {
232    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
233        use Outcome::*;
234        const INDENT: &str = "\n        ";
235        match self {
236            Unsupported { error, location } => write!(
237                f,
238                "Unsupported:{}:\n{}",
239                location,
240                error.display_with_causes()
241            ),
242            ParseFailure { error, location } => {
243                write!(
244                    f,
245                    "ParseFailure:{}:\n{}",
246                    location,
247                    error.display_with_causes()
248                )
249            }
250            PlanFailure {
251                error,
252                expected_error,
253                location,
254            } => {
255                if let Some(expected_error) = expected_error {
256                    write!(
257                        f,
258                        "PlanFailure:{}:\nerror does not match expected pattern:\n  expected: /{}/\n  actual:    {}",
259                        location,
260                        expected_error,
261                        error.display_with_causes()
262                    )
263                } else {
264                    write!(f, "PlanFailure:{}:\n{:#}", location, error)
265                }
266            }
267            UnexpectedPlanSuccess {
268                expected_error,
269                location,
270            } => write!(
271                f,
272                "UnexpectedPlanSuccess:{} expected error: {}",
273                location, expected_error
274            ),
275            WrongNumberOfRowsInserted {
276                expected_count,
277                actual_count,
278                location,
279            } => write!(
280                f,
281                "WrongNumberOfRowsInserted:{}{}expected: {}{}actually: {}",
282                location, INDENT, expected_count, INDENT, actual_count
283            ),
284            WrongColumnCount {
285                expected_count,
286                actual_count,
287                location,
288            } => write!(
289                f,
290                "WrongColumnCount:{}{}expected: {}{}actually: {}",
291                location, INDENT, expected_count, INDENT, actual_count
292            ),
293            WrongColumnNames {
294                expected_column_names,
295                actual_column_names,
296                actual_output: _,
297                location,
298            } => write!(
299                f,
300                "Wrong Column Names:{}:{}expected column names: {}{}inferred column names: {}",
301                location,
302                INDENT,
303                expected_column_names
304                    .iter()
305                    .map(|n| n.to_string())
306                    .collect::<Vec<_>>()
307                    .join(" "),
308                INDENT,
309                actual_column_names
310                    .iter()
311                    .map(|n| n.to_string())
312                    .collect::<Vec<_>>()
313                    .join(" ")
314            ),
315            OutputFailure {
316                expected_output,
317                actual_raw_output,
318                actual_output,
319                location,
320            } => write!(
321                f,
322                "OutputFailure:{}{}expected: {:?}{}actually: {:?}{}actual raw: {:?}",
323                location, INDENT, expected_output, INDENT, actual_output, INDENT, actual_raw_output
324            ),
325            InconsistentViewOutcome {
326                query_outcome,
327                view_outcome,
328                location,
329            } => write!(
330                f,
331                "InconsistentViewOutcome:{}{}expected from query: {}{}actually from indexed view: {}",
332                location, INDENT, query_outcome, INDENT, view_outcome
333            ),
334            Bail { cause, location } => write!(f, "Bail:{} {}", location, cause),
335            Warning { cause, location } => write!(f, "Warning:{} {}", location, cause),
336            Success => f.write_str("Success"),
337        }
338    }
339}
340
341#[derive(Default, Debug)]
342pub struct Outcomes {
343    stats: [usize; NUM_OUTCOMES],
344    details: Vec<String>,
345}
346
347impl ops::AddAssign<Outcomes> for Outcomes {
348    fn add_assign(&mut self, rhs: Outcomes) {
349        for (lhs, rhs) in self.stats.iter_mut().zip_eq(rhs.stats.iter()) {
350            *lhs += rhs
351        }
352    }
353}
354impl Outcomes {
355    pub fn any_failed(&self) -> bool {
356        self.stats[SUCCESS_OUTCOME] + self.stats[WARNING_OUTCOME] < self.stats.iter().sum::<usize>()
357    }
358
359    pub fn as_json(&self) -> serde_json::Value {
360        serde_json::json!({
361            "unsupported": self.stats[0],
362            "parse_failure": self.stats[1],
363            "plan_failure": self.stats[2],
364            "unexpected_plan_success": self.stats[3],
365            "wrong_number_of_rows_affected": self.stats[4],
366            "wrong_column_count": self.stats[5],
367            "wrong_column_names": self.stats[6],
368            "output_failure": self.stats[7],
369            "inconsistent_view_outcome": self.stats[8],
370            "bail": self.stats[9],
371            "warning": self.stats[10],
372            "success": self.stats[11],
373        })
374    }
375
376    pub fn display(&self, no_fail: bool, failure_details: bool) -> OutcomesDisplay<'_> {
377        OutcomesDisplay {
378            inner: self,
379            no_fail,
380            failure_details,
381        }
382    }
383}
384
385pub struct OutcomesDisplay<'a> {
386    inner: &'a Outcomes,
387    no_fail: bool,
388    failure_details: bool,
389}
390
391impl<'a> fmt::Display for OutcomesDisplay<'a> {
392    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
393        let total: usize = self.inner.stats.iter().sum();
394        if self.failure_details
395            && (self.inner.stats[SUCCESS_OUTCOME] + self.inner.stats[WARNING_OUTCOME] != total
396                || self.no_fail)
397        {
398            for outcome in &self.inner.details {
399                writeln!(f, "{}", outcome)?;
400            }
401            Ok(())
402        } else {
403            write!(
404                f,
405                "{}:",
406                if self.inner.stats[SUCCESS_OUTCOME] + self.inner.stats[WARNING_OUTCOME] == total {
407                    "PASS"
408                } else if self.no_fail {
409                    "FAIL-IGNORE"
410                } else {
411                    "FAIL"
412                }
413            )?;
414            static NAMES: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
415                vec![
416                    "unsupported",
417                    "parse-failure",
418                    "plan-failure",
419                    "unexpected-plan-success",
420                    "wrong-number-of-rows-inserted",
421                    "wrong-column-count",
422                    "wrong-column-names",
423                    "output-failure",
424                    "inconsistent-view-outcome",
425                    "bail",
426                    "warning",
427                    "success",
428                    "total",
429                ]
430            });
431            for (i, n) in self.inner.stats.iter().enumerate() {
432                if *n > 0 {
433                    write!(f, " {}={}", NAMES[i], n)?;
434                }
435            }
436            write!(f, " total={}", total)
437        }
438    }
439}
440
441struct QueryInfo {
442    is_select: bool,
443    num_attributes: Option<usize>,
444    /// Whether the `SELECT` carries an `AS OF` clause. Such queries are
445    /// excluded from the `--auto-index-selects` consistency check: re-running
446    /// them against a freshly created indexed view is inherently racy, because
447    /// the view's `since` advances with time and can move past a historical
448    /// `AS OF`, yielding a spurious "could not find a valid timestamp" failure.
449    has_as_of: bool,
450}
451
452enum PrepareQueryOutcome<'a> {
453    QueryPrepared(QueryInfo),
454    Outcome(Outcome<'a>),
455}
456
457pub struct Runner<'a> {
458    config: &'a RunConfig<'a>,
459    inner: Option<RunnerInner<'a>>,
460    /// Active `replace` substitutions, applied to actual query output before it
461    /// is compared/rewritten. Held on the outer `Runner` so they survive a
462    /// `reset-server`. See `Record::Replace`.
463    replacements: Vec<(Regex, String)>,
464}
465
466pub struct RunnerInner<'a> {
467    server_addr: SocketAddr,
468    internal_server_addr: SocketAddr,
469    password_server_addr: SocketAddr,
470    internal_http_server_addr: SocketAddr,
471    // Drop order matters for these fields.
472    client: tokio_postgres::Client,
473    system_client: tokio_postgres::Client,
474    clients: BTreeMap<String, tokio_postgres::Client>,
475    /// The user set by the most recent `user` directive, if any. Records run
476    /// on that user's connection (cached in `clients`) instead of `client`.
477    active_user: Option<String>,
478    auto_index_tables: bool,
479    auto_index_selects: bool,
480    auto_transactions: bool,
481    enable_table_keys: bool,
482    verbose: bool,
483    stdout: &'a dyn WriteFmt,
484    _shutdown_trigger: trigger::Trigger,
485    _server_thread: JoinOnDropHandle<()>,
486    _temp_dir: TempDir,
487}
488
489#[derive(Debug)]
490pub struct Slt(Value);
491
492impl<'a> FromSql<'a> for Slt {
493    fn from_sql(
494        ty: &PgType,
495        mut raw: &'a [u8],
496    ) -> Result<Self, Box<dyn Error + 'static + Send + Sync>> {
497        Ok(match *ty {
498            PgType::ACLITEM => Self(Value::AclItem(AclItem::decode_binary(
499                types::bytea_from_sql(raw),
500            )?)),
501            PgType::BOOL => Self(Value::Bool(types::bool_from_sql(raw)?)),
502            PgType::BYTEA => Self(Value::Bytea(types::bytea_from_sql(raw).to_vec())),
503            PgType::CHAR => Self(Value::Char(u8::from_be_bytes(
504                types::char_from_sql(raw)?.to_be_bytes(),
505            ))),
506            PgType::FLOAT4 => Self(Value::Float4(types::float4_from_sql(raw)?)),
507            PgType::FLOAT8 => Self(Value::Float8(types::float8_from_sql(raw)?)),
508            PgType::DATE => Self(Value::Date(Date::from_pg_epoch(types::int4_from_sql(
509                raw,
510            )?)?)),
511            PgType::INT2 => Self(Value::Int2(types::int2_from_sql(raw)?)),
512            PgType::INT4 => Self(Value::Int4(types::int4_from_sql(raw)?)),
513            PgType::INT8 => Self(Value::Int8(types::int8_from_sql(raw)?)),
514            PgType::INTERVAL => Self(Value::Interval(Interval::from_sql(ty, raw)?)),
515            PgType::JSONB => Self(Value::Jsonb(Jsonb::from_sql(ty, raw)?)),
516            PgType::NAME => Self(Value::Name(types::text_from_sql(raw)?.to_string())),
517            PgType::NUMERIC => Self(Value::Numeric(Numeric::from_sql(ty, raw)?)),
518            PgType::OID => Self(Value::Oid(types::oid_from_sql(raw)?)),
519            PgType::REGCLASS => Self(Value::Oid(types::oid_from_sql(raw)?)),
520            PgType::REGPROC => Self(Value::Oid(types::oid_from_sql(raw)?)),
521            PgType::REGTYPE => Self(Value::Oid(types::oid_from_sql(raw)?)),
522            PgType::TEXT | PgType::BPCHAR | PgType::VARCHAR => {
523                Self(Value::Text(types::text_from_sql(raw)?.to_string()))
524            }
525            PgType::TIME => Self(Value::Time(NaiveTime::from_sql(ty, raw)?)),
526            PgType::TIMESTAMP => Self(Value::Timestamp(
527                NaiveDateTime::from_sql(ty, raw)?.try_into()?,
528            )),
529            PgType::TIMESTAMPTZ => Self(Value::TimestampTz(
530                DateTime::<Utc>::from_sql(ty, raw)?.try_into()?,
531            )),
532            PgType::UUID => Self(Value::Uuid(Uuid::from_sql(ty, raw)?)),
533            PgType::RECORD => {
534                let num_fields = read_be_i32(&mut raw)?;
535                let mut tuple = vec![];
536                for _ in 0..num_fields {
537                    let oid = u32::reinterpret_cast(read_be_i32(&mut raw)?);
538                    let typ = match PgType::from_oid(oid) {
539                        Some(typ) => typ,
540                        None => return Err("unknown oid".into()),
541                    };
542                    let v = read_value::<Option<Slt>>(&typ, &mut raw)?;
543                    tuple.push(v.map(|v| v.0));
544                }
545                Self(Value::Record(tuple))
546            }
547            PgType::INT4_RANGE
548            | PgType::INT8_RANGE
549            | PgType::DATE_RANGE
550            | PgType::NUM_RANGE
551            | PgType::TS_RANGE
552            | PgType::TSTZ_RANGE => {
553                use mz_repr::adt::range::Range;
554                let range: Range<Slt> = Range::from_sql(ty, raw)?;
555                Self(Value::Range(range.into_bounds(|b| Box::new(b.0))))
556            }
557
558            _ => match ty.kind() {
559                PgKind::Array(arr_type) => {
560                    let arr = types::array_from_sql(raw)?;
561                    let elements: Vec<Option<Value>> = arr
562                        .values()
563                        .map(|v| match v {
564                            Some(v) => Ok(Some(Slt::from_sql(arr_type, v)?)),
565                            None => Ok(None),
566                        })
567                        .collect::<Vec<Option<Slt>>>()?
568                        .into_iter()
569                        // Map a Vec<Option<Slt>> to Vec<Option<Value>>.
570                        .map(|v| v.map(|v| v.0))
571                        .collect();
572
573                    Self(Value::Array {
574                        dims: arr
575                            .dimensions()
576                            .map(|d| {
577                                Ok(mz_repr::adt::array::ArrayDimension {
578                                    lower_bound: isize::cast_from(d.lower_bound),
579                                    length: usize::try_from(d.len)
580                                        .expect("cannot have negative length"),
581                                })
582                            })
583                            .collect()?,
584                        elements,
585                    })
586                }
587                _ => match ty.oid() {
588                    oid::TYPE_UINT2_OID => Self(Value::UInt2(UInt2::from_sql(ty, raw)?)),
589                    oid::TYPE_UINT4_OID => Self(Value::UInt4(UInt4::from_sql(ty, raw)?)),
590                    oid::TYPE_UINT8_OID => Self(Value::UInt8(UInt8::from_sql(ty, raw)?)),
591                    oid::TYPE_MZ_TIMESTAMP_OID => {
592                        let s = types::text_from_sql(raw)?;
593                        let t: mz_repr::Timestamp = s.parse()?;
594                        Self(Value::MzTimestamp(t))
595                    }
596                    oid::TYPE_MZ_ACL_ITEM_OID => Self(Value::MzAclItem(MzAclItem::decode_binary(
597                        types::bytea_from_sql(raw),
598                    )?)),
599                    _ => unreachable!(),
600                },
601            },
602        })
603    }
604    fn accepts(ty: &PgType) -> bool {
605        match ty.kind() {
606            PgKind::Array(_) | PgKind::Composite(_) => return true,
607            _ => {}
608        }
609        match ty.oid() {
610            oid::TYPE_UINT2_OID
611            | oid::TYPE_UINT4_OID
612            | oid::TYPE_UINT8_OID
613            | oid::TYPE_MZ_TIMESTAMP_OID
614            | oid::TYPE_MZ_ACL_ITEM_OID => return true,
615            _ => {}
616        }
617        matches!(
618            *ty,
619            PgType::ACLITEM
620                | PgType::BOOL
621                | PgType::BYTEA
622                | PgType::CHAR
623                | PgType::DATE
624                | PgType::FLOAT4
625                | PgType::FLOAT8
626                | PgType::INT2
627                | PgType::INT4
628                | PgType::INT8
629                | PgType::INTERVAL
630                | PgType::JSONB
631                | PgType::NAME
632                | PgType::NUMERIC
633                | PgType::OID
634                | PgType::REGCLASS
635                | PgType::REGPROC
636                | PgType::REGTYPE
637                | PgType::RECORD
638                | PgType::TEXT
639                | PgType::BPCHAR
640                | PgType::VARCHAR
641                | PgType::TIME
642                | PgType::TIMESTAMP
643                | PgType::TIMESTAMPTZ
644                | PgType::UUID
645                | PgType::INT4_RANGE
646                | PgType::INT4_RANGE_ARRAY
647                | PgType::INT8_RANGE
648                | PgType::INT8_RANGE_ARRAY
649                | PgType::DATE_RANGE
650                | PgType::DATE_RANGE_ARRAY
651                | PgType::NUM_RANGE
652                | PgType::NUM_RANGE_ARRAY
653                | PgType::TS_RANGE
654                | PgType::TS_RANGE_ARRAY
655                | PgType::TSTZ_RANGE
656                | PgType::TSTZ_RANGE_ARRAY
657        )
658    }
659}
660
661// From postgres-types/src/private.rs.
662fn read_be_i32(buf: &mut &[u8]) -> Result<i32, Box<dyn Error + Sync + Send>> {
663    if buf.len() < 4 {
664        return Err("invalid buffer size".into());
665    }
666    let mut bytes = [0; 4];
667    bytes.copy_from_slice(&buf[..4]);
668    *buf = &buf[4..];
669    Ok(i32::from_be_bytes(bytes))
670}
671
672// From postgres-types/src/private.rs.
673fn read_value<'a, T>(type_: &PgType, buf: &mut &'a [u8]) -> Result<T, Box<dyn Error + Sync + Send>>
674where
675    T: FromSql<'a>,
676{
677    let value = match usize::try_from(read_be_i32(buf)?) {
678        Err(_) => None,
679        Ok(len) => {
680            if len > buf.len() {
681                return Err("invalid buffer size".into());
682            }
683            let (head, tail) = buf.split_at(len);
684            *buf = tail;
685            Some(head)
686        }
687    };
688    T::from_sql_nullable(type_, value)
689}
690
691fn format_datum(d: Slt, typ: &Type, mode: Mode) -> String {
692    match (typ, d.0) {
693        (Type::Bool, Value::Bool(b)) => b.to_string(),
694
695        (Type::Integer, Value::Int2(i)) => i.to_string(),
696        (Type::Integer, Value::Int4(i)) => i.to_string(),
697        (Type::Integer, Value::Int8(i)) => i.to_string(),
698        (Type::Integer, Value::UInt2(u)) => u.0.to_string(),
699        (Type::Integer, Value::UInt4(u)) => u.0.to_string(),
700        (Type::Integer, Value::UInt8(u)) => u.0.to_string(),
701        (Type::Integer, Value::Oid(i)) => i.to_string(),
702        // TODO(benesch): rewrite to avoid `as`.
703        #[allow(clippy::as_conversions)]
704        (Type::Integer, Value::Float4(f)) => format!("{}", f as i64),
705        // TODO(benesch): rewrite to avoid `as`.
706        #[allow(clippy::as_conversions)]
707        (Type::Integer, Value::Float8(f)) => format!("{}", f as i64),
708        // This is so wrong, but sqlite needs it.
709        (Type::Integer, Value::Text(_)) => "0".to_string(),
710        (Type::Integer, Value::Bool(b)) => i8::from(b).to_string(),
711        (Type::Integer, Value::Numeric(d)) => {
712            let mut d = d.0.0.clone();
713            let mut cx = numeric::cx_datum();
714            // Truncate the decimal to match sqlite.
715            if mode == Mode::Standard {
716                cx.set_rounding(dec::Rounding::Down);
717            }
718            cx.round(&mut d);
719            numeric::munge_numeric(&mut d).unwrap();
720            d.to_standard_notation_string()
721        }
722
723        (Type::Real, Value::Int2(i)) => format!("{:.3}", i),
724        (Type::Real, Value::Int4(i)) => format!("{:.3}", i),
725        (Type::Real, Value::Int8(i)) => format!("{:.3}", i),
726        (Type::Real, Value::Float4(f)) => match mode {
727            Mode::Standard => format!("{:.3}", f),
728            Mode::Cockroach => format!("{}", f),
729        },
730        (Type::Real, Value::Float8(f)) => match mode {
731            Mode::Standard => format!("{:.3}", f),
732            Mode::Cockroach => format!("{}", f),
733        },
734        (Type::Real, Value::Numeric(d)) => match mode {
735            Mode::Standard => {
736                let mut d = d.0.0.clone();
737                if d.exponent() < -3 {
738                    numeric::rescale(&mut d, 3).unwrap();
739                }
740                numeric::munge_numeric(&mut d).unwrap();
741                d.to_standard_notation_string()
742            }
743            Mode::Cockroach => d.0.0.to_standard_notation_string(),
744        },
745
746        (Type::Text, Value::Text(s)) => {
747            if s.is_empty() {
748                "(empty)".to_string()
749            } else {
750                s
751            }
752        }
753        (Type::Text, Value::Bool(b)) => b.to_string(),
754        (Type::Text, Value::Float4(f)) => format!("{:.3}", f),
755        (Type::Text, Value::Float8(f)) => format!("{:.3}", f),
756        // Bytes are printed as text iff they are valid UTF-8. This
757        // seems guaranteed to confuse everyone, but it is required for
758        // compliance with the CockroachDB sqllogictest runner. [0]
759        //
760        // [0]: https://github.com/cockroachdb/cockroach/blob/970782487/pkg/sql/logictest/logic.go#L2038-L2043
761        (Type::Text, Value::Bytea(b)) => match str::from_utf8(&b) {
762            Ok(s) => s.to_string(),
763            Err(_) => format!("{:?}", b),
764        },
765        (Type::Text, Value::Numeric(d)) => d.0.0.to_standard_notation_string(),
766        // Everything else gets normal text encoding. This correctly handles things
767        // like arrays, tuples, and strings that need to be quoted.
768        (Type::Text, d) => {
769            let mut buf = BytesMut::new();
770            d.encode_text(&mut buf, mz_pgrepr::TextEncodeSettings::STABLE);
771            String::from_utf8_lossy(&buf).into_owned()
772        }
773
774        (Type::Oid, Value::Oid(o)) => o.to_string(),
775
776        // A mismatch between the declared column type and the value the query
777        // actually returned. Fall back to normal text encoding so the
778        // mismatch surfaces as an output diff rather than a panic that aborts
779        // the entire run.
780        (_, d) => {
781            let mut buf = BytesMut::new();
782            d.encode_text(&mut buf, mz_pgrepr::TextEncodeSettings::STABLE);
783            String::from_utf8_lossy(&buf).into_owned()
784        }
785    }
786}
787
788fn format_row(row: &Row, types: &[Type], mode: Mode) -> Vec<String> {
789    let mut formatted: Vec<String> = vec![];
790    for i in 0..row.len() {
791        let t: Option<Slt> = row.get::<usize, Option<Slt>>(i);
792        let t: Option<String> = t.map(|d| format_datum(d, &types[i], mode));
793        formatted.push(match t {
794            Some(t) => t,
795            None => "NULL".into(),
796        });
797    }
798
799    formatted
800}
801
802/// Reports whether `err` matches the `expected_error` regex of a record.
803///
804/// Expected errors in files imported from CockroachDB are not always valid
805/// regexes. An invalid regex cannot match, so the record fails with the usual
806/// mismatch outcome rather than aborting the entire run.
807fn error_matches(expected_error: &str, err: &str) -> bool {
808    match Regex::new(expected_error) {
809        Ok(re) => re.is_match(err),
810        Err(_) => false,
811    }
812}
813
814/// Removes CockroachDB-specific physical-layout items (`INDEX`,
815/// `UNIQUE INDEX`, `INVERTED INDEX`, and `FAMILY` definitions) from the
816/// column list of a `CREATE TABLE` statement. Constraints are kept, since
817/// dropping them would change the statement's semantics. Returns `None` if
818/// `sql` does not look like a `CREATE TABLE` statement.
819fn strip_crdb_table_items(sql: &str) -> Option<String> {
820    if !sql.trim_start().to_uppercase().starts_with("CREATE TABLE") {
821        return None;
822    }
823    let open = sql.find('(')?;
824    let mut depth = 1;
825    let mut in_string = false;
826    let mut in_ident = false;
827    let mut items: Vec<&str> = vec![];
828    let mut item_start = open + 1;
829    let mut close = None;
830    for (i, c) in sql[open + 1..].char_indices() {
831        let i = open + 1 + i;
832        match c {
833            '\'' if !in_ident => in_string = !in_string,
834            '"' if !in_string => in_ident = !in_ident,
835            _ if in_string || in_ident => (),
836            '(' => depth += 1,
837            ')' => {
838                depth -= 1;
839                if depth == 0 {
840                    close = Some(i);
841                    break;
842                }
843            }
844            ',' if depth == 1 => {
845                items.push(&sql[item_start..i]);
846                item_start = i + 1;
847            }
848            _ => (),
849        }
850    }
851    let close = close?;
852    items.push(&sql[item_start..close]);
853    fn first_word(s: &str) -> String {
854        s.trim_start()
855            .chars()
856            .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
857            .collect::<String>()
858            .to_uppercase()
859    }
860    let kept: Vec<&str> = items
861        .into_iter()
862        .filter(|item| {
863            let first = first_word(item);
864            let is_physical = match first.as_str() {
865                "INDEX" | "FAMILY" => true,
866                "UNIQUE" | "INVERTED" => {
867                    let rest = &item.trim_start()[first.len()..];
868                    first_word(rest) == "INDEX"
869                }
870                _ => false,
871            };
872            !is_physical
873        })
874        .collect();
875    Some(format!(
876        "{}{}{}",
877        &sql[..open + 1],
878        kept.join(","),
879        &sql[close..]
880    ))
881}
882
883impl<'a> Runner<'a> {
884    pub async fn start(config: &'a RunConfig<'a>) -> Result<Runner<'a>, anyhow::Error> {
885        let mut runner = Self {
886            config,
887            inner: None,
888            replacements: Vec::new(),
889        };
890        runner.reset().await?;
891        Ok(runner)
892    }
893
894    pub async fn reset(&mut self) -> Result<(), anyhow::Error> {
895        // Explicitly drop the old runner here to ensure that we wait for threads to terminate
896        // before starting a new runner
897        drop(self.inner.take());
898        self.inner = Some(RunnerInner::start(self.config).await?);
899
900        Ok(())
901    }
902
903    async fn run_record<'r>(
904        &mut self,
905        record: &'r Record<'r>,
906        in_transaction: &mut bool,
907    ) -> Result<Outcome<'r>, anyhow::Error> {
908        if let Record::ResetServer = record {
909            self.reset().await?;
910            Ok(Outcome::Success)
911        } else if let Record::Replace {
912            pattern,
913            replacement,
914        } = record
915        {
916            // Validated at parse time, so this compile cannot fail.
917            let regex = Regex::new(pattern).expect("replace regex validated by parser");
918            self.replacements.push((regex, replacement.clone()));
919            Ok(Outcome::Success)
920        } else {
921            self.inner
922                .as_mut()
923                .expect("RunnerInner missing")
924                .run_record(record, in_transaction, &self.replacements)
925                .await
926        }
927    }
928
929    async fn check_catalog(&self) -> Result<(), anyhow::Error> {
930        self.inner
931            .as_ref()
932            .expect("RunnerInner missing")
933            .check_catalog()
934            .await
935    }
936
937    #[allow(clippy::disallowed_methods)]
938    async fn reset_database(&mut self) -> Result<(), anyhow::Error> {
939        let inner = self.inner.as_mut().expect("RunnerInner missing");
940
941        inner.client.batch_execute("ROLLBACK;").await?;
942
943        inner
944            .system_client
945            .batch_execute(
946                "ROLLBACK;
947                 SET cluster = mz_catalog_server;
948                 RESET cluster_replica;",
949            )
950            .await?;
951
952        inner
953            .system_client
954            .batch_execute("ALTER SYSTEM RESET ALL")
955            .await?;
956
957        // Drop all databases, then recreate the `materialize` database.
958        for row in inner
959            .system_client
960            .query("SELECT name FROM mz_databases", &[])
961            .await?
962        {
963            let name: &str = row.get("name");
964            inner
965                .system_client
966                .batch_execute(sql!("DROP DATABASE {}", Sql::ident(name)).as_str())
967                .await?;
968        }
969
970        inner
971            .system_client
972            .batch_execute("CREATE DATABASE materialize")
973            .await?;
974
975        // Ensure quickstart cluster exists with one replica of size `self.config.replica_size`.
976        // We don't destroy the existing quickstart cluster replica if it exists, as turning
977        // on a cluster replica is exceptionally slow.
978        let mut needs_default_cluster = true;
979        for row in inner
980            .system_client
981            .query("SELECT name FROM mz_clusters WHERE id LIKE 'u%'", &[])
982            .await?
983        {
984            match row.get("name") {
985                "quickstart" => needs_default_cluster = false,
986                name => {
987                    inner
988                        .system_client
989                        .batch_execute(sql!("DROP CLUSTER {}", Sql::ident(name)).as_str())
990                        .await?
991                }
992            }
993        }
994        if needs_default_cluster {
995            inner
996                .system_client
997                .batch_execute("CREATE CLUSTER quickstart REPLICAS ()")
998                .await?;
999        }
1000        let mut needs_default_replica = false;
1001        let rows = inner
1002            .system_client
1003            .query(
1004                "SELECT name, size FROM mz_cluster_replicas
1005                 WHERE cluster_id = (SELECT id FROM mz_clusters WHERE name = 'quickstart')
1006                 ORDER BY name",
1007                &[],
1008            )
1009            .await?;
1010        if rows.len() != self.config.replicas {
1011            needs_default_replica = true;
1012        } else {
1013            for (i, row) in rows.iter().enumerate() {
1014                let name: &str = row.get("name");
1015                let size: &str = row.get("size");
1016                if name != format!("r{}", i + 1) || size != self.config.replica_size {
1017                    needs_default_replica = true;
1018                    break;
1019                }
1020            }
1021        }
1022
1023        if needs_default_replica {
1024            inner
1025                .system_client
1026                .batch_execute("ALTER CLUSTER quickstart SET (MANAGED = false)")
1027                .await?;
1028            for row in inner
1029                .system_client
1030                .query(
1031                    "SELECT name FROM mz_cluster_replicas
1032                     WHERE cluster_id = (SELECT id FROM mz_clusters WHERE name = 'quickstart')",
1033                    &[],
1034                )
1035                .await?
1036            {
1037                let name: &str = row.get("name");
1038                inner
1039                    .system_client
1040                    .batch_execute(
1041                        sql!("DROP CLUSTER REPLICA quickstart.{}", Sql::ident(name)).as_str(),
1042                    )
1043                    .await?;
1044            }
1045            for i in 1..=self.config.replicas {
1046                inner
1047                    .system_client
1048                    .batch_execute(
1049                        sql!(
1050                            "CREATE CLUSTER REPLICA quickstart.r{} SIZE {}",
1051                            i,
1052                            Sql::literal(&self.config.replica_size)
1053                        )
1054                        .as_str(),
1055                    )
1056                    .await?;
1057            }
1058            inner
1059                .system_client
1060                .batch_execute("ALTER CLUSTER quickstart SET (MANAGED = true)")
1061                .await?;
1062        }
1063
1064        // Drop all user roles except the default one, so that roles created
1065        // via the `user` directive or by the tests themselves do not leak
1066        // into later files. Best-effort: a role that still owns objects (for
1067        // example through default privileges) stays behind rather than
1068        // failing the reset.
1069        for row in inner
1070            .system_client
1071            .query(
1072                "SELECT name FROM mz_roles WHERE id LIKE 'u%' AND name != 'materialize'",
1073                &[],
1074            )
1075            .await?
1076        {
1077            let name: &str = row.get("name");
1078            let _ = inner
1079                .system_client
1080                .batch_execute(sql!("DROP ROLE {}", Sql::ident(name)).as_str())
1081                .await;
1082        }
1083
1084        // Grant initial privileges.
1085        inner
1086            .system_client
1087            .batch_execute("GRANT USAGE ON DATABASE materialize TO PUBLIC")
1088            .await?;
1089        inner
1090            .system_client
1091            .batch_execute("GRANT CREATE ON DATABASE materialize TO materialize")
1092            .await?;
1093        inner
1094            .system_client
1095            .batch_execute("GRANT CREATE ON SCHEMA materialize.public TO materialize")
1096            .await?;
1097        inner
1098            .system_client
1099            .batch_execute("GRANT USAGE ON CLUSTER quickstart TO PUBLIC")
1100            .await?;
1101        inner
1102            .system_client
1103            .batch_execute("GRANT CREATE ON CLUSTER quickstart TO materialize")
1104            .await?;
1105
1106        // Some sqllogictests require more than the default amount of tables, so we increase the
1107        // limit for all tests.
1108        inner
1109            .system_client
1110            .simple_query("ALTER SYSTEM SET max_tables = 100")
1111            .await?;
1112
1113        if inner.enable_table_keys {
1114            inner
1115                .system_client
1116                .simple_query("ALTER SYSTEM SET unsafe_enable_table_keys = true")
1117                .await?;
1118        }
1119
1120        inner.ensure_fixed_features().await?;
1121
1122        inner.client = connect(inner.server_addr, None, None).await.unwrap();
1123        inner.system_client = connect(inner.internal_server_addr, Some("mz_system"), None)
1124            .await
1125            .unwrap();
1126        inner.clients = BTreeMap::new();
1127        inner.active_user = None;
1128
1129        Ok(())
1130    }
1131}
1132
1133impl<'a> RunnerInner<'a> {
1134    pub async fn start(config: &RunConfig<'a>) -> Result<RunnerInner<'a>, anyhow::Error> {
1135        let temp_dir = tempfile::tempdir()?;
1136        let scratch_dir = tempfile::tempdir()?;
1137        let environment_id = EnvironmentId::for_tests();
1138        let (consensus_uri, timestamp_oracle_url): (SensitiveUrl, SensitiveUrl) = {
1139            let postgres_url = &config.postgres_url;
1140            let prefix = &config.prefix;
1141            info!(%postgres_url, "starting server");
1142            let (client, conn) = Retry::default()
1143                .max_tries(5)
1144                .retry_async(|_| async {
1145                    match tokio_postgres::connect(postgres_url, NoTls).await {
1146                        Ok(c) => Ok(c),
1147                        Err(e) => {
1148                            error!(%e, "failed to connect to postgres");
1149                            Err(e)
1150                        }
1151                    }
1152                })
1153                .await?;
1154            task::spawn(|| "sqllogictest_connect", async move {
1155                if let Err(e) = conn.await {
1156                    panic!("connection error: {}", e);
1157                }
1158            });
1159            // `prefix` is generated by sqllogictest harness configuration and
1160            // cannot be represented with composable `Sql`.
1161            #[allow(clippy::disallowed_methods)]
1162            client
1163                .batch_execute(&format!(
1164                    "DROP SCHEMA IF EXISTS {prefix}_tsoracle CASCADE;
1165                     CREATE SCHEMA IF NOT EXISTS {prefix}_consensus;
1166                     CREATE SCHEMA {prefix}_tsoracle;"
1167                ))
1168                .await?;
1169            (
1170                format!("{postgres_url}?options=--search_path={prefix}_consensus")
1171                    .parse()
1172                    .expect("invalid consensus URI"),
1173                format!("{postgres_url}?options=--search_path={prefix}_tsoracle")
1174                    .parse()
1175                    .expect("invalid timestamp oracle URI"),
1176            )
1177        };
1178
1179        let secrets_dir = temp_dir.path().join("secrets");
1180        let orchestrator = Arc::new(
1181            ProcessOrchestrator::new(ProcessOrchestratorConfig {
1182                image_dir: env::current_exe()?.parent().unwrap().to_path_buf(),
1183                suppress_output: false,
1184                environment_id: environment_id.to_string(),
1185                secrets_dir: secrets_dir.clone(),
1186                command_wrapper: config
1187                    .orchestrator_process_wrapper
1188                    .as_ref()
1189                    .map_or(Ok(vec![]), |s| shell_words::split(s))?,
1190                propagate_crashes: true,
1191                tcp_proxy: None,
1192                scratch_directory: scratch_dir.path().to_path_buf(),
1193            })
1194            .await?,
1195        );
1196        let now = SYSTEM_TIME.clone();
1197        let metrics_registry = MetricsRegistry::new();
1198
1199        let persist_config = PersistConfig::new(
1200            &mz_environmentd::BUILD_INFO,
1201            now.clone(),
1202            mz_dyncfgs::all_dyncfgs(),
1203        );
1204        let persist_pubsub_server =
1205            PersistGrpcPubSubServer::new(&persist_config, &metrics_registry);
1206        let persist_pubsub_client = persist_pubsub_server.new_same_process_connection();
1207        let persist_pubsub_tcp_listener =
1208            TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
1209                .await
1210                .expect("pubsub addr binding");
1211        let persist_pubsub_server_port = persist_pubsub_tcp_listener
1212            .local_addr()
1213            .expect("pubsub addr has local addr")
1214            .port();
1215        info!("listening for persist pubsub connections on localhost:{persist_pubsub_server_port}");
1216        mz_ore::task::spawn(|| "persist_pubsub_server", async move {
1217            persist_pubsub_server
1218                .serve_with_stream(TcpListenerStream::new(persist_pubsub_tcp_listener))
1219                .await
1220                .expect("success")
1221        });
1222        let persist_clients =
1223            PersistClientCache::new(persist_config, &metrics_registry, |cfg, metrics| {
1224                let sender: Arc<dyn PubSubSender> = Arc::new(MetricsSameProcessPubSubSender::new(
1225                    cfg,
1226                    persist_pubsub_client.sender,
1227                    metrics,
1228                ));
1229                PubSubClientConnection::new(sender, persist_pubsub_client.receiver)
1230            });
1231        let persist_clients = Arc::new(persist_clients);
1232
1233        let secrets_controller = Arc::clone(&orchestrator);
1234        let connection_context = ConnectionContext::for_tests(orchestrator.reader());
1235        let orchestrator = Arc::new(TracingOrchestrator::new(
1236            orchestrator,
1237            config.tracing.clone(),
1238        ));
1239        let listeners_config = ListenersConfig {
1240            sql: btreemap! {
1241                "external".to_owned() => SqlListenerConfig {
1242                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1243                    authenticator_kind: AuthenticatorKind::None,
1244                    allowed_roles: AllowedRoles::Normal,
1245                    enable_tls: false,
1246                },
1247                "internal".to_owned() => SqlListenerConfig {
1248                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1249                    authenticator_kind: AuthenticatorKind::None,
1250                    allowed_roles: AllowedRoles::Internal,
1251                    enable_tls: false,
1252                },
1253                "password".to_owned() => SqlListenerConfig {
1254                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1255                    authenticator_kind: AuthenticatorKind::Password,
1256                    allowed_roles: AllowedRoles::Normal,
1257                    enable_tls: false,
1258                },
1259            },
1260            http: btreemap![
1261                "external".to_owned() => HttpListenerConfig {
1262                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1263                    authenticator_kind: AuthenticatorKind::None,
1264                    enable_tls: false,
1265                    routes: HttpRoutesEnabled {
1266                        base: RouteGroup::Enabled(AllowedRoles::Normal),
1267                        webhook: RouteGroup::Enabled(AllowedRoles::Normal),
1268                        internal: RouteGroup::Disabled,
1269                        metrics: RouteGroup::Disabled,
1270                        profiling: RouteGroup::Disabled,
1271                        mcp_agent: RouteGroup::Disabled,
1272                        mcp_developer: RouteGroup::Disabled,
1273                        console_config: RouteGroup::Enabled(AllowedRoles::Normal),
1274                    },
1275                },
1276                "internal".to_owned() => HttpListenerConfig {
1277                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1278                    authenticator_kind: AuthenticatorKind::None,
1279                    enable_tls: false,
1280                    routes: HttpRoutesEnabled {
1281                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1282                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1283                        internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1284                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1285                        profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1286                        mcp_agent: RouteGroup::Disabled,
1287                        mcp_developer: RouteGroup::Disabled,
1288                        console_config: RouteGroup::Disabled,
1289                    },
1290                },
1291            ],
1292        };
1293        let listeners = mz_environmentd::Listeners::bind(listeners_config).await?;
1294        let host_name = format!(
1295            "localhost:{}",
1296            listeners.http["external"].handle.local_addr.port()
1297        );
1298        let catalog_config = CatalogConfig {
1299            persist_clients: Arc::clone(&persist_clients),
1300            metrics: Arc::new(mz_catalog::durable::Metrics::new(&MetricsRegistry::new())),
1301        };
1302        let system_dyncfgs = Arc::clone(&persist_clients.cfg().configs);
1303        let server_config = mz_environmentd::Config {
1304            catalog_config,
1305            timestamp_oracle_url: Some(timestamp_oracle_url),
1306            controller: ControllerConfig {
1307                build_info: &mz_environmentd::BUILD_INFO,
1308                orchestrator,
1309                clusterd_image: "clusterd".into(),
1310                init_container_image: None,
1311                deploy_generation: 0,
1312                persist_location: PersistLocation {
1313                    blob_uri: format!(
1314                        "file://{}/persist/blob",
1315                        config.persist_dir.path().display()
1316                    )
1317                    .parse()
1318                    .expect("invalid blob URI"),
1319                    consensus_uri,
1320                },
1321                persist_clients,
1322                now: SYSTEM_TIME.clone(),
1323                metrics_registry: metrics_registry.clone(),
1324                persist_pubsub_url: format!("http://localhost:{}", persist_pubsub_server_port),
1325                secrets_args: mz_service::secrets::SecretsReaderCliArgs {
1326                    secrets_reader: mz_service::secrets::SecretsControllerKind::LocalFile,
1327                    secrets_reader_local_file_dir: Some(secrets_dir),
1328                    secrets_reader_kubernetes_context: None,
1329                    secrets_reader_aws_prefix: None,
1330                    secrets_reader_name_prefix: None,
1331                },
1332                connection_context,
1333                replica_http_locator: Arc::new(ReplicaHttpLocator::default()),
1334            },
1335            secrets_controller,
1336            cloud_resource_controller: None,
1337            system_dyncfgs,
1338            tls: None,
1339            frontegg: None,
1340            frontegg_oauth_issuer_url: None,
1341            cors_allowed_origin: AllowOrigin::list([]),
1342            cors_allowed_origin_list: Vec::new(),
1343            unsafe_mode: true,
1344            all_features: false,
1345            metrics_registry,
1346            now,
1347            environment_id,
1348            cluster_replica_sizes: ClusterReplicaSizeMap::for_tests(),
1349            bootstrap_default_cluster_replica_size: config.replica_size.clone(),
1350            bootstrap_default_cluster_replication_factor: config
1351                .replicas
1352                .try_into()
1353                .expect("replicas must fit"),
1354            bootstrap_builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
1355                replication_factor: SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1356                size: config.replica_size.clone(),
1357            },
1358            bootstrap_builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
1359                replication_factor: CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1360                size: config.replica_size.clone(),
1361            },
1362            bootstrap_builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
1363                replication_factor: PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1364                size: config.replica_size.clone(),
1365            },
1366            bootstrap_builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
1367                replication_factor: SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1368                size: config.replica_size.clone(),
1369            },
1370            bootstrap_builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
1371                replication_factor: ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1372                size: config.replica_size.clone(),
1373            },
1374            system_parameter_defaults: {
1375                let mut params = BTreeMap::new();
1376                params.insert(
1377                    "log_filter".to_string(),
1378                    config.tracing.startup_log_filter.to_string(),
1379                );
1380                params.extend(config.system_parameter_defaults.clone());
1381                params
1382            },
1383            availability_zones: Default::default(),
1384            tracing_handle: config.tracing_handle.clone(),
1385            storage_usage_collection_interval: Duration::from_secs(3600),
1386            storage_usage_retention_period: None,
1387            segment_api_key: None,
1388            segment_client_side: false,
1389            // SLT doesn't like eternally running tasks since it waits for them to finish inbetween SLT files
1390            test_only_dummy_segment_client: false,
1391            egress_addresses: vec![],
1392            aws_account_id: None,
1393            aws_privatelink_availability_zones: None,
1394            launchdarkly_sdk_key: None,
1395            launchdarkly_base_uri: None,
1396            launchdarkly_key_map: Default::default(),
1397            config_sync_file_path: None,
1398            config_sync_timeout: Duration::from_secs(30),
1399            config_sync_loop_interval: None,
1400            bootstrap_role: Some("materialize".into()),
1401            http_host_name: Some(host_name),
1402            internal_console_redirect_url: None,
1403            tls_reload_certs: mz_server_core::cert_reload_never_reload(),
1404            helm_chart_version: None,
1405            license_key: ValidatedLicenseKey::for_tests(),
1406            external_login_password_mz_system: None,
1407            force_builtin_schema_migration: None,
1408        };
1409        // We need to run the server on its own Tokio runtime, which in turn
1410        // requires its own thread, so that we can wait for any tasks spawned
1411        // by the server to be shutdown at the end of each file. If we were to
1412        // share a Tokio runtime, tasks from the last file's server would still
1413        // be live at the start of the next file's server.
1414        let (server_addr_tx, server_addr_rx): (oneshot::Sender<Result<_, anyhow::Error>>, _) =
1415            oneshot::channel();
1416        let (internal_server_addr_tx, internal_server_addr_rx) = oneshot::channel();
1417        let (password_server_addr_tx, password_server_addr_rx) = oneshot::channel();
1418        let (internal_http_server_addr_tx, internal_http_server_addr_rx) = oneshot::channel();
1419        let (shutdown_trigger, shutdown_trigger_rx) = trigger::channel();
1420        let server_thread = thread::spawn(|| {
1421            let runtime = match Runtime::new() {
1422                Ok(runtime) => runtime,
1423                Err(e) => {
1424                    server_addr_tx
1425                        .send(Err(e.into()))
1426                        .expect("receiver should not drop first");
1427                    return;
1428                }
1429            };
1430            let server = match runtime.block_on(listeners.serve(server_config)) {
1431                Ok(runtime) => runtime,
1432                Err(e) => {
1433                    server_addr_tx
1434                        .send(Err(e.into()))
1435                        .expect("receiver should not drop first");
1436                    return;
1437                }
1438            };
1439            server_addr_tx
1440                .send(Ok(server.sql_listener_handles["external"].local_addr))
1441                .expect("receiver should not drop first");
1442            internal_server_addr_tx
1443                .send(server.sql_listener_handles["internal"].local_addr)
1444                .expect("receiver should not drop first");
1445            password_server_addr_tx
1446                .send(server.sql_listener_handles["password"].local_addr)
1447                .expect("receiver should not drop first");
1448            internal_http_server_addr_tx
1449                .send(server.http_listener_handles["internal"].local_addr)
1450                .expect("receiver should not drop first");
1451            runtime.block_on(shutdown_trigger_rx);
1452        });
1453        let server_addr = server_addr_rx.await??;
1454        let internal_server_addr = internal_server_addr_rx.await?;
1455        let password_server_addr = password_server_addr_rx.await?;
1456        let internal_http_server_addr = internal_http_server_addr_rx.await?;
1457
1458        let system_client = connect(internal_server_addr, Some("mz_system"), None)
1459            .await
1460            .unwrap();
1461        let client = connect(server_addr, None, None).await.unwrap();
1462
1463        let inner = RunnerInner {
1464            server_addr,
1465            internal_server_addr,
1466            password_server_addr,
1467            internal_http_server_addr,
1468            _shutdown_trigger: shutdown_trigger,
1469            _server_thread: server_thread.join_on_drop(),
1470            _temp_dir: temp_dir,
1471            client,
1472            system_client,
1473            clients: BTreeMap::new(),
1474            active_user: None,
1475            auto_index_tables: config.auto_index_tables,
1476            auto_index_selects: config.auto_index_selects,
1477            auto_transactions: config.auto_transactions,
1478            enable_table_keys: config.enable_table_keys,
1479            verbose: config.verbose,
1480            stdout: config.stdout,
1481        };
1482        inner.ensure_fixed_features().await?;
1483
1484        Ok(inner)
1485    }
1486
1487    /// Set features that should be enabled regardless of whether reset-server was
1488    /// called. These features may be set conditionally depending on the run configuration.
1489    #[allow(clippy::disallowed_methods)]
1490    async fn ensure_fixed_features(&self) -> Result<(), anyhow::Error> {
1491        // We turn on enable_reduce_mfp_fusion, as we wish
1492        // to get as much coverage of these features as we can.
1493        // TODO(vmarcos): Remove this code when we retire this feature flag.
1494        self.system_client
1495            .execute("ALTER SYSTEM SET enable_reduce_mfp_fusion = on", &[])
1496            .await?;
1497
1498        // Dangerous functions are useful for tests so we enable it for all tests.
1499        self.system_client
1500            .execute("ALTER SYSTEM SET unsafe_enable_unsafe_functions = on", &[])
1501            .await?;
1502        Ok(())
1503    }
1504
1505    /// The connection records run on: the `user` directive's connection when
1506    /// one is active, the default connection otherwise.
1507    fn active_client(&self) -> &tokio_postgres::Client {
1508        match &self.active_user {
1509            Some(user) => self
1510                .clients
1511                .get(user)
1512                .expect("connection for the active user exists"),
1513            None => &self.client,
1514        }
1515    }
1516
1517    /// Handles a `user` directive: ensures the role exists, connects as it,
1518    /// and makes it the active user. CockroachDB's `root` maps to the
1519    /// default `materialize` user.
1520    #[allow(clippy::disallowed_methods)]
1521    async fn run_user<'r>(
1522        &mut self,
1523        user: &'r str,
1524        location: &Location,
1525        in_transaction: &mut bool,
1526    ) -> Result<Outcome<'r>, anyhow::Error> {
1527        if self.auto_transactions && *in_transaction {
1528            self.active_client().execute("COMMIT", &[]).await?;
1529            *in_transaction = false;
1530        }
1531        if user == "root" || user == "materialize" {
1532            self.active_user = None;
1533            return Ok(Outcome::Success);
1534        }
1535        if !self.clients.contains_key(user) {
1536            let create = sql!("CREATE ROLE {}", Sql::ident(user));
1537            if let Err(error) = self.system_client.batch_execute(create.as_str()).await {
1538                if !error.to_string_with_causes().contains("already exists") {
1539                    return Ok(Outcome::Bail {
1540                        cause: Box::new(Outcome::PlanFailure {
1541                            error: anyhow!(error),
1542                            expected_error: None,
1543                            location: location.clone(),
1544                        }),
1545                        location: location.clone(),
1546                    });
1547                }
1548            }
1549            match connect(self.server_addr, Some(user), None).await {
1550                Ok(client) => {
1551                    self.clients.insert(user.to_string(), client);
1552                }
1553                Err(error) => {
1554                    return Ok(Outcome::Bail {
1555                        cause: Box::new(Outcome::PlanFailure {
1556                            error: anyhow!(error),
1557                            expected_error: None,
1558                            location: location.clone(),
1559                        }),
1560                        location: location.clone(),
1561                    });
1562                }
1563            }
1564        }
1565        self.active_user = Some(user.to_string());
1566        Ok(Outcome::Success)
1567    }
1568
1569    #[allow(clippy::disallowed_methods)]
1570    async fn run_record<'r>(
1571        &mut self,
1572        record: &'r Record<'r>,
1573        in_transaction: &mut bool,
1574        replacements: &[(Regex, String)],
1575    ) -> Result<Outcome<'r>, anyhow::Error> {
1576        match &record {
1577            Record::User { user, location } => self.run_user(user, location, in_transaction).await,
1578            Record::Statement {
1579                expected_error,
1580                rows_affected,
1581                sql,
1582                location,
1583            } => {
1584                if self.auto_transactions && *in_transaction {
1585                    self.active_client().execute("COMMIT", &[]).await?;
1586                    *in_transaction = false;
1587                }
1588                match self
1589                    .run_statement(*expected_error, *rows_affected, sql, location.clone())
1590                    .await?
1591                {
1592                    Outcome::Success => {
1593                        if self.auto_index_tables {
1594                            let additional = mutate(sql);
1595                            for stmt in additional {
1596                                self.active_client().execute(&stmt, &[]).await?;
1597                            }
1598                        }
1599                        Ok(Outcome::Success)
1600                    }
1601                    other => {
1602                        if expected_error.is_some() {
1603                            Ok(other)
1604                        } else {
1605                            // If we failed to execute a statement that was supposed to succeed,
1606                            // running the rest of the tests in this file will probably cause
1607                            // false positives, so just give up on the file entirely.
1608                            Ok(Outcome::Bail {
1609                                cause: Box::new(other),
1610                                location: location.clone(),
1611                            })
1612                        }
1613                    }
1614                }
1615            }
1616            Record::Query {
1617                sql,
1618                output,
1619                location,
1620            } => {
1621                self.run_query(sql, output, location.clone(), in_transaction, replacements)
1622                    .await
1623            }
1624            Record::Simple {
1625                conn,
1626                user,
1627                password,
1628                sql,
1629                sort,
1630                output,
1631                location,
1632                ..
1633            } => {
1634                self.run_simple(
1635                    *conn,
1636                    *user,
1637                    *password,
1638                    sql,
1639                    sort.clone(),
1640                    output,
1641                    location.clone(),
1642                )
1643                .await
1644            }
1645            Record::Copy {
1646                table_name,
1647                tsv_path,
1648            } => {
1649                let tsv = tokio::fs::read(tsv_path).await?;
1650                let copy = self
1651                    .client
1652                    .copy_in(sql!("COPY {} FROM STDIN", Sql::ident(*table_name)).as_str())
1653                    .await?;
1654                tokio::pin!(copy);
1655                copy.send(bytes::Bytes::from(tsv)).await?;
1656                copy.finish().await?;
1657                Ok(Outcome::Success)
1658            }
1659            _ => Ok(Outcome::Success),
1660        }
1661    }
1662
1663    #[allow(clippy::disallowed_methods)]
1664    async fn run_statement<'r>(
1665        &self,
1666        expected_error: Option<&'r str>,
1667        expected_rows_affected: Option<u64>,
1668        sql: &'r str,
1669        location: Location,
1670    ) -> Result<Outcome<'r>, anyhow::Error> {
1671        static UNSUPPORTED_INDEX_STATEMENT_REGEX: LazyLock<Regex> =
1672            LazyLock::new(|| Regex::new("^(CREATE UNIQUE INDEX|REINDEX)").unwrap());
1673        if UNSUPPORTED_INDEX_STATEMENT_REGEX.is_match(sql) {
1674            // sure, we totally made you an index
1675            return Ok(Outcome::Success);
1676        }
1677
1678        // CockroachDB's logic tests frequently pack multiple commands into
1679        // one statement record. The extended protocol used by `execute`
1680        // rejects those, and Materialize restricts DDL inside the implicit
1681        // transaction the simple protocol would wrap them in, so run each
1682        // command separately and report the first error. This path cannot
1683        // report an affected-row count, so records that assert one keep the
1684        // single-command path (and fail if they contain multiple commands).
1685        let commands = if expected_rows_affected.is_none() {
1686            match mz_sql::parse::parse(sql) {
1687                Ok(stmts) if stmts.len() > 1 => {
1688                    Some(stmts.iter().map(|s| s.sql.to_owned()).collect::<Vec<_>>())
1689                }
1690                Ok(_) => None,
1691                // A statement Materialize cannot parse may be a CockroachDB
1692                // `CREATE TABLE` with inline physical-layout items. Retry
1693                // with those stripped, so the semantic records that follow
1694                // can still run against the table.
1695                Err(_) => strip_crdb_table_items(sql)
1696                    .filter(|stripped| mz_sql::parse::parse(stripped).is_ok())
1697                    .map(|stripped| vec![stripped]),
1698            }
1699        } else {
1700            None
1701        };
1702        let result = match commands {
1703            Some(commands) => {
1704                let mut result = Ok(0);
1705                for command in &commands {
1706                    if let Err(error) = self.active_client().execute(command, &[]).await {
1707                        result = Err(error);
1708                        break;
1709                    }
1710                }
1711                result
1712            }
1713            None => self.active_client().execute(sql, &[]).await,
1714        };
1715        match result {
1716            Ok(actual) => {
1717                if let Some(expected_error) = expected_error {
1718                    return Ok(Outcome::UnexpectedPlanSuccess {
1719                        expected_error,
1720                        location,
1721                    });
1722                }
1723                match expected_rows_affected {
1724                    None => Ok(Outcome::Success),
1725                    Some(expected) => {
1726                        if expected != actual {
1727                            Ok(Outcome::WrongNumberOfRowsInserted {
1728                                expected_count: expected,
1729                                actual_count: actual,
1730                                location,
1731                            })
1732                        } else {
1733                            Ok(Outcome::Success)
1734                        }
1735                    }
1736                }
1737            }
1738            Err(error) => {
1739                if let Some(expected_error) = expected_error {
1740                    if error_matches(expected_error, &error.to_string_with_causes()) {
1741                        return Ok(Outcome::Success);
1742                    }
1743                    return Ok(Outcome::PlanFailure {
1744                        error: anyhow!(error),
1745                        expected_error: Some(expected_error.to_string()),
1746                        location,
1747                    });
1748                }
1749                // CockroachDB tests configure many CockroachDB-specific
1750                // session settings. Treat setting an unknown parameter as a
1751                // no-op success so the records that follow still run.
1752                static SET_STATEMENT_REGEX: LazyLock<Regex> =
1753                    LazyLock::new(|| Regex::new("(?i)^(SET|RESET) ").unwrap());
1754                if SET_STATEMENT_REGEX.is_match(sql.trim_start())
1755                    && error
1756                        .to_string_with_causes()
1757                        .contains("unrecognized configuration parameter")
1758                {
1759                    return Ok(Outcome::Success);
1760                }
1761                Ok(Outcome::PlanFailure {
1762                    error: anyhow!(error),
1763                    expected_error: None,
1764                    location,
1765                })
1766            }
1767        }
1768    }
1769
1770    #[allow(clippy::disallowed_methods)]
1771    async fn prepare_query<'r>(
1772        &self,
1773        sql: &str,
1774        output: &'r Result<QueryOutput<'_>, &'r str>,
1775        location: Location,
1776        in_transaction: &mut bool,
1777    ) -> Result<PrepareQueryOutcome<'r>, anyhow::Error> {
1778        // get statement
1779        let statements = match mz_sql::parse::parse(sql) {
1780            Ok(statements) => statements,
1781            Err(e) => match output {
1782                Ok(_) => {
1783                    return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1784                        error: e.into(),
1785                        location,
1786                    }));
1787                }
1788                Err(expected_error) => {
1789                    if error_matches(expected_error, &e.to_string_with_causes()) {
1790                        return Ok(PrepareQueryOutcome::Outcome(Outcome::Success));
1791                    } else {
1792                        return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1793                            error: e.into(),
1794                            location,
1795                        }));
1796                    }
1797                }
1798            },
1799        };
1800        // Query records in files imported from CockroachDB occasionally
1801        // contain zero or multiple statements. Fail the record rather than
1802        // the entire run.
1803        let statement = match &*statements {
1804            [statement] => &statement.ast,
1805            _ => {
1806                return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1807                    error: anyhow!("expected one statement, but got {}", statements.len()),
1808                    location,
1809                }));
1810            }
1811        };
1812        let (is_select, num_attributes, has_as_of) = match statement {
1813            Statement::Select(stmt) => (
1814                true,
1815                derive_num_attributes(&stmt.query.body),
1816                stmt.as_of.is_some(),
1817            ),
1818            _ => (false, None, false),
1819        };
1820
1821        match output {
1822            Ok(_) => {
1823                if self.auto_transactions && !*in_transaction {
1824                    // No ISOLATION LEVEL SERIALIZABLE because of database-issues#5323
1825                    self.active_client().execute("BEGIN", &[]).await?;
1826                    *in_transaction = true;
1827                }
1828            }
1829            Err(_) => {
1830                if self.auto_transactions && *in_transaction {
1831                    self.active_client().execute("COMMIT", &[]).await?;
1832                    *in_transaction = false;
1833                }
1834            }
1835        }
1836
1837        // `SHOW` commands reference catalog schema, thus are not in the same timedomain and not
1838        // allowed in the same transaction, see:
1839        // https://materialize.com/docs/sql/begin/#same-timedomain-error
1840        match statement {
1841            Statement::Show(..) => {
1842                if self.auto_transactions && *in_transaction {
1843                    self.active_client().execute("COMMIT", &[]).await?;
1844                    *in_transaction = false;
1845                }
1846            }
1847            _ => (),
1848        }
1849        Ok(PrepareQueryOutcome::QueryPrepared(QueryInfo {
1850            is_select,
1851            num_attributes,
1852            has_as_of,
1853        }))
1854    }
1855
1856    #[allow(clippy::disallowed_methods)]
1857    async fn execute_query<'r>(
1858        &self,
1859        sql: &str,
1860        output: &'r Result<QueryOutput<'_>, &'r str>,
1861        location: Location,
1862        replacements: &[(Regex, String)],
1863    ) -> Result<Outcome<'r>, anyhow::Error> {
1864        let rows = match self.active_client().query(sql, &[]).await {
1865            Ok(rows) => rows,
1866            Err(error) => {
1867                let error_string = error.to_string_with_causes();
1868                return match output {
1869                    Ok(_) => {
1870                        if error_string.contains("supported") || error_string.contains("overload") {
1871                            // this is a failure, but it's caused by lack of support rather than by bugs
1872                            Ok(Outcome::Unsupported {
1873                                error: anyhow!(error),
1874                                location,
1875                            })
1876                        } else {
1877                            Ok(Outcome::PlanFailure {
1878                                error: anyhow!(error),
1879                                expected_error: None,
1880                                location,
1881                            })
1882                        }
1883                    }
1884                    Err(expected_error) => {
1885                        if error_matches(expected_error, &error_string) {
1886                            Ok(Outcome::Success)
1887                        } else {
1888                            Ok(Outcome::PlanFailure {
1889                                error: anyhow!(error),
1890                                expected_error: Some(expected_error.to_string()),
1891                                location,
1892                            })
1893                        }
1894                    }
1895                };
1896            }
1897        };
1898
1899        // unpack expected output
1900        let QueryOutput {
1901            sort,
1902            types: expected_types,
1903            column_names: expected_column_names,
1904            output: expected_output,
1905            mode,
1906            ..
1907        } = match output {
1908            Err(expected_error) => {
1909                return Ok(Outcome::UnexpectedPlanSuccess {
1910                    expected_error,
1911                    location,
1912                });
1913            }
1914            Ok(query_output) => query_output,
1915        };
1916
1917        // format output
1918        let mut formatted_rows = vec![];
1919        for row in &rows {
1920            if row.len() != expected_types.len() {
1921                return Ok(Outcome::WrongColumnCount {
1922                    expected_count: expected_types.len(),
1923                    actual_count: row.len(),
1924                    location,
1925                });
1926            }
1927            let row = format_row(row, expected_types, *mode);
1928            formatted_rows.push(row);
1929        }
1930
1931        // sort formatted output
1932        if let Sort::Row = sort {
1933            formatted_rows.sort();
1934        }
1935        let mut values = formatted_rows.into_iter().flatten().collect::<Vec<_>>();
1936        if let Sort::Value = sort {
1937            values.sort();
1938        }
1939
1940        // Apply any `replace` substitutions to the actual output, so that
1941        // non-deterministic tokens (e.g. a folded `mz_now()` timestamp) are
1942        // masked before the output is compared against, or rewritten into, the
1943        // expected output.
1944        if !replacements.is_empty() {
1945            for value in &mut values {
1946                for (regex, replacement) in replacements {
1947                    *value = regex.replace_all(value, replacement.as_str()).into_owned();
1948                }
1949            }
1950        }
1951
1952        // Various checks as long as there are returned rows.
1953        if let Some(row) = rows.get(0) {
1954            // check column names
1955            if let Some(expected_column_names) = expected_column_names {
1956                let actual_column_names = row
1957                    .columns()
1958                    .iter()
1959                    .map(|t| ColumnName::from(t.name()))
1960                    .collect::<Vec<_>>();
1961                if expected_column_names != &actual_column_names {
1962                    return Ok(Outcome::WrongColumnNames {
1963                        expected_column_names,
1964                        actual_column_names,
1965                        actual_output: Output::Values(values),
1966                        location,
1967                    });
1968                }
1969            }
1970        }
1971
1972        // check output
1973        match expected_output {
1974            Output::Values(expected_values) => {
1975                if values != *expected_values {
1976                    return Ok(Outcome::OutputFailure {
1977                        expected_output,
1978                        actual_raw_output: rows,
1979                        actual_output: Output::Values(values),
1980                        location,
1981                    });
1982                }
1983            }
1984            Output::Hashed {
1985                num_values,
1986                md5: expected_md5,
1987            } => {
1988                let mut hasher = Md5::new();
1989                for value in &values {
1990                    hasher.update(value);
1991                    hasher.update("\n");
1992                }
1993                let md5 = format!("{:x}", hasher.finalize());
1994                if values.len() != *num_values || md5 != *expected_md5 {
1995                    return Ok(Outcome::OutputFailure {
1996                        expected_output,
1997                        actual_raw_output: rows,
1998                        actual_output: Output::Hashed {
1999                            num_values: values.len(),
2000                            md5,
2001                        },
2002                        location,
2003                    });
2004                }
2005            }
2006        }
2007
2008        Ok(Outcome::Success)
2009    }
2010
2011    #[allow(clippy::disallowed_methods)]
2012    async fn execute_view_inner<'r>(
2013        &self,
2014        sql: &str,
2015        output: &'r Result<QueryOutput<'_>, &'r str>,
2016        location: Location,
2017    ) -> Result<Option<Outcome<'r>>, anyhow::Error> {
2018        print_sql_if(self.stdout, sql, self.verbose);
2019        let sql_result = self.active_client().execute(sql, &[]).await;
2020
2021        // Evaluate if we already reached an outcome or not.
2022        let tentative_outcome = if let Err(view_error) = sql_result {
2023            if let Err(expected_error) = output {
2024                if error_matches(expected_error, &view_error.to_string_with_causes()) {
2025                    Some(Outcome::Success)
2026                } else {
2027                    Some(Outcome::PlanFailure {
2028                        error: view_error.into(),
2029                        expected_error: Some(expected_error.to_string()),
2030                        location: location.clone(),
2031                    })
2032                }
2033            } else {
2034                Some(Outcome::PlanFailure {
2035                    error: view_error.into(),
2036                    expected_error: None,
2037                    location: location.clone(),
2038                })
2039            }
2040        } else {
2041            None
2042        };
2043        Ok(tentative_outcome)
2044    }
2045
2046    #[allow(clippy::disallowed_methods)]
2047    async fn execute_view<'r>(
2048        &self,
2049        sql: &str,
2050        num_attributes: Option<usize>,
2051        output: &'r Result<QueryOutput<'_>, &'r str>,
2052        location: Location,
2053        replacements: &[(Regex, String)],
2054    ) -> Result<Outcome<'r>, anyhow::Error> {
2055        // Create indexed view SQL commands and execute `CREATE VIEW`.
2056        let expected_column_names = if let Ok(QueryOutput { column_names, .. }) = output {
2057            column_names.clone()
2058        } else {
2059            None
2060        };
2061        let (create_view, create_index, view_sql, drop_view) = generate_view_sql(
2062            sql,
2063            Uuid::new_v4().as_simple(),
2064            num_attributes,
2065            expected_column_names,
2066        );
2067        let tentative_outcome = self
2068            .execute_view_inner(create_view.as_str(), output, location.clone())
2069            .await?;
2070
2071        // Either we already have an outcome or alternatively,
2072        // we proceed to index and query the view.
2073        if let Some(view_outcome) = tentative_outcome {
2074            return Ok(view_outcome);
2075        }
2076
2077        let tentative_outcome = self
2078            .execute_view_inner(create_index.as_str(), output, location.clone())
2079            .await?;
2080
2081        let view_outcome;
2082        if let Some(outcome) = tentative_outcome {
2083            view_outcome = outcome;
2084        } else {
2085            print_sql_if(self.stdout, view_sql.as_str(), self.verbose);
2086            view_outcome = self
2087                .execute_query(view_sql.as_str(), output, location.clone(), replacements)
2088                .await?;
2089        }
2090
2091        // Remember to clean up after ourselves by dropping the view.
2092        print_sql_if(self.stdout, drop_view.as_str(), self.verbose);
2093        self.active_client()
2094            .execute(drop_view.as_str(), &[])
2095            .await?;
2096
2097        Ok(view_outcome)
2098    }
2099
2100    async fn run_query<'r>(
2101        &self,
2102        sql: &'r str,
2103        output: &'r Result<QueryOutput<'_>, &'r str>,
2104        location: Location,
2105        in_transaction: &mut bool,
2106        replacements: &[(Regex, String)],
2107    ) -> Result<Outcome<'r>, anyhow::Error> {
2108        let prepare_outcome = self
2109            .prepare_query(sql, output, location.clone(), in_transaction)
2110            .await?;
2111        match prepare_outcome {
2112            PrepareQueryOutcome::QueryPrepared(QueryInfo {
2113                is_select,
2114                num_attributes,
2115                has_as_of,
2116            }) => {
2117                let query_outcome = self
2118                    .execute_query(sql, output, location.clone(), replacements)
2119                    .await?;
2120                // `AS OF` queries are excluded from the indexed-view consistency
2121                // check: re-running them against a freshly created indexed view
2122                // is racy, since the view's `since` can advance past a historical
2123                // `AS OF` and spuriously fail with "could not find a valid
2124                // timestamp" even though the one-shot query succeeded.
2125                if is_select && !has_as_of && self.auto_index_selects && query_outcome.success() {
2126                    let view_outcome = self
2127                        .execute_view(sql, None, output, location.clone(), replacements)
2128                        .await?;
2129
2130                    if !view_outcome.success() {
2131                        // Before producing a failure outcome, we try to obtain a new
2132                        // outcome for view-based execution exploiting analysis of the
2133                        // number of attributes. This two-level strategy can avoid errors
2134                        // produced by column ambiguity in the `SELECT`.
2135                        let view_outcome = if num_attributes.is_some() {
2136                            self.execute_view(
2137                                sql,
2138                                num_attributes,
2139                                output,
2140                                location.clone(),
2141                                replacements,
2142                            )
2143                            .await?
2144                        } else {
2145                            view_outcome
2146                        };
2147
2148                        if !view_outcome.success() {
2149                            let inconsistent_view_outcome = Outcome::InconsistentViewOutcome {
2150                                query_outcome: Box::new(query_outcome),
2151                                view_outcome: Box::new(view_outcome),
2152                                location: location.clone(),
2153                            };
2154                            // Determine if this inconsistent view outcome should be reported
2155                            // as an error or only as a warning.
2156                            let outcome = if should_warn(&inconsistent_view_outcome) {
2157                                Outcome::Warning {
2158                                    cause: Box::new(inconsistent_view_outcome),
2159                                    location: location.clone(),
2160                                }
2161                            } else {
2162                                inconsistent_view_outcome
2163                            };
2164                            return Ok(outcome);
2165                        }
2166                    }
2167                }
2168                Ok(query_outcome)
2169            }
2170            PrepareQueryOutcome::Outcome(outcome) => Ok(outcome),
2171        }
2172    }
2173
2174    async fn get_conn(
2175        &mut self,
2176        name: Option<&str>,
2177        user: Option<&str>,
2178        password: Option<&str>,
2179    ) -> Result<&tokio_postgres::Client, tokio_postgres::Error> {
2180        match name {
2181            None => Ok(&self.client),
2182            Some(name) => {
2183                if !self.clients.contains_key(name) {
2184                    let addr = if matches!(user, Some("mz_system") | Some("mz_support")) {
2185                        self.internal_server_addr
2186                    } else if password.is_some() {
2187                        // Use password server for password authentication
2188                        self.password_server_addr
2189                    } else {
2190                        self.server_addr
2191                    };
2192                    let client = connect(addr, user, password).await?;
2193                    self.clients.insert(name.into(), client);
2194                }
2195                Ok(self.clients.get(name).unwrap())
2196            }
2197        }
2198    }
2199
2200    #[allow(clippy::disallowed_methods)]
2201    async fn run_simple<'r>(
2202        &mut self,
2203        conn: Option<&'r str>,
2204        user: Option<&'r str>,
2205        password: Option<&'r str>,
2206        sql: &'r str,
2207        sort: Sort,
2208        output: &'r Output,
2209        location: Location,
2210    ) -> Result<Outcome<'r>, anyhow::Error> {
2211        let actual = match self.get_conn(conn, user, password).await {
2212            Ok(client) => match client.simple_query(sql).await {
2213                Ok(result) => {
2214                    let mut rows = Vec::new();
2215
2216                    for m in result.into_iter() {
2217                        match m {
2218                            SimpleQueryMessage::Row(row) => {
2219                                let mut s = vec![];
2220                                for i in 0..row.len() {
2221                                    s.push(row.get(i).unwrap_or("NULL"));
2222                                }
2223                                rows.push(s.join(","));
2224                            }
2225                            SimpleQueryMessage::CommandComplete(count) => {
2226                                // This applies any sort on the COMPLETE line as
2227                                // well, but we do the same for the expected output.
2228                                rows.push(format!("COMPLETE {}", count));
2229                            }
2230                            SimpleQueryMessage::RowDescription(_) => {}
2231                            _ => panic!("unexpected"),
2232                        }
2233                    }
2234
2235                    if let Sort::Row = sort {
2236                        rows.sort();
2237                    }
2238
2239                    Output::Values(rows)
2240                }
2241                // Errors can contain multiple lines (say if there are details), and rewrite
2242                // sticks them each on their own line, so we need to split up the lines here to
2243                // each be its own String in the Vec.
2244                Err(error) => Output::Values(
2245                    error
2246                        .to_string_with_causes()
2247                        .lines()
2248                        .map(|s| s.to_string())
2249                        .collect(),
2250                ),
2251            },
2252            Err(error) => Output::Values(
2253                error
2254                    .to_string_with_causes()
2255                    .lines()
2256                    .map(|s| s.to_string())
2257                    .collect(),
2258            ),
2259        };
2260        if *output != actual {
2261            Ok(Outcome::OutputFailure {
2262                expected_output: output,
2263                actual_raw_output: vec![],
2264                actual_output: actual,
2265                location,
2266            })
2267        } else {
2268            Ok(Outcome::Success)
2269        }
2270    }
2271
2272    async fn check_catalog(&self) -> Result<(), anyhow::Error> {
2273        let url = format!(
2274            "http://{}/api/catalog/check",
2275            self.internal_http_server_addr
2276        );
2277        let response: serde_json::Value = reqwest::get(&url).await?.json().await?;
2278
2279        if let Some(inconsistencies) = response.get("err") {
2280            let inconsistencies = serde_json::to_string_pretty(&inconsistencies)
2281                .expect("serializing Value cannot fail");
2282            Err(anyhow::anyhow!("Catalog inconsistency\n{inconsistencies}"))
2283        } else {
2284            Ok(())
2285        }
2286    }
2287}
2288
2289async fn connect(
2290    addr: SocketAddr,
2291    user: Option<&str>,
2292    password: Option<&str>,
2293) -> Result<tokio_postgres::Client, tokio_postgres::Error> {
2294    let mut config = tokio_postgres::Config::new();
2295    config.host(addr.ip().to_string());
2296    config.port(addr.port());
2297    config.user(user.unwrap_or("materialize"));
2298    if let Some(password) = password {
2299        config.password(password);
2300    }
2301    let (client, connection) = config.connect(NoTls).await?;
2302
2303    task::spawn(|| "sqllogictest_connect", async move {
2304        if let Err(e) = connection.await {
2305            eprintln!("connection error: {}", e);
2306        }
2307    });
2308    Ok(client)
2309}
2310
2311pub trait WriteFmt {
2312    fn write_fmt(&self, fmt: fmt::Arguments<'_>);
2313}
2314
2315pub struct RunConfig<'a> {
2316    pub stdout: &'a dyn WriteFmt,
2317    pub stderr: &'a dyn WriteFmt,
2318    pub verbose: bool,
2319    pub quiet: bool,
2320    pub postgres_url: String,
2321    pub prefix: String,
2322    pub no_fail: bool,
2323    pub fail_fast: bool,
2324    pub auto_index_tables: bool,
2325    pub auto_index_selects: bool,
2326    pub auto_transactions: bool,
2327    pub enable_table_keys: bool,
2328    pub orchestrator_process_wrapper: Option<String>,
2329    pub tracing: TracingCliArgs,
2330    pub tracing_handle: TracingHandle,
2331    pub system_parameter_defaults: BTreeMap<String, String>,
2332    /// Persist state is handled specially because:
2333    /// - Persist background workers do not necessarily shut down immediately once the server is
2334    ///   shut down, and may panic if their storage is deleted out from under them.
2335    /// - It's safe for different databases to reference the same state: all data is scoped by UUID.
2336    pub persist_dir: TempDir,
2337    pub replicas: usize,
2338    pub replica_size: String,
2339}
2340
2341/// Indentation used for verbose output of SQL statements.
2342const PRINT_INDENT: usize = 4;
2343
2344fn print_record(config: &RunConfig<'_>, record: &Record) {
2345    match record {
2346        Record::Statement { sql, .. } | Record::Query { sql, .. } => {
2347            print_sql(config.stdout, sql, None)
2348        }
2349        Record::Simple { conn, sql, .. } => print_sql(config.stdout, sql, *conn),
2350        Record::Copy {
2351            table_name,
2352            tsv_path,
2353        } => {
2354            writeln!(
2355                config.stdout,
2356                "{}slt copy {} from {}",
2357                " ".repeat(PRINT_INDENT),
2358                table_name,
2359                tsv_path
2360            )
2361        }
2362        Record::User { user, .. } => {
2363            writeln!(config.stdout, "{}user {}", " ".repeat(PRINT_INDENT), user)
2364        }
2365        Record::ResetServer => {
2366            writeln!(config.stdout, "{}reset-server", " ".repeat(PRINT_INDENT))
2367        }
2368        Record::Halt => {
2369            writeln!(config.stdout, "{}halt", " ".repeat(PRINT_INDENT))
2370        }
2371        Record::HashThreshold { threshold } => {
2372            writeln!(
2373                config.stdout,
2374                "{}hash-threshold {}",
2375                " ".repeat(PRINT_INDENT),
2376                threshold
2377            )
2378        }
2379        Record::Replace {
2380            pattern,
2381            replacement,
2382        } => {
2383            writeln!(
2384                config.stdout,
2385                "{}replace {}  {}",
2386                " ".repeat(PRINT_INDENT),
2387                pattern,
2388                replacement
2389            )
2390        }
2391    }
2392}
2393
2394fn print_sql_if<'a>(stdout: &'a dyn WriteFmt, sql: &str, cond: bool) {
2395    if cond {
2396        print_sql(stdout, sql, None)
2397    }
2398}
2399
2400fn print_sql<'a>(stdout: &'a dyn WriteFmt, sql: &str, conn: Option<&str>) {
2401    let text = if let Some(conn) = conn {
2402        format!("[conn={}] {}", conn, sql)
2403    } else {
2404        sql.to_string()
2405    };
2406    writeln!(stdout, "{}", util::indent(&text, PRINT_INDENT))
2407}
2408
2409/// Regular expressions for matching error messages that should force a plan failure
2410/// in an inconsistent view outcome into a warning if the corresponding query succeeds.
2411const INCONSISTENT_VIEW_OUTCOME_WARNING_REGEXPS: [&str; 9] = [
2412    // The following are unfixable errors in indexed views given our
2413    // current constraints.
2414    "cannot materialize call to",
2415    "SHOW commands are not allowed in views",
2416    "cannot create view with unstable dependencies",
2417    "cannot use wildcard expansions or NATURAL JOINs in a view that depends on system objects",
2418    "no valid schema selected",
2419    r#"system schema '\w+' cannot be modified"#,
2420    r#"permission denied for (SCHEMA|CLUSTER) "(\w+\.)?\w+""#,
2421    // NOTE(vmarcos): Column ambiguity that could not be eliminated by our
2422    // currently implemented syntactic rewrites is considered unfixable.
2423    // In addition, if some column cannot be dealt with, e.g., in `ORDER BY`
2424    // references, we treat this condition as unfixable as well.
2425    r#"column "[\w\?]+" specified more than once"#,
2426    r#"column "(\w+\.)?\w+" does not exist"#,
2427];
2428
2429/// Evaluates if the given outcome should be returned directly or if it should
2430/// be wrapped as a warning. Note that this function should be used for outcomes
2431/// that can be judged in a context-independent manner, i.e., the outcome itself
2432/// provides enough information as to whether a warning should be emitted or not.
2433fn should_warn(outcome: &Outcome) -> bool {
2434    match outcome {
2435        Outcome::InconsistentViewOutcome { view_outcome, .. } => match view_outcome.as_ref() {
2436            Outcome::PlanFailure { error, .. } => {
2437                INCONSISTENT_VIEW_OUTCOME_WARNING_REGEXPS.iter().any(|s| {
2438                    Regex::new(s)
2439                        .expect("unexpected error in regular expression parsing")
2440                        .is_match(&error.to_string_with_causes())
2441                })
2442            }
2443            _ => false,
2444        },
2445        _ => false,
2446    }
2447}
2448
2449pub async fn run_string(
2450    runner: &mut Runner<'_>,
2451    source: &str,
2452    input: &str,
2453) -> Result<Outcomes, anyhow::Error> {
2454    runner.reset_database().await?;
2455    // `replace` directives are scoped to a single file; clear any registered by
2456    // a previous file so they do not leak into this one.
2457    runner.replacements.clear();
2458
2459    let mut outcomes = Outcomes::default();
2460    let mut parser = crate::parser::Parser::new(source, input);
2461    // Transactions are currently relatively slow. Since sqllogictest runs in a single connection
2462    // there should be no difference in having longer running transactions.
2463    let mut in_transaction = false;
2464    writeln!(runner.config.stdout, "--- {}", source);
2465
2466    for record in parser.parse_records()? {
2467        // In maximal-verbose mode, print the query before attempting to run
2468        // it. Running the query might panic, so it is important to print out
2469        // what query we are trying to run *before* we panic.
2470        if runner.config.verbose {
2471            print_record(runner.config, &record);
2472        }
2473
2474        let outcome = runner
2475            .run_record(&record, &mut in_transaction)
2476            .await
2477            .map_err(|err| format!("In {}:\n{}", source, err))
2478            .unwrap();
2479
2480        // Print warnings and failures in verbose mode.
2481        if !runner.config.quiet && !outcome.success() {
2482            if !runner.config.verbose {
2483                // If `verbose` is enabled, we'll already have printed the record,
2484                // so don't print it again. Yes, this is an ugly bit of logic.
2485                // Please don't try to consolidate it with the `print_record`
2486                // call above, as it's important to have a mode in which records
2487                // are printed before they are run, so that if running the
2488                // record panics, you can tell which record caused it.
2489                if !outcome.failure() {
2490                    writeln!(
2491                        runner.config.stdout,
2492                        "{}",
2493                        util::indent("Warning detected for: ", 4)
2494                    );
2495                }
2496                print_record(runner.config, &record);
2497            }
2498            if runner.config.verbose || outcome.failure() {
2499                writeln!(
2500                    runner.config.stdout,
2501                    "{}",
2502                    util::indent(&outcome.to_string(), 4)
2503                );
2504                writeln!(runner.config.stdout, "{}", util::indent("----", 4));
2505            }
2506        }
2507
2508        outcomes.stats[outcome.code()] += 1;
2509        if outcome.failure() {
2510            outcomes.details.push(format!("{}", outcome));
2511        }
2512
2513        if let Outcome::Bail { .. } = outcome {
2514            break;
2515        }
2516
2517        if runner.config.fail_fast && outcome.failure() {
2518            break;
2519        }
2520    }
2521    Ok(outcomes)
2522}
2523
2524pub async fn run_file(runner: &mut Runner<'_>, filename: &Path) -> Result<Outcomes, anyhow::Error> {
2525    let mut input = String::new();
2526    File::open(filename)?.read_to_string(&mut input)?;
2527    let outcomes = run_string(runner, &format!("{}", filename.display()), &input).await?;
2528    runner.check_catalog().await?;
2529
2530    Ok(outcomes)
2531}
2532
2533pub async fn rewrite_file(runner: &mut Runner<'_>, filename: &Path) -> Result<(), anyhow::Error> {
2534    runner.reset_database().await?;
2535    // `replace` directives are scoped to a single file; clear any registered by
2536    // a previous file so they do not leak into this one.
2537    runner.replacements.clear();
2538
2539    let mut file = OpenOptions::new().read(true).write(true).open(filename)?;
2540
2541    let mut input = String::new();
2542    file.read_to_string(&mut input)?;
2543
2544    let mut buf = RewriteBuffer::new(&input);
2545
2546    let mut parser = crate::parser::Parser::new(filename.to_str().unwrap_or(""), &input);
2547    writeln!(runner.config.stdout, "--- {}", filename.display());
2548    let mut in_transaction = false;
2549
2550    fn append_values_output(
2551        buf: &mut RewriteBuffer,
2552        input: &String,
2553        expected_output: &str,
2554        mode: &Mode,
2555        types: &Vec<Type>,
2556        column_names: Option<&Vec<ColumnName>>,
2557        actual_output: &Vec<String>,
2558        multiline: bool,
2559    ) {
2560        buf.append_header(input, expected_output, column_names);
2561
2562        for (i, row) in actual_output.chunks(types.len()).enumerate() {
2563            match mode {
2564                // In Cockroach mode, output each row on its own line, with
2565                // two spaces between each column.
2566                Mode::Cockroach => {
2567                    if i != 0 {
2568                        buf.append("\n");
2569                    }
2570
2571                    if row.len() == 0 {
2572                        // nothing to do
2573                    } else if row.len() == 1 {
2574                        // If there is only one column, then there is no need for space
2575                        // substitution, so we only do newline substitution.
2576                        if multiline {
2577                            buf.append(&row[0]);
2578                        } else {
2579                            buf.append(&row[0].replace('\n', "⏎"))
2580                        }
2581                    } else {
2582                        // Substitute spaces with ␠ to avoid mistaking the spaces in the result
2583                        // values with spaces that separate columns.
2584                        buf.append(
2585                            &row.iter()
2586                                .map(|col| {
2587                                    let mut col = col.replace(' ', "␠");
2588                                    if !multiline {
2589                                        col = col.replace('\n', "⏎");
2590                                    }
2591                                    col
2592                                })
2593                                .join("  "),
2594                        );
2595                    }
2596                }
2597                // In standard mode, output each value on its own line,
2598                // and ignore row boundaries.
2599                // No need to substitute spaces, because every value (not row) is on a separate
2600                // line. But we do need to substitute newlines.
2601                Mode::Standard => {
2602                    for (j, col) in row.iter().enumerate() {
2603                        if i != 0 || j != 0 {
2604                            buf.append("\n");
2605                        }
2606                        buf.append(&if multiline {
2607                            col.clone()
2608                        } else {
2609                            col.replace('\n', "⏎")
2610                        });
2611                    }
2612                }
2613            }
2614        }
2615    }
2616
2617    for record in parser.parse_records()? {
2618        let outcome = runner.run_record(&record, &mut in_transaction).await?;
2619
2620        match (&record, &outcome) {
2621            // If we see an output failure for a query, rewrite the expected output
2622            // to match the observed output.
2623            (
2624                Record::Query {
2625                    output:
2626                        Ok(QueryOutput {
2627                            mode,
2628                            output: Output::Values(_),
2629                            output_str: expected_output,
2630                            types,
2631                            column_names,
2632                            multiline,
2633                            ..
2634                        }),
2635                    ..
2636                },
2637                Outcome::OutputFailure {
2638                    actual_output: Output::Values(actual_output),
2639                    ..
2640                },
2641            ) => {
2642                append_values_output(
2643                    &mut buf,
2644                    &input,
2645                    expected_output,
2646                    mode,
2647                    types,
2648                    column_names.as_ref(),
2649                    actual_output,
2650                    *multiline,
2651                );
2652            }
2653            (
2654                Record::Query {
2655                    output:
2656                        Ok(QueryOutput {
2657                            mode,
2658                            output: Output::Values(_),
2659                            output_str: expected_output,
2660                            types,
2661                            multiline,
2662                            ..
2663                        }),
2664                    ..
2665                },
2666                Outcome::WrongColumnNames {
2667                    actual_column_names,
2668                    actual_output: Output::Values(actual_output),
2669                    ..
2670                },
2671            ) => {
2672                append_values_output(
2673                    &mut buf,
2674                    &input,
2675                    expected_output,
2676                    mode,
2677                    types,
2678                    Some(actual_column_names),
2679                    actual_output,
2680                    *multiline,
2681                );
2682            }
2683            (
2684                Record::Query {
2685                    output:
2686                        Ok(QueryOutput {
2687                            output: Output::Hashed { .. },
2688                            output_str: expected_output,
2689                            column_names,
2690                            ..
2691                        }),
2692                    ..
2693                },
2694                Outcome::OutputFailure {
2695                    actual_output: Output::Hashed { num_values, md5 },
2696                    ..
2697                },
2698            ) => {
2699                buf.append_header(&input, expected_output, column_names.as_ref());
2700
2701                buf.append(format!("{} values hashing to {}\n", num_values, md5).as_str())
2702            }
2703            (
2704                Record::Simple {
2705                    output_str: expected_output,
2706                    ..
2707                },
2708                Outcome::OutputFailure {
2709                    actual_output: Output::Values(actual_output),
2710                    ..
2711                },
2712            ) => {
2713                buf.append_header(&input, expected_output, None);
2714
2715                for (i, row) in actual_output.iter().enumerate() {
2716                    if i != 0 {
2717                        buf.append("\n");
2718                    }
2719                    buf.append(row);
2720                }
2721            }
2722            (
2723                Record::Query {
2724                    sql,
2725                    output: Err(err),
2726                    ..
2727                },
2728                outcome,
2729            )
2730            | (
2731                Record::Statement {
2732                    expected_error: Some(err),
2733                    sql,
2734                    ..
2735                },
2736                outcome,
2737            ) if outcome.err_msg().is_some() => {
2738                buf.rewrite_expected_error(&input, err, &outcome.err_msg().unwrap(), sql)
2739            }
2740            (_, Outcome::Success) => {}
2741            _ => bail!("unexpected: {:?} {:?}", record, outcome),
2742        }
2743    }
2744
2745    file.set_len(0)?;
2746    file.seek(SeekFrom::Start(0))?;
2747    file.write_all(buf.finish().as_bytes())?;
2748    file.sync_all()?;
2749    Ok(())
2750}
2751
2752/// Provides a means to rewrite the `.slt` file while iterating over it.
2753///
2754/// This struct takes the slt file as its `input`, tracks a cursor into it
2755/// (`input_offset`), and provides a buffer (`output`) to store the rewritten
2756/// results.
2757///
2758/// Functions that modify the file will lazily move `input` into `output` using
2759/// `flush_to`. However, those calls should all be interior to other functions.
2760#[derive(Debug)]
2761struct RewriteBuffer<'a> {
2762    input: &'a str,
2763    input_offset: usize,
2764    output: String,
2765}
2766
2767impl<'a> RewriteBuffer<'a> {
2768    fn new(input: &'a str) -> RewriteBuffer<'a> {
2769        RewriteBuffer {
2770            input,
2771            input_offset: 0,
2772            output: String::new(),
2773        }
2774    }
2775
2776    fn flush_to(&mut self, offset: usize) {
2777        assert!(offset >= self.input_offset);
2778        let chunk = &self.input[self.input_offset..offset];
2779        self.output.push_str(chunk);
2780        self.input_offset = offset;
2781    }
2782
2783    fn skip_to(&mut self, offset: usize) {
2784        assert!(offset >= self.input_offset);
2785        self.input_offset = offset;
2786    }
2787
2788    fn append(&mut self, s: &str) {
2789        self.output.push_str(s);
2790    }
2791
2792    fn append_header(
2793        &mut self,
2794        input: &String,
2795        expected_output: &str,
2796        column_names: Option<&Vec<ColumnName>>,
2797    ) {
2798        // Output everything before this record.
2799        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2800        #[allow(clippy::as_conversions)]
2801        let offset = expected_output.as_ptr() as usize - input.as_ptr() as usize;
2802        self.flush_to(offset);
2803        self.skip_to(offset + expected_output.len());
2804
2805        // Attempt to install the result separator (----), if it does
2806        // not already exist.
2807        if self.peek_last(5) == "\n----" {
2808            self.append("\n");
2809        } else if self.peek_last(6) != "\n----\n" {
2810            self.append("\n----\n");
2811        }
2812
2813        let Some(names) = column_names else {
2814            return;
2815        };
2816        self.append(
2817            &names
2818                .iter()
2819                .map(|name| name.replace(' ', "␠"))
2820                .collect::<Vec<_>>()
2821                .join(" "),
2822        );
2823        self.append("\n");
2824    }
2825
2826    fn rewrite_expected_error(
2827        &mut self,
2828        input: &String,
2829        old_err: &str,
2830        new_err: &str,
2831        query: &str,
2832    ) {
2833        // Output everything before this error message.
2834        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2835        #[allow(clippy::as_conversions)]
2836        let err_offset = old_err.as_ptr() as usize - input.as_ptr() as usize;
2837        self.flush_to(err_offset);
2838        self.append(new_err);
2839        self.append("\n");
2840        self.append(query);
2841        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2842        #[allow(clippy::as_conversions)]
2843        self.skip_to(query.as_ptr() as usize - input.as_ptr() as usize + query.len())
2844    }
2845
2846    fn peek_last(&self, n: usize) -> &str {
2847        &self.output[self.output.len() - n..]
2848    }
2849
2850    fn finish(mut self) -> String {
2851        self.flush_to(self.input.len());
2852        self.output
2853    }
2854}
2855
2856/// Generates view creation, view indexing, view querying, and view
2857/// dropping SQL commands for a given `SELECT` query. If the number
2858/// of attributes produced by the query is known, the view commands
2859/// are specialized to avoid issues with column ambiguity. This
2860/// function is a helper for `--auto_index_selects` and assumes that
2861/// the provided input SQL has already been run through the parser,
2862/// resulting in a valid `SELECT` statement.
2863fn generate_view_sql(
2864    sql: &str,
2865    view_uuid: &Simple,
2866    num_attributes: Option<usize>,
2867    expected_column_names: Option<Vec<ColumnName>>,
2868) -> (String, String, String, String) {
2869    // To create the view, re-parse the sql; note that we must find exactly
2870    // one statement and it must be a `SELECT`.
2871    // NOTE(vmarcos): Direct string manipulation was attempted while
2872    // prototyping the code below, which avoids the extra parsing and
2873    // data structure cloning. However, running DDL is so slow that
2874    // it did not matter in terms of runtime. We can revisit this if
2875    // DDL cost drops dramatically in the future.
2876    let stmts = parser::parse_statements(sql).unwrap_or_default();
2877    assert!(stmts.len() == 1);
2878    let (query, query_as_of) = match &stmts[0].ast {
2879        Statement::Select(stmt) => (&stmt.query, &stmt.as_of),
2880        _ => unreachable!("This function should only be called for SELECTs"),
2881    };
2882
2883    // Prior to creating the view, process the `ORDER BY` clause of
2884    // the `SELECT` query, if any. Ordering is not preserved when a
2885    // view includes an `ORDER BY` clause and must be re-enforced by
2886    // an external `ORDER BY` clause when querying the view.
2887    let (view_order_by, extra_columns, distinct) = if num_attributes.is_none() {
2888        (query.order_by.clone(), vec![], None)
2889    } else {
2890        derive_order_by(&query.body, &query.order_by)
2891    };
2892
2893    // Since one-shot SELECT statements may contain ambiguous column names,
2894    // we either use the expected column names, if that option was
2895    // provided, or else just rename the output schema of the view
2896    // using numerically increasing attribute names, whenever possible.
2897    // This strategy makes it possible to use `CREATE INDEX`, thus
2898    // matching the behavior of the option `auto_index_tables`. However,
2899    // we may be presented with a `SELECT *` query, in which case the parser
2900    // does not produce sufficient information to allow us to compute
2901    // the number of output columns. In the latter case, we are supplied
2902    // with `None` for `num_attributes` and just employ the command
2903    // `CREATE DEFAULT INDEX` instead. Additionally, the view is created
2904    // without schema renaming. This strategy is insufficient to dodge
2905    // column name ambiguity in all cases, but we assume here that we
2906    // can adjust the (hopefully) small number of tests that eventually
2907    // challenge us in this particular way.
2908    let name = UnresolvedItemName(vec![Ident::new_unchecked(format!("v{}", view_uuid))]);
2909    let projection = expected_column_names.map_or_else(
2910        || {
2911            num_attributes.map_or(vec![], |n| {
2912                (1..=n)
2913                    .map(|i| Ident::new_unchecked(format!("a{i}")))
2914                    .collect()
2915            })
2916        },
2917        |cols| {
2918            cols.iter()
2919                .map(|c| Ident::new_unchecked(c.as_str()))
2920                .collect()
2921        },
2922    );
2923    let columns: Vec<Ident> = projection
2924        .iter()
2925        .cloned()
2926        .chain(extra_columns.iter().map(|item| {
2927            if let SelectItem::Expr {
2928                expr: _,
2929                alias: Some(ident),
2930            } = item
2931            {
2932                ident.clone()
2933            } else {
2934                unreachable!("alias must be given for extra column")
2935            }
2936        }))
2937        .collect();
2938
2939    // Build a `CREATE VIEW` with the columns computed above.
2940    let mut query = query.clone();
2941    if extra_columns.len() > 0 {
2942        match &mut query.body {
2943            SetExpr::Select(stmt) => stmt.projection.extend(extra_columns.iter().cloned()),
2944            _ => unimplemented!("cannot yet rewrite projections of nested queries"),
2945        }
2946    }
2947    let create_view = AstStatement::<Raw>::CreateView(CreateViewStatement {
2948        if_exists: IfExistsBehavior::Error,
2949        temporary: false,
2950        definition: ViewDefinition {
2951            name: name.clone(),
2952            columns: columns.clone(),
2953            query,
2954        },
2955    })
2956    .to_ast_string_stable();
2957
2958    // We then create either a `CREATE INDEX` or a `CREATE DEFAULT INDEX`
2959    // statement, depending on whether we could obtain the number of
2960    // attributes from the original `SELECT`.
2961    let create_index = AstStatement::<Raw>::CreateIndex(CreateIndexStatement {
2962        name: None,
2963        in_cluster: None,
2964        on_name: RawItemName::Name(name.clone()),
2965        key_parts: if columns.len() == 0 {
2966            None
2967        } else {
2968            Some(
2969                columns
2970                    .iter()
2971                    .map(|ident| Expr::Identifier(vec![ident.clone()]))
2972                    .collect(),
2973            )
2974        },
2975        with_options: Vec::new(),
2976        if_not_exists: false,
2977    })
2978    .to_ast_string_stable();
2979
2980    // Assert if DISTINCT semantics are unchanged from view
2981    let distinct_unneeded = extra_columns.len() == 0
2982        || match distinct {
2983            None | Some(Distinct::On(_)) => true,
2984            Some(Distinct::EntireRow) => false,
2985        };
2986    let distinct = if distinct_unneeded { None } else { distinct };
2987
2988    // `SELECT [* | {projection}] FROM {name} [ORDER BY {view_order_by}]`
2989    let view_sql = AstStatement::<Raw>::Select(SelectStatement {
2990        query: Query {
2991            ctes: CteBlock::Simple(vec![]),
2992            body: SetExpr::Select(Box::new(Select {
2993                distinct,
2994                projection: if projection.len() == 0 {
2995                    vec![SelectItem::Wildcard]
2996                } else {
2997                    projection
2998                        .iter()
2999                        .map(|ident| SelectItem::Expr {
3000                            expr: Expr::Identifier(vec![ident.clone()]),
3001                            alias: None,
3002                        })
3003                        .collect()
3004                },
3005                from: vec![TableWithJoins {
3006                    relation: TableFactor::Table {
3007                        name: RawItemName::Name(name.clone()),
3008                        alias: None,
3009                    },
3010                    joins: vec![],
3011                }],
3012                selection: None,
3013                group_by: vec![],
3014                having: None,
3015                qualify: None,
3016                options: vec![],
3017            })),
3018            order_by: view_order_by,
3019            limit: None,
3020            offset: None,
3021        },
3022        as_of: query_as_of.clone(),
3023    })
3024    .to_ast_string_stable();
3025
3026    // `DROP VIEW {name}`
3027    let drop_view = AstStatement::<Raw>::DropObjects(DropObjectsStatement {
3028        object_type: ObjectType::View,
3029        if_exists: false,
3030        names: vec![UnresolvedObjectName::Item(name)],
3031        cascade: false,
3032    })
3033    .to_ast_string_stable();
3034
3035    (create_view, create_index, view_sql, drop_view)
3036}
3037
3038/// Analyzes the provided query `body` to derive the number of
3039/// attributes in the query. We only consider syntactic cues,
3040/// so we may end up deriving `None` for the number of attributes
3041/// as a conservative approximation.
3042fn derive_num_attributes(body: &SetExpr<Raw>) -> Option<usize> {
3043    let Some((projection, _)) = find_projection(body) else {
3044        return None;
3045    };
3046    derive_num_attributes_from_projection(projection)
3047}
3048
3049/// Analyzes a query's `ORDER BY` clause to derive an `ORDER BY`
3050/// clause that makes numeric references to any expressions in
3051/// the projection and generated-attribute references to expressions
3052/// that need to be added as extra columns to the projection list.
3053/// The rewritten `ORDER BY` clause is then usable when querying a
3054/// view that contains the same `SELECT` as the given query.
3055/// This function returns both the rewritten `ORDER BY` clause
3056/// as well as a list of extra columns that need to be added
3057/// to the query's projection for the `ORDER BY` clause to
3058/// succeed.
3059fn derive_order_by(
3060    body: &SetExpr<Raw>,
3061    order_by: &Vec<OrderByExpr<Raw>>,
3062) -> (
3063    Vec<OrderByExpr<Raw>>,
3064    Vec<SelectItem<Raw>>,
3065    Option<Distinct<Raw>>,
3066) {
3067    let Some((projection, distinct)) = find_projection(body) else {
3068        return (vec![], vec![], None);
3069    };
3070    let (view_order_by, extra_columns) = derive_order_by_from_projection(projection, order_by);
3071    (view_order_by, extra_columns, distinct.clone())
3072}
3073
3074/// Finds the projection list in a `SELECT` query body.
3075fn find_projection(body: &SetExpr<Raw>) -> Option<(&Vec<SelectItem<Raw>>, &Option<Distinct<Raw>>)> {
3076    // Iterate to peel off the query body until the query's
3077    // projection list is found.
3078    let mut set_expr = body;
3079    loop {
3080        match set_expr {
3081            SetExpr::Select(select) => {
3082                return Some((&select.projection, &select.distinct));
3083            }
3084            SetExpr::SetOperation { left, .. } => set_expr = left.as_ref(),
3085            SetExpr::Query(query) => set_expr = &query.body,
3086            _ => return None,
3087        }
3088    }
3089}
3090
3091/// Computes the number of attributes that are obtained by the
3092/// projection of a `SELECT` query. The projection may include
3093/// wildcards, in which case the analysis just returns `None`.
3094fn derive_num_attributes_from_projection(projection: &Vec<SelectItem<Raw>>) -> Option<usize> {
3095    let mut num_attributes = 0usize;
3096    for item in projection.iter() {
3097        let SelectItem::Expr { expr, .. } = item else {
3098            return None;
3099        };
3100        match expr {
3101            Expr::QualifiedWildcard(..) | Expr::WildcardAccess(..) => {
3102                return None;
3103            }
3104            _ => {
3105                num_attributes += 1;
3106            }
3107        }
3108    }
3109    Some(num_attributes)
3110}
3111
3112/// Computes an `ORDER BY` clause with only numeric references
3113/// from given projection and `ORDER BY` of a `SELECT` query.
3114/// If the derivation fails to match a given expression, the
3115/// matched prefix is returned. Note that this could be empty.
3116fn derive_order_by_from_projection(
3117    projection: &Vec<SelectItem<Raw>>,
3118    order_by: &Vec<OrderByExpr<Raw>>,
3119) -> (Vec<OrderByExpr<Raw>>, Vec<SelectItem<Raw>>) {
3120    let mut view_order_by: Vec<OrderByExpr<Raw>> = vec![];
3121    let mut extra_columns: Vec<SelectItem<Raw>> = vec![];
3122    for order_by_expr in order_by.iter() {
3123        let query_expr = &order_by_expr.expr;
3124        let view_expr = match query_expr {
3125            Expr::Value(mz_sql_parser::ast::Value::Number(_)) => query_expr.clone(),
3126            _ => {
3127                // Find expression in query projection, if we can.
3128                if let Some(i) = projection.iter().position(|item| match item {
3129                    SelectItem::Expr { expr, alias } => {
3130                        expr == query_expr
3131                            || match query_expr {
3132                                Expr::Identifier(ident) => {
3133                                    ident.len() == 1 && Some(&ident[0]) == alias.as_ref()
3134                                }
3135                                _ => false,
3136                            }
3137                    }
3138                    SelectItem::Wildcard => false,
3139                }) {
3140                    Expr::Value(mz_sql_parser::ast::Value::Number((i + 1).to_string()))
3141                } else {
3142                    // If the expression is not found in the
3143                    // projection, add extra column.
3144                    let ident = Ident::new_unchecked(format!(
3145                        "a{}",
3146                        (projection.len() + extra_columns.len() + 1)
3147                    ));
3148                    extra_columns.push(SelectItem::Expr {
3149                        expr: query_expr.clone(),
3150                        alias: Some(ident.clone()),
3151                    });
3152                    Expr::Identifier(vec![ident])
3153                }
3154            }
3155        };
3156        view_order_by.push(OrderByExpr {
3157            expr: view_expr,
3158            asc: order_by_expr.asc,
3159            nulls_last: order_by_expr.nulls_last,
3160        });
3161    }
3162    (view_order_by, extra_columns)
3163}
3164
3165/// Returns extra statements to execute after `stmt` is executed.
3166fn mutate(sql: &str) -> Vec<String> {
3167    let stmts = parser::parse_statements(sql).unwrap_or_default();
3168    let mut additional = Vec::new();
3169    for stmt in stmts {
3170        match stmt.ast {
3171            AstStatement::CreateTable(stmt) => additional.push(
3172                // CREATE TABLE -> CREATE INDEX. Specify all columns manually in case CREATE
3173                // DEFAULT INDEX ever goes away.
3174                AstStatement::<Raw>::CreateIndex(CreateIndexStatement {
3175                    name: None,
3176                    in_cluster: None,
3177                    on_name: RawItemName::Name(stmt.name.clone()),
3178                    key_parts: Some(
3179                        stmt.columns
3180                            .iter()
3181                            .map(|def| Expr::Identifier(vec![def.name.clone()]))
3182                            .collect(),
3183                    ),
3184                    with_options: Vec::new(),
3185                    if_not_exists: false,
3186                })
3187                .to_ast_string_stable(),
3188            ),
3189            _ => {}
3190        }
3191    }
3192    additional
3193}
3194
3195#[mz_ore::test]
3196#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
3197fn test_generate_view_sql() {
3198    let uuid = Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c8").unwrap();
3199    let cases = vec![
3200        (("SELECT * FROM t", None, None),
3201        (
3202            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM "t""#.to_string(),
3203            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3204            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3205            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3206        )),
3207        (("SELECT a, b, c FROM t1, t2", Some(3), Some(vec![ColumnName::from("a"), ColumnName::from("b"), ColumnName::from("c")])),
3208        (
3209            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c") AS SELECT "a", "b", "c" FROM "t1", "t2""#.to_string(),
3210            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c")"#.to_string(),
3211            r#"SELECT "a", "b", "c" FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3212            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3213        )),
3214        (("SELECT a, b, c FROM t1, t2", Some(3), None),
3215        (
3216            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3") AS SELECT "a", "b", "c" FROM "t1", "t2""#.to_string(),
3217            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3218            r#"SELECT "a1", "a2", "a3" FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3219            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3220        )),
3221        // A case with ambiguity that is accepted by the function, illustrating that
3222        // our measures to dodge this issue are imperfect.
3223        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a)", None, None),
3224        (
3225            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a")"#.to_string(),
3226            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3227            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3228            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3229        )),
3230        (("SELECT a, b, b + d AS c, a + b AS d FROM t1, t2 ORDER BY a, c, a + b", Some(4), Some(vec![ColumnName::from("a"), ColumnName::from("b"), ColumnName::from("c"), ColumnName::from("d")])),
3231        (
3232            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c", "d") AS SELECT "a", "b", "b" + "d" AS "c", "a" + "b" AS "d" FROM "t1", "t2" ORDER BY "a", "c", "a" + "b""#.to_string(),
3233            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c", "d")"#.to_string(),
3234            r#"SELECT "a", "b", "c", "d" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1, 3, 4"#.to_string(),
3235            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3236        )),
3237        (("((SELECT 1 AS a UNION SELECT 2 AS b) UNION SELECT 3 AS c) ORDER BY a", Some(1), None),
3238        (
3239            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a1") AS (SELECT 1 AS "a" UNION SELECT 2 AS "b") UNION SELECT 3 AS "c" ORDER BY "a""#.to_string(),
3240            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1")"#.to_string(),
3241            r#"SELECT "a1" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1"#.to_string(),
3242            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3243        )),
3244        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a) ORDER BY 1", None, None),
3245        (
3246            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a") ORDER BY 1"#.to_string(),
3247            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3248            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1"#.to_string(),
3249            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3250        )),
3251        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a) ORDER BY a", None, None),
3252        (
3253            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a") ORDER BY "a""#.to_string(),
3254            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3255            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY "a""#.to_string(),
3256            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3257        )),
3258        (("SELECT a, sum(b) AS a FROM t GROUP BY a, c ORDER BY a, c", Some(2), None),
3259        (
3260            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3") AS SELECT "a", "sum"("b") AS "a", "c" AS "a3" FROM "t" GROUP BY "a", "c" ORDER BY "a", "c""#.to_string(),
3261            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3262            r#"SELECT "a1", "a2" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1, "a3""#.to_string(),
3263            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3264        )),
3265        (("SELECT a, sum(b) AS a FROM t GROUP BY a, c ORDER BY c, a", Some(2), None),
3266        (
3267            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3") AS SELECT "a", "sum"("b") AS "a", "c" AS "a3" FROM "t" GROUP BY "a", "c" ORDER BY "c", "a""#.to_string(),
3268            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3269            r#"SELECT "a1", "a2" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY "a3", 1"#.to_string(),
3270            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3271        )),
3272    ];
3273    for ((sql, num_attributes, expected_column_names), expected) in cases {
3274        let view_sql =
3275            generate_view_sql(sql, uuid.as_simple(), num_attributes, expected_column_names);
3276        assert_eq!(expected, view_sql);
3277    }
3278}
3279
3280#[mz_ore::test]
3281fn test_mutate() {
3282    let cases = vec![
3283        ("CREATE TABLE t ()", vec![r#"CREATE INDEX ON "t" ()"#]),
3284        (
3285            "CREATE TABLE t (a INT)",
3286            vec![r#"CREATE INDEX ON "t" ("a")"#],
3287        ),
3288        (
3289            "CREATE TABLE t (a INT, b TEXT)",
3290            vec![r#"CREATE INDEX ON "t" ("a", "b")"#],
3291        ),
3292        // Invalid syntax works, just returns nothing.
3293        ("BAD SYNTAX", Vec::new()),
3294    ];
3295    for (sql, expected) in cases {
3296        let stmts = mutate(sql);
3297        assert_eq!(expected, stmts, "sql: {sql}");
3298    }
3299}
3300
3301#[mz_ore::test]
3302fn test_strip_crdb_table_items() {
3303    let cases = vec![
3304        (
3305            "CREATE TABLE t (a INT, INDEX foo (a), b INT)",
3306            Some("CREATE TABLE t (a INT, b INT)"),
3307        ),
3308        (
3309            "CREATE TABLE t (a INT, INDEX(a))",
3310            Some("CREATE TABLE t (a INT)"),
3311        ),
3312        (
3313            "CREATE TABLE t (a INT, UNIQUE INDEX foo (a), INVERTED INDEX bar (a))",
3314            Some("CREATE TABLE t (a INT)"),
3315        ),
3316        (
3317            "CREATE TABLE t (k INT, v STRING, FAMILY \"primary\" (k, v))",
3318            Some("CREATE TABLE t (k INT, v STRING)"),
3319        ),
3320        // Partial and expression indexes have nested clauses.
3321        (
3322            "CREATE TABLE t (a INT, INDEX (a) WHERE a = 0, INDEX ((a + 1)))",
3323            Some("CREATE TABLE t (a INT)"),
3324        ),
3325        // Constraints are kept.
3326        (
3327            "CREATE TABLE t (a INT, UNIQUE (a), CHECK (a > 0))",
3328            Some("CREATE TABLE t (a INT, UNIQUE (a), CHECK (a > 0))"),
3329        ),
3330        // Column names that merely start with a stripped keyword are kept.
3331        (
3332            "CREATE TABLE t (index_col INT, \"index\" INT)",
3333            Some("CREATE TABLE t (index_col INT, \"index\" INT)"),
3334        ),
3335        // Parens inside strings do not confuse the item splitter.
3336        (
3337            "CREATE TABLE t (a TEXT DEFAULT 'a,(b', INDEX (a))",
3338            Some("CREATE TABLE t (a TEXT DEFAULT 'a,(b')"),
3339        ),
3340        ("SELECT 1", None),
3341    ];
3342    for (sql, expected) in cases {
3343        let stripped = strip_crdb_table_items(sql);
3344        assert_eq!(expected.map(|e| e.to_string()), stripped, "sql: {sql}");
3345    }
3346}