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 futures::sink::SinkExt;
43use itertools::Itertools;
44use maplit::btreemap;
45use md5::{Digest, Md5};
46use mz_adapter_types::bootstrap_builtin_cluster_config::{
47    ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
48    CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
49    SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR, SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
50};
51use mz_catalog::config::ClusterReplicaSizeMap;
52use mz_controller::{ControllerConfig, ReplicaHttpLocator};
53use mz_environmentd::CatalogConfig;
54use mz_license_keys::ValidatedLicenseKey;
55use mz_orchestrator_process::{ProcessOrchestrator, ProcessOrchestratorConfig};
56use mz_orchestrator_tracing::{TracingCliArgs, TracingOrchestrator};
57use mz_ore::cast::{CastFrom, ReinterpretCast};
58use mz_ore::channel::trigger;
59use mz_ore::error::ErrorExt;
60use mz_ore::metrics::MetricsRegistry;
61use mz_ore::now::SYSTEM_TIME;
62use mz_ore::retry::Retry;
63use mz_ore::sql;
64use mz_ore::sql::Sql;
65use mz_ore::task;
66use mz_ore::thread::{JoinHandleExt, JoinOnDropHandle};
67use mz_ore::tracing::TracingHandle;
68use mz_ore::url::SensitiveUrl;
69use mz_persist_client::PersistLocation;
70use mz_persist_client::cache::PersistClientCache;
71use mz_persist_client::cfg::PersistConfig;
72use mz_persist_client::rpc::{
73    MetricsSameProcessPubSubSender, PersistGrpcPubSubServer, PubSubClientConnection, PubSubSender,
74};
75use mz_pgrepr::{Interval, Jsonb, Numeric, UInt2, UInt4, UInt8, Value, oid};
76use mz_repr::ColumnName;
77use mz_repr::adt::date::Date;
78use mz_repr::adt::mz_acl_item::{AclItem, MzAclItem};
79use mz_repr::adt::numeric;
80use mz_secrets::SecretsController;
81use mz_server_core::listeners::v26_32_0::ListenersConfig;
82use mz_server_core::listeners::{
83    AllowedRoles, AuthenticatorKind, HttpListenerConfig, HttpRoutesEnabled, RouteGroup,
84    SqlListenerConfig,
85};
86use mz_sql::ast::{Expr, Raw, Statement};
87use mz_sql::catalog::EnvironmentId;
88use mz_sql_parser::ast::display::AstDisplay;
89use mz_sql_parser::ast::{
90    CreateIndexStatement, CreateViewStatement, CteBlock, Distinct, DropObjectsStatement, Ident,
91    IfExistsBehavior, ObjectType, OrderByExpr, Query, RawItemName, Select, SelectItem,
92    SelectStatement, SetExpr, Statement as AstStatement, TableFactor, TableWithJoins,
93    UnresolvedItemName, UnresolvedObjectName, ViewDefinition,
94};
95use mz_sql_parser::parser;
96use mz_storage_types::connections::ConnectionContext;
97use postgres_protocol::types;
98use regex::Regex;
99use tempfile::TempDir;
100use tokio::net::TcpListener;
101use tokio::runtime::Runtime;
102use tokio::sync::oneshot;
103use tokio_postgres::fallible_iterator::FallibleIterator;
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                        mcp: RouteGroup::Disabled,
1274                        console_config: RouteGroup::Enabled(AllowedRoles::Normal),
1275                    },
1276                },
1277                "internal".to_owned() => HttpListenerConfig {
1278                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
1279                    authenticator_kind: AuthenticatorKind::None,
1280                    enable_tls: false,
1281                    routes: HttpRoutesEnabled {
1282                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1283                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1284                        internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1285                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1286                        profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
1287                        mcp_agent: RouteGroup::Disabled,
1288                        mcp_developer: RouteGroup::Disabled,
1289                        mcp: RouteGroup::Disabled,
1290                        console_config: RouteGroup::Disabled,
1291                    },
1292                },
1293            ],
1294        };
1295        let listeners = mz_environmentd::Listeners::bind(listeners_config)?;
1296        let host_name = format!(
1297            "localhost:{}",
1298            listeners.http["external"].handle.local_addr.port()
1299        );
1300        let catalog_config = CatalogConfig {
1301            persist_clients: Arc::clone(&persist_clients),
1302            metrics: Arc::new(mz_catalog::durable::Metrics::new(&MetricsRegistry::new())),
1303        };
1304        let system_dyncfgs = Arc::clone(&persist_clients.cfg().configs);
1305        let server_config = mz_environmentd::Config {
1306            catalog_config,
1307            timestamp_oracle_url: Some(timestamp_oracle_url),
1308            controller: ControllerConfig {
1309                build_info: &mz_environmentd::BUILD_INFO,
1310                orchestrator,
1311                clusterd_image: "clusterd".into(),
1312                init_container_image: None,
1313                deploy_generation: 0,
1314                persist_location: PersistLocation {
1315                    blob_uri: format!(
1316                        "file://{}/persist/blob",
1317                        config.persist_dir.path().display()
1318                    )
1319                    .parse()
1320                    .expect("invalid blob URI"),
1321                    consensus_uri,
1322                },
1323                persist_clients,
1324                now: SYSTEM_TIME.clone(),
1325                metrics_registry: metrics_registry.clone(),
1326                persist_pubsub_url: format!("http://localhost:{}", persist_pubsub_server_port),
1327                secrets_args: mz_secrets_cli::SecretsReaderCliArgs {
1328                    secrets_reader: mz_secrets_cli::SecretsControllerKind::LocalFile,
1329                    secrets_reader_local_file_dir: Some(secrets_dir),
1330                    secrets_reader_kubernetes_context: None,
1331                    secrets_reader_aws_prefix: None,
1332                    secrets_reader_name_prefix: None,
1333                },
1334                connection_context,
1335                replica_http_locator: Arc::new(ReplicaHttpLocator::default()),
1336            },
1337            secrets_controller,
1338            cloud_resource_controller: None,
1339            system_dyncfgs,
1340            tls: None,
1341            frontegg: None,
1342            frontegg_oauth_issuer_url: None,
1343            cors_allowed_origin: AllowOrigin::list([]),
1344            cors_allowed_origin_list: Vec::new(),
1345            unsafe_mode: true,
1346            all_features: false,
1347            restarted_in_place: false,
1348            metrics_registry,
1349            now,
1350            environment_id,
1351            cluster_replica_sizes: ClusterReplicaSizeMap::for_tests(),
1352            bootstrap_default_cluster_replica_size: config.replica_size.clone(),
1353            bootstrap_default_cluster_replication_factor: config
1354                .replicas
1355                .try_into()
1356                .expect("replicas must fit"),
1357            bootstrap_builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
1358                replication_factor: SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1359                size: config.replica_size.clone(),
1360            },
1361            bootstrap_builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
1362                replication_factor: CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1363                size: config.replica_size.clone(),
1364            },
1365            bootstrap_builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
1366                replication_factor: PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1367                size: config.replica_size.clone(),
1368            },
1369            bootstrap_builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
1370                replication_factor: SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1371                size: config.replica_size.clone(),
1372            },
1373            bootstrap_builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
1374                replication_factor: ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR,
1375                size: config.replica_size.clone(),
1376            },
1377            system_parameter_defaults: {
1378                let mut params = BTreeMap::new();
1379                params.insert(
1380                    "log_filter".to_string(),
1381                    config.tracing.startup_log_filter.to_string(),
1382                );
1383                params.extend(config.system_parameter_defaults.clone());
1384                params
1385            },
1386            availability_zones: Default::default(),
1387            tracing_handle: config.tracing_handle.clone(),
1388            storage_usage_collection_interval: Duration::from_secs(3600),
1389            storage_usage_retention_period: None,
1390            segment_api_key: None,
1391            segment_client_side: false,
1392            // SLT doesn't like eternally running tasks since it waits for them to finish inbetween SLT files
1393            test_only_dummy_segment_client: false,
1394            egress_addresses: vec![],
1395            aws_account_id: None,
1396            aws_privatelink_availability_zones: None,
1397            launchdarkly_sdk_key: None,
1398            launchdarkly_base_uri: None,
1399            launchdarkly_key_map: Default::default(),
1400            config_sync_file_path: None,
1401            config_sync_timeout: Duration::from_secs(30),
1402            config_sync_loop_interval: None,
1403            bootstrap_role: Some("materialize".into()),
1404            http_host_name: Some(host_name),
1405            internal_console_redirect_url: None,
1406            tls_reload_certs: mz_server_core::cert_reload_never_reload(),
1407            helm_chart_version: None,
1408            license_key: ValidatedLicenseKey::for_tests(),
1409            external_login_password_mz_system: None,
1410            force_builtin_schema_migration: None,
1411            force_catalog_migration: false,
1412        };
1413        // We need to run the server on its own Tokio runtime, which in turn
1414        // requires its own thread, so that we can wait for any tasks spawned
1415        // by the server to be shutdown at the end of each file. If we were to
1416        // share a Tokio runtime, tasks from the last file's server would still
1417        // be live at the start of the next file's server.
1418        let (server_addr_tx, server_addr_rx): (oneshot::Sender<Result<_, anyhow::Error>>, _) =
1419            oneshot::channel();
1420        let (internal_server_addr_tx, internal_server_addr_rx) = oneshot::channel();
1421        let (password_server_addr_tx, password_server_addr_rx) = oneshot::channel();
1422        let (internal_http_server_addr_tx, internal_http_server_addr_rx) = oneshot::channel();
1423        let (shutdown_trigger, shutdown_trigger_rx) = trigger::channel();
1424        let server_thread = thread::spawn(|| {
1425            let runtime = match Runtime::new() {
1426                Ok(runtime) => runtime,
1427                Err(e) => {
1428                    server_addr_tx
1429                        .send(Err(e.into()))
1430                        .expect("receiver should not drop first");
1431                    return;
1432                }
1433            };
1434            let server = match runtime.block_on(listeners.serve(server_config)) {
1435                Ok(runtime) => runtime,
1436                Err(e) => {
1437                    server_addr_tx
1438                        .send(Err(e.into()))
1439                        .expect("receiver should not drop first");
1440                    return;
1441                }
1442            };
1443            server_addr_tx
1444                .send(Ok(server.sql_listener_handles["external"].local_addr))
1445                .expect("receiver should not drop first");
1446            internal_server_addr_tx
1447                .send(server.sql_listener_handles["internal"].local_addr)
1448                .expect("receiver should not drop first");
1449            password_server_addr_tx
1450                .send(server.sql_listener_handles["password"].local_addr)
1451                .expect("receiver should not drop first");
1452            internal_http_server_addr_tx
1453                .send(server.http_listener_handles["internal"].local_addr)
1454                .expect("receiver should not drop first");
1455            runtime.block_on(shutdown_trigger_rx);
1456        });
1457        let server_addr = server_addr_rx.await??;
1458        let internal_server_addr = internal_server_addr_rx.await?;
1459        let password_server_addr = password_server_addr_rx.await?;
1460        let internal_http_server_addr = internal_http_server_addr_rx.await?;
1461
1462        let system_client = connect(internal_server_addr, Some("mz_system"), None)
1463            .await
1464            .unwrap();
1465        let client = connect(server_addr, None, None).await.unwrap();
1466
1467        let inner = RunnerInner {
1468            server_addr,
1469            internal_server_addr,
1470            password_server_addr,
1471            internal_http_server_addr,
1472            _shutdown_trigger: shutdown_trigger,
1473            _server_thread: server_thread.join_on_drop(),
1474            _temp_dir: temp_dir,
1475            client,
1476            system_client,
1477            clients: BTreeMap::new(),
1478            active_user: None,
1479            auto_index_tables: config.auto_index_tables,
1480            auto_index_selects: config.auto_index_selects,
1481            auto_transactions: config.auto_transactions,
1482            enable_table_keys: config.enable_table_keys,
1483            verbose: config.verbose,
1484            stdout: config.stdout,
1485        };
1486        inner.ensure_fixed_features().await?;
1487
1488        Ok(inner)
1489    }
1490
1491    /// Set features that should be enabled regardless of whether reset-server was
1492    /// called. These features may be set conditionally depending on the run configuration.
1493    #[allow(clippy::disallowed_methods)]
1494    async fn ensure_fixed_features(&self) -> Result<(), anyhow::Error> {
1495        // We turn on enable_reduce_mfp_fusion, as we wish
1496        // to get as much coverage of these features as we can.
1497        // TODO(vmarcos): Remove this code when we retire this feature flag.
1498        self.system_client
1499            .execute("ALTER SYSTEM SET enable_reduce_mfp_fusion = on", &[])
1500            .await?;
1501
1502        // Dangerous functions are useful for tests so we enable it for all tests.
1503        self.system_client
1504            .execute("ALTER SYSTEM SET unsafe_enable_unsafe_functions = on", &[])
1505            .await?;
1506        Ok(())
1507    }
1508
1509    /// The connection records run on: the `user` directive's connection when
1510    /// one is active, the default connection otherwise.
1511    fn active_client(&self) -> &tokio_postgres::Client {
1512        match &self.active_user {
1513            Some(user) => self
1514                .clients
1515                .get(user)
1516                .expect("connection for the active user exists"),
1517            None => &self.client,
1518        }
1519    }
1520
1521    /// Handles a `user` directive: ensures the role exists, connects as it,
1522    /// and makes it the active user. CockroachDB's `root` maps to the
1523    /// default `materialize` user.
1524    #[allow(clippy::disallowed_methods)]
1525    async fn run_user<'r>(
1526        &mut self,
1527        user: &'r str,
1528        location: &Location,
1529        in_transaction: &mut bool,
1530    ) -> Result<Outcome<'r>, anyhow::Error> {
1531        if self.auto_transactions && *in_transaction {
1532            self.active_client().execute("COMMIT", &[]).await?;
1533            *in_transaction = false;
1534        }
1535        if user == "root" || user == "materialize" {
1536            self.active_user = None;
1537            return Ok(Outcome::Success);
1538        }
1539        if !self.clients.contains_key(user) {
1540            let create = sql!("CREATE ROLE {}", Sql::ident(user));
1541            if let Err(error) = self.system_client.batch_execute(create.as_str()).await {
1542                if !error.to_string_with_causes().contains("already exists") {
1543                    return Ok(Outcome::Bail {
1544                        cause: Box::new(Outcome::PlanFailure {
1545                            error: anyhow!(error),
1546                            expected_error: None,
1547                            location: location.clone(),
1548                        }),
1549                        location: location.clone(),
1550                    });
1551                }
1552            }
1553            match connect(self.server_addr, Some(user), None).await {
1554                Ok(client) => {
1555                    self.clients.insert(user.to_string(), client);
1556                }
1557                Err(error) => {
1558                    return Ok(Outcome::Bail {
1559                        cause: Box::new(Outcome::PlanFailure {
1560                            error: anyhow!(error),
1561                            expected_error: None,
1562                            location: location.clone(),
1563                        }),
1564                        location: location.clone(),
1565                    });
1566                }
1567            }
1568        }
1569        self.active_user = Some(user.to_string());
1570        Ok(Outcome::Success)
1571    }
1572
1573    #[allow(clippy::disallowed_methods)]
1574    async fn run_record<'r>(
1575        &mut self,
1576        record: &'r Record<'r>,
1577        in_transaction: &mut bool,
1578        replacements: &[(Regex, String)],
1579    ) -> Result<Outcome<'r>, anyhow::Error> {
1580        match &record {
1581            Record::User { user, location } => self.run_user(user, location, in_transaction).await,
1582            Record::Statement {
1583                expected_error,
1584                rows_affected,
1585                sql,
1586                location,
1587            } => {
1588                if self.auto_transactions && *in_transaction {
1589                    self.active_client().execute("COMMIT", &[]).await?;
1590                    *in_transaction = false;
1591                }
1592                match self
1593                    .run_statement(*expected_error, *rows_affected, sql, location.clone())
1594                    .await?
1595                {
1596                    Outcome::Success => {
1597                        if self.auto_index_tables {
1598                            let additional = mutate(sql);
1599                            for stmt in additional {
1600                                self.active_client().execute(&stmt, &[]).await?;
1601                            }
1602                        }
1603                        Ok(Outcome::Success)
1604                    }
1605                    other => {
1606                        if expected_error.is_some() {
1607                            Ok(other)
1608                        } else {
1609                            // If we failed to execute a statement that was supposed to succeed,
1610                            // running the rest of the tests in this file will probably cause
1611                            // false positives, so just give up on the file entirely.
1612                            Ok(Outcome::Bail {
1613                                cause: Box::new(other),
1614                                location: location.clone(),
1615                            })
1616                        }
1617                    }
1618                }
1619            }
1620            Record::Query {
1621                sql,
1622                output,
1623                location,
1624            } => {
1625                self.run_query(sql, output, location.clone(), in_transaction, replacements)
1626                    .await
1627            }
1628            Record::Simple {
1629                conn,
1630                user,
1631                password,
1632                sql,
1633                sort,
1634                output,
1635                location,
1636                ..
1637            } => {
1638                self.run_simple(
1639                    *conn,
1640                    *user,
1641                    *password,
1642                    sql,
1643                    sort.clone(),
1644                    output,
1645                    location.clone(),
1646                )
1647                .await
1648            }
1649            Record::Copy {
1650                table_name,
1651                tsv_path,
1652            } => {
1653                let tsv = tokio::fs::read(tsv_path).await?;
1654                let copy = self
1655                    .client
1656                    .copy_in(sql!("COPY {} FROM STDIN", Sql::ident(*table_name)).as_str())
1657                    .await?;
1658                tokio::pin!(copy);
1659                copy.send(bytes::Bytes::from(tsv)).await?;
1660                copy.finish().await?;
1661                Ok(Outcome::Success)
1662            }
1663            _ => Ok(Outcome::Success),
1664        }
1665    }
1666
1667    #[allow(clippy::disallowed_methods)]
1668    async fn run_statement<'r>(
1669        &self,
1670        expected_error: Option<&'r str>,
1671        expected_rows_affected: Option<u64>,
1672        sql: &'r str,
1673        location: Location,
1674    ) -> Result<Outcome<'r>, anyhow::Error> {
1675        static UNSUPPORTED_INDEX_STATEMENT_REGEX: LazyLock<Regex> =
1676            LazyLock::new(|| Regex::new("^(CREATE UNIQUE INDEX|REINDEX)").unwrap());
1677        if UNSUPPORTED_INDEX_STATEMENT_REGEX.is_match(sql) {
1678            // sure, we totally made you an index
1679            return Ok(Outcome::Success);
1680        }
1681
1682        // CockroachDB's logic tests frequently pack multiple commands into
1683        // one statement record. The extended protocol used by `execute`
1684        // rejects those, and Materialize restricts DDL inside the implicit
1685        // transaction the simple protocol would wrap them in, so run each
1686        // command separately and report the first error. This path cannot
1687        // report an affected-row count, so records that assert one keep the
1688        // single-command path (and fail if they contain multiple commands).
1689        let commands = if expected_rows_affected.is_none() {
1690            match mz_sql::parse::parse(sql) {
1691                Ok(stmts) if stmts.len() > 1 => {
1692                    Some(stmts.iter().map(|s| s.sql.to_owned()).collect::<Vec<_>>())
1693                }
1694                Ok(_) => None,
1695                // A statement Materialize cannot parse may be a CockroachDB
1696                // `CREATE TABLE` with inline physical-layout items. Retry
1697                // with those stripped, so the semantic records that follow
1698                // can still run against the table.
1699                Err(_) => strip_crdb_table_items(sql)
1700                    .filter(|stripped| mz_sql::parse::parse(stripped).is_ok())
1701                    .map(|stripped| vec![stripped]),
1702            }
1703        } else {
1704            None
1705        };
1706        let result = match commands {
1707            Some(commands) => {
1708                let mut result = Ok(0);
1709                for command in &commands {
1710                    if let Err(error) = self.active_client().execute(command, &[]).await {
1711                        result = Err(error);
1712                        break;
1713                    }
1714                }
1715                result
1716            }
1717            None => self.active_client().execute(sql, &[]).await,
1718        };
1719        match result {
1720            Ok(actual) => {
1721                if let Some(expected_error) = expected_error {
1722                    return Ok(Outcome::UnexpectedPlanSuccess {
1723                        expected_error,
1724                        location,
1725                    });
1726                }
1727                match expected_rows_affected {
1728                    None => Ok(Outcome::Success),
1729                    Some(expected) => {
1730                        if expected != actual {
1731                            Ok(Outcome::WrongNumberOfRowsInserted {
1732                                expected_count: expected,
1733                                actual_count: actual,
1734                                location,
1735                            })
1736                        } else {
1737                            Ok(Outcome::Success)
1738                        }
1739                    }
1740                }
1741            }
1742            Err(error) => {
1743                if let Some(expected_error) = expected_error {
1744                    if error_matches(expected_error, &error.to_string_with_causes()) {
1745                        return Ok(Outcome::Success);
1746                    }
1747                    return Ok(Outcome::PlanFailure {
1748                        error: anyhow!(error),
1749                        expected_error: Some(expected_error.to_string()),
1750                        location,
1751                    });
1752                }
1753                // CockroachDB tests configure many CockroachDB-specific
1754                // session settings. Treat setting an unknown parameter as a
1755                // no-op success so the records that follow still run.
1756                static SET_STATEMENT_REGEX: LazyLock<Regex> =
1757                    LazyLock::new(|| Regex::new("(?i)^(SET|RESET) ").unwrap());
1758                if SET_STATEMENT_REGEX.is_match(sql.trim_start())
1759                    && error
1760                        .to_string_with_causes()
1761                        .contains("unrecognized configuration parameter")
1762                {
1763                    return Ok(Outcome::Success);
1764                }
1765                Ok(Outcome::PlanFailure {
1766                    error: anyhow!(error),
1767                    expected_error: None,
1768                    location,
1769                })
1770            }
1771        }
1772    }
1773
1774    #[allow(clippy::disallowed_methods)]
1775    async fn prepare_query<'r>(
1776        &self,
1777        sql: &str,
1778        output: &'r Result<QueryOutput<'_>, &'r str>,
1779        location: Location,
1780        in_transaction: &mut bool,
1781    ) -> Result<PrepareQueryOutcome<'r>, anyhow::Error> {
1782        // get statement
1783        let statements = match mz_sql::parse::parse(sql) {
1784            Ok(statements) => statements,
1785            Err(e) => match output {
1786                Ok(_) => {
1787                    return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1788                        error: e.into(),
1789                        location,
1790                    }));
1791                }
1792                Err(expected_error) => {
1793                    if error_matches(expected_error, &e.to_string_with_causes()) {
1794                        return Ok(PrepareQueryOutcome::Outcome(Outcome::Success));
1795                    } else {
1796                        return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1797                            error: e.into(),
1798                            location,
1799                        }));
1800                    }
1801                }
1802            },
1803        };
1804        // Query records in files imported from CockroachDB occasionally
1805        // contain zero or multiple statements. Fail the record rather than
1806        // the entire run.
1807        let statement = match &*statements {
1808            [statement] => &statement.ast,
1809            _ => {
1810                return Ok(PrepareQueryOutcome::Outcome(Outcome::ParseFailure {
1811                    error: anyhow!("expected one statement, but got {}", statements.len()),
1812                    location,
1813                }));
1814            }
1815        };
1816        let (is_select, num_attributes, has_as_of) = match statement {
1817            Statement::Select(stmt) => (
1818                true,
1819                derive_num_attributes(&stmt.query.body),
1820                stmt.as_of.is_some(),
1821            ),
1822            _ => (false, None, false),
1823        };
1824
1825        match output {
1826            Ok(_) => {
1827                if self.auto_transactions && !*in_transaction {
1828                    // No ISOLATION LEVEL SERIALIZABLE because of database-issues#5323
1829                    self.active_client().execute("BEGIN", &[]).await?;
1830                    *in_transaction = true;
1831                }
1832            }
1833            Err(_) => {
1834                if self.auto_transactions && *in_transaction {
1835                    self.active_client().execute("COMMIT", &[]).await?;
1836                    *in_transaction = false;
1837                }
1838            }
1839        }
1840
1841        // `SHOW` commands reference catalog schema, thus are not in the same timedomain and not
1842        // allowed in the same transaction, see:
1843        // https://materialize.com/docs/sql/begin/#same-timedomain-error
1844        match statement {
1845            Statement::Show(..) => {
1846                if self.auto_transactions && *in_transaction {
1847                    self.active_client().execute("COMMIT", &[]).await?;
1848                    *in_transaction = false;
1849                }
1850            }
1851            _ => (),
1852        }
1853        Ok(PrepareQueryOutcome::QueryPrepared(QueryInfo {
1854            is_select,
1855            num_attributes,
1856            has_as_of,
1857        }))
1858    }
1859
1860    #[allow(clippy::disallowed_methods)]
1861    async fn execute_query<'r>(
1862        &self,
1863        sql: &str,
1864        output: &'r Result<QueryOutput<'_>, &'r str>,
1865        location: Location,
1866        replacements: &[(Regex, String)],
1867    ) -> Result<Outcome<'r>, anyhow::Error> {
1868        let rows = match self.active_client().query(sql, &[]).await {
1869            Ok(rows) => rows,
1870            Err(error) => {
1871                let error_string = error.to_string_with_causes();
1872                return match output {
1873                    Ok(_) => {
1874                        if error_string.contains("supported") || error_string.contains("overload") {
1875                            // this is a failure, but it's caused by lack of support rather than by bugs
1876                            Ok(Outcome::Unsupported {
1877                                error: anyhow!(error),
1878                                location,
1879                            })
1880                        } else {
1881                            Ok(Outcome::PlanFailure {
1882                                error: anyhow!(error),
1883                                expected_error: None,
1884                                location,
1885                            })
1886                        }
1887                    }
1888                    Err(expected_error) => {
1889                        if error_matches(expected_error, &error_string) {
1890                            Ok(Outcome::Success)
1891                        } else {
1892                            Ok(Outcome::PlanFailure {
1893                                error: anyhow!(error),
1894                                expected_error: Some(expected_error.to_string()),
1895                                location,
1896                            })
1897                        }
1898                    }
1899                };
1900            }
1901        };
1902
1903        // unpack expected output
1904        let QueryOutput {
1905            sort,
1906            types: expected_types,
1907            column_names: expected_column_names,
1908            output: expected_output,
1909            mode,
1910            ..
1911        } = match output {
1912            Err(expected_error) => {
1913                return Ok(Outcome::UnexpectedPlanSuccess {
1914                    expected_error,
1915                    location,
1916                });
1917            }
1918            Ok(query_output) => query_output,
1919        };
1920
1921        // format output
1922        let mut formatted_rows = vec![];
1923        for row in &rows {
1924            if row.len() != expected_types.len() {
1925                return Ok(Outcome::WrongColumnCount {
1926                    expected_count: expected_types.len(),
1927                    actual_count: row.len(),
1928                    location,
1929                });
1930            }
1931            let row = format_row(row, expected_types, *mode);
1932            formatted_rows.push(row);
1933        }
1934
1935        // sort formatted output
1936        if let Sort::Row = sort {
1937            formatted_rows.sort();
1938        }
1939        let mut values = formatted_rows.into_iter().flatten().collect::<Vec<_>>();
1940        if let Sort::Value = sort {
1941            values.sort();
1942        }
1943
1944        // Apply any `replace` substitutions to the actual output, so that
1945        // non-deterministic tokens (e.g. a folded `mz_now()` timestamp) are
1946        // masked before the output is compared against, or rewritten into, the
1947        // expected output.
1948        if !replacements.is_empty() {
1949            for value in &mut values {
1950                for (regex, replacement) in replacements {
1951                    *value = regex.replace_all(value, replacement.as_str()).into_owned();
1952                }
1953            }
1954        }
1955
1956        // Various checks as long as there are returned rows.
1957        if let Some(row) = rows.get(0) {
1958            // check column names
1959            if let Some(expected_column_names) = expected_column_names {
1960                let actual_column_names = row
1961                    .columns()
1962                    .iter()
1963                    .map(|t| ColumnName::from(t.name()))
1964                    .collect::<Vec<_>>();
1965                if expected_column_names != &actual_column_names {
1966                    return Ok(Outcome::WrongColumnNames {
1967                        expected_column_names,
1968                        actual_column_names,
1969                        actual_output: Output::Values(values),
1970                        location,
1971                    });
1972                }
1973            }
1974        }
1975
1976        // check output
1977        match expected_output {
1978            Output::Values(expected_values) => {
1979                if values != *expected_values {
1980                    return Ok(Outcome::OutputFailure {
1981                        expected_output,
1982                        actual_raw_output: rows,
1983                        actual_output: Output::Values(values),
1984                        location,
1985                    });
1986                }
1987            }
1988            Output::Hashed {
1989                num_values,
1990                md5: expected_md5,
1991            } => {
1992                let mut hasher = Md5::new();
1993                for value in &values {
1994                    hasher.update(value);
1995                    hasher.update("\n");
1996                }
1997                let md5 = hex::encode(hasher.finalize());
1998                if values.len() != *num_values || md5 != *expected_md5 {
1999                    return Ok(Outcome::OutputFailure {
2000                        expected_output,
2001                        actual_raw_output: rows,
2002                        actual_output: Output::Hashed {
2003                            num_values: values.len(),
2004                            md5,
2005                        },
2006                        location,
2007                    });
2008                }
2009            }
2010        }
2011
2012        Ok(Outcome::Success)
2013    }
2014
2015    #[allow(clippy::disallowed_methods)]
2016    async fn execute_view_inner<'r>(
2017        &self,
2018        sql: &str,
2019        output: &'r Result<QueryOutput<'_>, &'r str>,
2020        location: Location,
2021    ) -> Result<Option<Outcome<'r>>, anyhow::Error> {
2022        print_sql_if(self.stdout, sql, self.verbose);
2023        let sql_result = self.active_client().execute(sql, &[]).await;
2024
2025        // Evaluate if we already reached an outcome or not.
2026        let tentative_outcome = if let Err(view_error) = sql_result {
2027            if let Err(expected_error) = output {
2028                if error_matches(expected_error, &view_error.to_string_with_causes()) {
2029                    Some(Outcome::Success)
2030                } else {
2031                    Some(Outcome::PlanFailure {
2032                        error: view_error.into(),
2033                        expected_error: Some(expected_error.to_string()),
2034                        location: location.clone(),
2035                    })
2036                }
2037            } else {
2038                Some(Outcome::PlanFailure {
2039                    error: view_error.into(),
2040                    expected_error: None,
2041                    location: location.clone(),
2042                })
2043            }
2044        } else {
2045            None
2046        };
2047        Ok(tentative_outcome)
2048    }
2049
2050    #[allow(clippy::disallowed_methods)]
2051    async fn execute_view<'r>(
2052        &self,
2053        sql: &str,
2054        num_attributes: Option<usize>,
2055        output: &'r Result<QueryOutput<'_>, &'r str>,
2056        location: Location,
2057        replacements: &[(Regex, String)],
2058    ) -> Result<Outcome<'r>, anyhow::Error> {
2059        // Create indexed view SQL commands and execute `CREATE VIEW`.
2060        let expected_column_names = if let Ok(QueryOutput { column_names, .. }) = output {
2061            column_names.clone()
2062        } else {
2063            None
2064        };
2065        let (create_view, create_index, view_sql, drop_view) = generate_view_sql(
2066            sql,
2067            Uuid::new_v4().as_simple(),
2068            num_attributes,
2069            expected_column_names,
2070        );
2071        let tentative_outcome = self
2072            .execute_view_inner(create_view.as_str(), output, location.clone())
2073            .await?;
2074
2075        // Either we already have an outcome or alternatively,
2076        // we proceed to index and query the view.
2077        if let Some(view_outcome) = tentative_outcome {
2078            return Ok(view_outcome);
2079        }
2080
2081        let tentative_outcome = self
2082            .execute_view_inner(create_index.as_str(), output, location.clone())
2083            .await?;
2084
2085        let view_outcome;
2086        if let Some(outcome) = tentative_outcome {
2087            view_outcome = outcome;
2088        } else {
2089            print_sql_if(self.stdout, view_sql.as_str(), self.verbose);
2090            view_outcome = self
2091                .execute_query(view_sql.as_str(), output, location.clone(), replacements)
2092                .await?;
2093        }
2094
2095        // Remember to clean up after ourselves by dropping the view.
2096        print_sql_if(self.stdout, drop_view.as_str(), self.verbose);
2097        self.active_client()
2098            .execute(drop_view.as_str(), &[])
2099            .await?;
2100
2101        Ok(view_outcome)
2102    }
2103
2104    async fn run_query<'r>(
2105        &self,
2106        sql: &'r str,
2107        output: &'r Result<QueryOutput<'_>, &'r str>,
2108        location: Location,
2109        in_transaction: &mut bool,
2110        replacements: &[(Regex, String)],
2111    ) -> Result<Outcome<'r>, anyhow::Error> {
2112        let prepare_outcome = self
2113            .prepare_query(sql, output, location.clone(), in_transaction)
2114            .await?;
2115        match prepare_outcome {
2116            PrepareQueryOutcome::QueryPrepared(QueryInfo {
2117                is_select,
2118                num_attributes,
2119                has_as_of,
2120            }) => {
2121                let query_outcome = self
2122                    .execute_query(sql, output, location.clone(), replacements)
2123                    .await?;
2124                // `AS OF` queries are excluded from the indexed-view consistency
2125                // check: re-running them against a freshly created indexed view
2126                // is racy, since the view's `since` can advance past a historical
2127                // `AS OF` and spuriously fail with "could not find a valid
2128                // timestamp" even though the one-shot query succeeded.
2129                if is_select && !has_as_of && self.auto_index_selects && query_outcome.success() {
2130                    let view_outcome = self
2131                        .execute_view(sql, None, output, location.clone(), replacements)
2132                        .await?;
2133
2134                    if !view_outcome.success() {
2135                        // Before producing a failure outcome, we try to obtain a new
2136                        // outcome for view-based execution exploiting analysis of the
2137                        // number of attributes. This two-level strategy can avoid errors
2138                        // produced by column ambiguity in the `SELECT`.
2139                        let view_outcome = if num_attributes.is_some() {
2140                            self.execute_view(
2141                                sql,
2142                                num_attributes,
2143                                output,
2144                                location.clone(),
2145                                replacements,
2146                            )
2147                            .await?
2148                        } else {
2149                            view_outcome
2150                        };
2151
2152                        if !view_outcome.success() {
2153                            let inconsistent_view_outcome = Outcome::InconsistentViewOutcome {
2154                                query_outcome: Box::new(query_outcome),
2155                                view_outcome: Box::new(view_outcome),
2156                                location: location.clone(),
2157                            };
2158                            // Determine if this inconsistent view outcome should be reported
2159                            // as an error or only as a warning.
2160                            let outcome = if should_warn(&inconsistent_view_outcome) {
2161                                Outcome::Warning {
2162                                    cause: Box::new(inconsistent_view_outcome),
2163                                    location: location.clone(),
2164                                }
2165                            } else {
2166                                inconsistent_view_outcome
2167                            };
2168                            return Ok(outcome);
2169                        }
2170                    }
2171                }
2172                Ok(query_outcome)
2173            }
2174            PrepareQueryOutcome::Outcome(outcome) => Ok(outcome),
2175        }
2176    }
2177
2178    async fn get_conn(
2179        &mut self,
2180        name: Option<&str>,
2181        user: Option<&str>,
2182        password: Option<&str>,
2183    ) -> Result<&tokio_postgres::Client, tokio_postgres::Error> {
2184        match name {
2185            None => Ok(&self.client),
2186            Some(name) => {
2187                if !self.clients.contains_key(name) {
2188                    let addr = if matches!(user, Some("mz_system") | Some("mz_support")) {
2189                        self.internal_server_addr
2190                    } else if password.is_some() {
2191                        // Use password server for password authentication
2192                        self.password_server_addr
2193                    } else {
2194                        self.server_addr
2195                    };
2196                    let client = connect(addr, user, password).await?;
2197                    self.clients.insert(name.into(), client);
2198                }
2199                Ok(self.clients.get(name).unwrap())
2200            }
2201        }
2202    }
2203
2204    #[allow(clippy::disallowed_methods)]
2205    async fn run_simple<'r>(
2206        &mut self,
2207        conn: Option<&'r str>,
2208        user: Option<&'r str>,
2209        password: Option<&'r str>,
2210        sql: &'r str,
2211        sort: Sort,
2212        output: &'r Output,
2213        location: Location,
2214    ) -> Result<Outcome<'r>, anyhow::Error> {
2215        let actual = match self.get_conn(conn, user, password).await {
2216            Ok(client) => match client.simple_query(sql).await {
2217                Ok(result) => {
2218                    let mut rows = Vec::new();
2219
2220                    for m in result.into_iter() {
2221                        match m {
2222                            SimpleQueryMessage::Row(row) => {
2223                                let mut s = vec![];
2224                                for i in 0..row.len() {
2225                                    s.push(row.get(i).unwrap_or("NULL"));
2226                                }
2227                                rows.push(s.join(","));
2228                            }
2229                            SimpleQueryMessage::CommandComplete(count) => {
2230                                // This applies any sort on the COMPLETE line as
2231                                // well, but we do the same for the expected output.
2232                                rows.push(format!("COMPLETE {}", count));
2233                            }
2234                            SimpleQueryMessage::RowDescription(_) => {}
2235                            _ => panic!("unexpected"),
2236                        }
2237                    }
2238
2239                    if let Sort::Row = sort {
2240                        rows.sort();
2241                    }
2242
2243                    Output::Values(rows)
2244                }
2245                // Errors can contain multiple lines (say if there are details), and rewrite
2246                // sticks them each on their own line, so we need to split up the lines here to
2247                // each be its own String in the Vec.
2248                Err(error) => Output::Values(
2249                    error
2250                        .to_string_with_causes()
2251                        .lines()
2252                        .map(|s| s.to_string())
2253                        .collect(),
2254                ),
2255            },
2256            Err(error) => Output::Values(
2257                error
2258                    .to_string_with_causes()
2259                    .lines()
2260                    .map(|s| s.to_string())
2261                    .collect(),
2262            ),
2263        };
2264        if *output != actual {
2265            Ok(Outcome::OutputFailure {
2266                expected_output: output,
2267                actual_raw_output: vec![],
2268                actual_output: actual,
2269                location,
2270            })
2271        } else {
2272            Ok(Outcome::Success)
2273        }
2274    }
2275
2276    async fn check_catalog(&self) -> Result<(), anyhow::Error> {
2277        let url = format!(
2278            "http://{}/api/catalog/check",
2279            self.internal_http_server_addr
2280        );
2281        let response: serde_json::Value = reqwest::get(&url).await?.json().await?;
2282
2283        if let Some(inconsistencies) = response.get("err") {
2284            let inconsistencies = serde_json::to_string_pretty(&inconsistencies)
2285                .expect("serializing Value cannot fail");
2286            Err(anyhow::anyhow!("Catalog inconsistency\n{inconsistencies}"))
2287        } else {
2288            Ok(())
2289        }
2290    }
2291}
2292
2293async fn connect(
2294    addr: SocketAddr,
2295    user: Option<&str>,
2296    password: Option<&str>,
2297) -> Result<tokio_postgres::Client, tokio_postgres::Error> {
2298    let mut config = tokio_postgres::Config::new();
2299    config.host(addr.ip().to_string());
2300    config.port(addr.port());
2301    config.user(user.unwrap_or("materialize"));
2302    if let Some(password) = password {
2303        config.password(password);
2304    }
2305    let (client, connection) = config.connect(NoTls).await?;
2306
2307    task::spawn(|| "sqllogictest_connect", async move {
2308        if let Err(e) = connection.await {
2309            eprintln!("connection error: {}", e);
2310        }
2311    });
2312    Ok(client)
2313}
2314
2315pub trait WriteFmt {
2316    fn write_fmt(&self, fmt: fmt::Arguments<'_>);
2317}
2318
2319pub struct RunConfig<'a> {
2320    pub stdout: &'a dyn WriteFmt,
2321    pub stderr: &'a dyn WriteFmt,
2322    pub verbose: bool,
2323    pub quiet: bool,
2324    pub postgres_url: String,
2325    pub prefix: String,
2326    pub no_fail: bool,
2327    pub fail_fast: bool,
2328    pub auto_index_tables: bool,
2329    pub auto_index_selects: bool,
2330    pub auto_transactions: bool,
2331    pub enable_table_keys: bool,
2332    pub orchestrator_process_wrapper: Option<String>,
2333    pub tracing: TracingCliArgs,
2334    pub tracing_handle: TracingHandle,
2335    pub system_parameter_defaults: BTreeMap<String, String>,
2336    /// Persist state is handled specially because:
2337    /// - Persist background workers do not necessarily shut down immediately once the server is
2338    ///   shut down, and may panic if their storage is deleted out from under them.
2339    /// - It's safe for different databases to reference the same state: all data is scoped by UUID.
2340    pub persist_dir: TempDir,
2341    pub replicas: usize,
2342    pub replica_size: String,
2343}
2344
2345/// Indentation used for verbose output of SQL statements.
2346const PRINT_INDENT: usize = 4;
2347
2348fn print_record(config: &RunConfig<'_>, record: &Record) {
2349    match record {
2350        Record::Statement { sql, .. } | Record::Query { sql, .. } => {
2351            print_sql(config.stdout, sql, None)
2352        }
2353        Record::Simple { conn, sql, .. } => print_sql(config.stdout, sql, *conn),
2354        Record::Copy {
2355            table_name,
2356            tsv_path,
2357        } => {
2358            writeln!(
2359                config.stdout,
2360                "{}slt copy {} from {}",
2361                " ".repeat(PRINT_INDENT),
2362                table_name,
2363                tsv_path
2364            )
2365        }
2366        Record::User { user, .. } => {
2367            writeln!(config.stdout, "{}user {}", " ".repeat(PRINT_INDENT), user)
2368        }
2369        Record::ResetServer => {
2370            writeln!(config.stdout, "{}reset-server", " ".repeat(PRINT_INDENT))
2371        }
2372        Record::Halt => {
2373            writeln!(config.stdout, "{}halt", " ".repeat(PRINT_INDENT))
2374        }
2375        Record::HashThreshold { threshold } => {
2376            writeln!(
2377                config.stdout,
2378                "{}hash-threshold {}",
2379                " ".repeat(PRINT_INDENT),
2380                threshold
2381            )
2382        }
2383        Record::Replace {
2384            pattern,
2385            replacement,
2386        } => {
2387            writeln!(
2388                config.stdout,
2389                "{}replace {}  {}",
2390                " ".repeat(PRINT_INDENT),
2391                pattern,
2392                replacement
2393            )
2394        }
2395    }
2396}
2397
2398fn print_sql_if<'a>(stdout: &'a dyn WriteFmt, sql: &str, cond: bool) {
2399    if cond {
2400        print_sql(stdout, sql, None)
2401    }
2402}
2403
2404fn print_sql<'a>(stdout: &'a dyn WriteFmt, sql: &str, conn: Option<&str>) {
2405    let text = if let Some(conn) = conn {
2406        format!("[conn={}] {}", conn, sql)
2407    } else {
2408        sql.to_string()
2409    };
2410    writeln!(stdout, "{}", util::indent(&text, PRINT_INDENT))
2411}
2412
2413/// Regular expressions for matching error messages that should force a plan failure
2414/// in an inconsistent view outcome into a warning if the corresponding query succeeds.
2415const INCONSISTENT_VIEW_OUTCOME_WARNING_REGEXPS: [&str; 9] = [
2416    // The following are unfixable errors in indexed views given our
2417    // current constraints.
2418    "cannot materialize call to",
2419    "SHOW commands are not allowed in views",
2420    "cannot create view with unstable dependencies",
2421    "cannot use wildcard expansions or NATURAL JOINs in a view that depends on system objects",
2422    "no valid schema selected",
2423    r#"system schema '\w+' cannot be modified"#,
2424    r#"permission denied for (SCHEMA|CLUSTER) "(\w+\.)?\w+""#,
2425    // NOTE(vmarcos): Column ambiguity that could not be eliminated by our
2426    // currently implemented syntactic rewrites is considered unfixable.
2427    // In addition, if some column cannot be dealt with, e.g., in `ORDER BY`
2428    // references, we treat this condition as unfixable as well.
2429    r#"column "[\w\?]+" specified more than once"#,
2430    r#"column "(\w+\.)?\w+" does not exist"#,
2431];
2432
2433/// Evaluates if the given outcome should be returned directly or if it should
2434/// be wrapped as a warning. Note that this function should be used for outcomes
2435/// that can be judged in a context-independent manner, i.e., the outcome itself
2436/// provides enough information as to whether a warning should be emitted or not.
2437fn should_warn(outcome: &Outcome) -> bool {
2438    match outcome {
2439        Outcome::InconsistentViewOutcome { view_outcome, .. } => match view_outcome.as_ref() {
2440            Outcome::PlanFailure { error, .. } => {
2441                INCONSISTENT_VIEW_OUTCOME_WARNING_REGEXPS.iter().any(|s| {
2442                    Regex::new(s)
2443                        .expect("unexpected error in regular expression parsing")
2444                        .is_match(&error.to_string_with_causes())
2445                })
2446            }
2447            _ => false,
2448        },
2449        _ => false,
2450    }
2451}
2452
2453pub async fn run_string(
2454    runner: &mut Runner<'_>,
2455    source: &str,
2456    input: &str,
2457) -> Result<Outcomes, anyhow::Error> {
2458    runner.reset_database().await?;
2459    // `replace` directives are scoped to a single file; clear any registered by
2460    // a previous file so they do not leak into this one.
2461    runner.replacements.clear();
2462
2463    let mut outcomes = Outcomes::default();
2464    let mut parser = crate::parser::Parser::new(source, input);
2465    // Transactions are currently relatively slow. Since sqllogictest runs in a single connection
2466    // there should be no difference in having longer running transactions.
2467    let mut in_transaction = false;
2468    writeln!(runner.config.stdout, "--- {}", source);
2469
2470    for record in parser.parse_records()? {
2471        // In maximal-verbose mode, print the query before attempting to run
2472        // it. Running the query might panic, so it is important to print out
2473        // what query we are trying to run *before* we panic.
2474        if runner.config.verbose {
2475            print_record(runner.config, &record);
2476        }
2477
2478        let outcome = runner
2479            .run_record(&record, &mut in_transaction)
2480            .await
2481            .map_err(|err| format!("In {}:\n{}", source, err))
2482            .unwrap();
2483
2484        // Print warnings and failures in verbose mode.
2485        if !runner.config.quiet && !outcome.success() {
2486            if !runner.config.verbose {
2487                // If `verbose` is enabled, we'll already have printed the record,
2488                // so don't print it again. Yes, this is an ugly bit of logic.
2489                // Please don't try to consolidate it with the `print_record`
2490                // call above, as it's important to have a mode in which records
2491                // are printed before they are run, so that if running the
2492                // record panics, you can tell which record caused it.
2493                if !outcome.failure() {
2494                    writeln!(
2495                        runner.config.stdout,
2496                        "{}",
2497                        util::indent("Warning detected for: ", 4)
2498                    );
2499                }
2500                print_record(runner.config, &record);
2501            }
2502            if runner.config.verbose || outcome.failure() {
2503                writeln!(
2504                    runner.config.stdout,
2505                    "{}",
2506                    util::indent(&outcome.to_string(), 4)
2507                );
2508                writeln!(runner.config.stdout, "{}", util::indent("----", 4));
2509            }
2510        }
2511
2512        outcomes.stats[outcome.code()] += 1;
2513        if outcome.failure() {
2514            outcomes.details.push(format!("{}", outcome));
2515        }
2516
2517        if let Outcome::Bail { .. } = outcome {
2518            break;
2519        }
2520
2521        if runner.config.fail_fast && outcome.failure() {
2522            break;
2523        }
2524    }
2525    Ok(outcomes)
2526}
2527
2528pub async fn run_file(runner: &mut Runner<'_>, filename: &Path) -> Result<Outcomes, anyhow::Error> {
2529    let mut input = String::new();
2530    File::open(filename)?.read_to_string(&mut input)?;
2531    let outcomes = run_string(runner, &format!("{}", filename.display()), &input).await?;
2532    runner.check_catalog().await?;
2533
2534    Ok(outcomes)
2535}
2536
2537pub async fn rewrite_file(runner: &mut Runner<'_>, filename: &Path) -> Result<(), anyhow::Error> {
2538    runner.reset_database().await?;
2539    // `replace` directives are scoped to a single file; clear any registered by
2540    // a previous file so they do not leak into this one.
2541    runner.replacements.clear();
2542
2543    let mut file = OpenOptions::new().read(true).write(true).open(filename)?;
2544
2545    let mut input = String::new();
2546    file.read_to_string(&mut input)?;
2547
2548    let mut buf = RewriteBuffer::new(&input);
2549
2550    let mut parser = crate::parser::Parser::new(filename.to_str().unwrap_or(""), &input);
2551    writeln!(runner.config.stdout, "--- {}", filename.display());
2552    let mut in_transaction = false;
2553
2554    fn append_values_output(
2555        buf: &mut RewriteBuffer,
2556        input: &String,
2557        expected_output: &str,
2558        mode: &Mode,
2559        types: &Vec<Type>,
2560        column_names: Option<&Vec<ColumnName>>,
2561        actual_output: &Vec<String>,
2562        multiline: bool,
2563    ) {
2564        buf.append_header(input, expected_output, column_names);
2565
2566        for (i, row) in actual_output.chunks(types.len()).enumerate() {
2567            match mode {
2568                // In Cockroach mode, output each row on its own line, with
2569                // two spaces between each column.
2570                Mode::Cockroach => {
2571                    if i != 0 {
2572                        buf.append("\n");
2573                    }
2574
2575                    if row.len() == 0 {
2576                        // nothing to do
2577                    } else if row.len() == 1 {
2578                        // If there is only one column, then there is no need for space
2579                        // substitution, so we only do newline substitution.
2580                        if multiline {
2581                            buf.append(&row[0]);
2582                        } else {
2583                            buf.append(&row[0].replace('\n', "⏎"))
2584                        }
2585                    } else {
2586                        // Substitute spaces with ␠ to avoid mistaking the spaces in the result
2587                        // values with spaces that separate columns.
2588                        buf.append(
2589                            &row.iter()
2590                                .map(|col| {
2591                                    let mut col = col.replace(' ', "␠");
2592                                    if !multiline {
2593                                        col = col.replace('\n', "⏎");
2594                                    }
2595                                    col
2596                                })
2597                                .join("  "),
2598                        );
2599                    }
2600                }
2601                // In standard mode, output each value on its own line,
2602                // and ignore row boundaries.
2603                // No need to substitute spaces, because every value (not row) is on a separate
2604                // line. But we do need to substitute newlines.
2605                Mode::Standard => {
2606                    for (j, col) in row.iter().enumerate() {
2607                        if i != 0 || j != 0 {
2608                            buf.append("\n");
2609                        }
2610                        buf.append(&if multiline {
2611                            col.clone()
2612                        } else {
2613                            col.replace('\n', "⏎")
2614                        });
2615                    }
2616                }
2617            }
2618        }
2619    }
2620
2621    for record in parser.parse_records()? {
2622        let outcome = runner.run_record(&record, &mut in_transaction).await?;
2623
2624        match (&record, &outcome) {
2625            // If we see an output failure for a query, rewrite the expected output
2626            // to match the observed output.
2627            (
2628                Record::Query {
2629                    output:
2630                        Ok(QueryOutput {
2631                            mode,
2632                            output: Output::Values(_),
2633                            output_str: expected_output,
2634                            types,
2635                            column_names,
2636                            multiline,
2637                            ..
2638                        }),
2639                    ..
2640                },
2641                Outcome::OutputFailure {
2642                    actual_output: Output::Values(actual_output),
2643                    ..
2644                },
2645            ) => {
2646                append_values_output(
2647                    &mut buf,
2648                    &input,
2649                    expected_output,
2650                    mode,
2651                    types,
2652                    column_names.as_ref(),
2653                    actual_output,
2654                    *multiline,
2655                );
2656            }
2657            (
2658                Record::Query {
2659                    output:
2660                        Ok(QueryOutput {
2661                            mode,
2662                            output: Output::Values(_),
2663                            output_str: expected_output,
2664                            types,
2665                            multiline,
2666                            ..
2667                        }),
2668                    ..
2669                },
2670                Outcome::WrongColumnNames {
2671                    actual_column_names,
2672                    actual_output: Output::Values(actual_output),
2673                    ..
2674                },
2675            ) => {
2676                append_values_output(
2677                    &mut buf,
2678                    &input,
2679                    expected_output,
2680                    mode,
2681                    types,
2682                    Some(actual_column_names),
2683                    actual_output,
2684                    *multiline,
2685                );
2686            }
2687            (
2688                Record::Query {
2689                    output:
2690                        Ok(QueryOutput {
2691                            output: Output::Hashed { .. },
2692                            output_str: expected_output,
2693                            column_names,
2694                            ..
2695                        }),
2696                    ..
2697                },
2698                Outcome::OutputFailure {
2699                    actual_output: Output::Hashed { num_values, md5 },
2700                    ..
2701                },
2702            ) => {
2703                buf.append_header(&input, expected_output, column_names.as_ref());
2704
2705                buf.append(format!("{} values hashing to {}\n", num_values, md5).as_str())
2706            }
2707            (
2708                Record::Simple {
2709                    output_str: expected_output,
2710                    ..
2711                },
2712                Outcome::OutputFailure {
2713                    actual_output: Output::Values(actual_output),
2714                    ..
2715                },
2716            ) => {
2717                buf.append_header(&input, expected_output, None);
2718
2719                for (i, row) in actual_output.iter().enumerate() {
2720                    if i != 0 {
2721                        buf.append("\n");
2722                    }
2723                    buf.append(row);
2724                }
2725            }
2726            (
2727                Record::Query {
2728                    sql,
2729                    output: Err(err),
2730                    ..
2731                },
2732                outcome,
2733            )
2734            | (
2735                Record::Statement {
2736                    expected_error: Some(err),
2737                    sql,
2738                    ..
2739                },
2740                outcome,
2741            ) if outcome.err_msg().is_some() => {
2742                buf.rewrite_expected_error(&input, err, &outcome.err_msg().unwrap(), sql)
2743            }
2744            (_, Outcome::Success) => {}
2745            _ => bail!("unexpected: {:?} {:?}", record, outcome),
2746        }
2747    }
2748
2749    file.set_len(0)?;
2750    file.seek(SeekFrom::Start(0))?;
2751    file.write_all(buf.finish().as_bytes())?;
2752    file.sync_all()?;
2753    Ok(())
2754}
2755
2756/// Provides a means to rewrite the `.slt` file while iterating over it.
2757///
2758/// This struct takes the slt file as its `input`, tracks a cursor into it
2759/// (`input_offset`), and provides a buffer (`output`) to store the rewritten
2760/// results.
2761///
2762/// Functions that modify the file will lazily move `input` into `output` using
2763/// `flush_to`. However, those calls should all be interior to other functions.
2764#[derive(Debug)]
2765struct RewriteBuffer<'a> {
2766    input: &'a str,
2767    input_offset: usize,
2768    output: String,
2769}
2770
2771impl<'a> RewriteBuffer<'a> {
2772    fn new(input: &'a str) -> RewriteBuffer<'a> {
2773        RewriteBuffer {
2774            input,
2775            input_offset: 0,
2776            output: String::new(),
2777        }
2778    }
2779
2780    fn flush_to(&mut self, offset: usize) {
2781        assert!(offset >= self.input_offset);
2782        let chunk = &self.input[self.input_offset..offset];
2783        self.output.push_str(chunk);
2784        self.input_offset = offset;
2785    }
2786
2787    fn skip_to(&mut self, offset: usize) {
2788        assert!(offset >= self.input_offset);
2789        self.input_offset = offset;
2790    }
2791
2792    fn append(&mut self, s: &str) {
2793        self.output.push_str(s);
2794    }
2795
2796    fn append_header(
2797        &mut self,
2798        input: &String,
2799        expected_output: &str,
2800        column_names: Option<&Vec<ColumnName>>,
2801    ) {
2802        // Output everything before this record.
2803        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2804        #[allow(clippy::as_conversions)]
2805        let offset = expected_output.as_ptr() as usize - input.as_ptr() as usize;
2806        self.flush_to(offset);
2807        self.skip_to(offset + expected_output.len());
2808
2809        // Attempt to install the result separator (----), if it does
2810        // not already exist.
2811        if self.peek_last(5) == "\n----" {
2812            self.append("\n");
2813        } else if self.peek_last(6) != "\n----\n" {
2814            self.append("\n----\n");
2815        }
2816
2817        let Some(names) = column_names else {
2818            return;
2819        };
2820        self.append(
2821            &names
2822                .iter()
2823                .map(|name| name.replace(' ', "␠"))
2824                .collect::<Vec<_>>()
2825                .join(" "),
2826        );
2827        self.append("\n");
2828    }
2829
2830    fn rewrite_expected_error(
2831        &mut self,
2832        input: &String,
2833        old_err: &str,
2834        new_err: &str,
2835        query: &str,
2836    ) {
2837        // Output everything before this error message.
2838        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2839        #[allow(clippy::as_conversions)]
2840        let err_offset = old_err.as_ptr() as usize - input.as_ptr() as usize;
2841        self.flush_to(err_offset);
2842        self.append(new_err);
2843        self.append("\n");
2844        self.append(query);
2845        // TODO(benesch): is it possible to rewrite this to avoid `as`?
2846        #[allow(clippy::as_conversions)]
2847        self.skip_to(query.as_ptr() as usize - input.as_ptr() as usize + query.len())
2848    }
2849
2850    fn peek_last(&self, n: usize) -> &str {
2851        &self.output[self.output.len() - n..]
2852    }
2853
2854    fn finish(mut self) -> String {
2855        self.flush_to(self.input.len());
2856        self.output
2857    }
2858}
2859
2860/// Generates view creation, view indexing, view querying, and view
2861/// dropping SQL commands for a given `SELECT` query. If the number
2862/// of attributes produced by the query is known, the view commands
2863/// are specialized to avoid issues with column ambiguity. This
2864/// function is a helper for `--auto_index_selects` and assumes that
2865/// the provided input SQL has already been run through the parser,
2866/// resulting in a valid `SELECT` statement.
2867fn generate_view_sql(
2868    sql: &str,
2869    view_uuid: &Simple,
2870    num_attributes: Option<usize>,
2871    expected_column_names: Option<Vec<ColumnName>>,
2872) -> (String, String, String, String) {
2873    // To create the view, re-parse the sql; note that we must find exactly
2874    // one statement and it must be a `SELECT`.
2875    // NOTE(vmarcos): Direct string manipulation was attempted while
2876    // prototyping the code below, which avoids the extra parsing and
2877    // data structure cloning. However, running DDL is so slow that
2878    // it did not matter in terms of runtime. We can revisit this if
2879    // DDL cost drops dramatically in the future.
2880    let stmts = parser::parse_statements(sql).unwrap_or_default();
2881    assert!(stmts.len() == 1);
2882    let (query, query_as_of) = match &stmts[0].ast {
2883        Statement::Select(stmt) => (&stmt.query, &stmt.as_of),
2884        _ => unreachable!("This function should only be called for SELECTs"),
2885    };
2886
2887    // Prior to creating the view, process the `ORDER BY` clause of
2888    // the `SELECT` query, if any. Ordering is not preserved when a
2889    // view includes an `ORDER BY` clause and must be re-enforced by
2890    // an external `ORDER BY` clause when querying the view.
2891    let (view_order_by, extra_columns, distinct) = if num_attributes.is_none() {
2892        (query.order_by.clone(), vec![], None)
2893    } else {
2894        derive_order_by(&query.body, &query.order_by)
2895    };
2896
2897    // Since one-shot SELECT statements may contain ambiguous column names,
2898    // we either use the expected column names, if that option was
2899    // provided, or else just rename the output schema of the view
2900    // using numerically increasing attribute names, whenever possible.
2901    // This strategy makes it possible to use `CREATE INDEX`, thus
2902    // matching the behavior of the option `auto_index_tables`. However,
2903    // we may be presented with a `SELECT *` query, in which case the parser
2904    // does not produce sufficient information to allow us to compute
2905    // the number of output columns. In the latter case, we are supplied
2906    // with `None` for `num_attributes` and just employ the command
2907    // `CREATE DEFAULT INDEX` instead. Additionally, the view is created
2908    // without schema renaming. This strategy is insufficient to dodge
2909    // column name ambiguity in all cases, but we assume here that we
2910    // can adjust the (hopefully) small number of tests that eventually
2911    // challenge us in this particular way.
2912    let name = UnresolvedItemName(vec![Ident::new_unchecked(format!("v{}", view_uuid))]);
2913    let projection = expected_column_names.map_or_else(
2914        || {
2915            num_attributes.map_or(vec![], |n| {
2916                (1..=n)
2917                    .map(|i| Ident::new_unchecked(format!("a{i}")))
2918                    .collect()
2919            })
2920        },
2921        |cols| {
2922            cols.iter()
2923                .map(|c| Ident::new_unchecked(c.as_str()))
2924                .collect()
2925        },
2926    );
2927    let columns: Vec<Ident> = projection
2928        .iter()
2929        .cloned()
2930        .chain(extra_columns.iter().map(|item| {
2931            if let SelectItem::Expr {
2932                expr: _,
2933                alias: Some(ident),
2934            } = item
2935            {
2936                ident.clone()
2937            } else {
2938                unreachable!("alias must be given for extra column")
2939            }
2940        }))
2941        .collect();
2942
2943    // Build a `CREATE VIEW` with the columns computed above.
2944    let mut query = query.clone();
2945    if extra_columns.len() > 0 {
2946        match &mut query.body {
2947            SetExpr::Select(stmt) => stmt.projection.extend(extra_columns.iter().cloned()),
2948            _ => unimplemented!("cannot yet rewrite projections of nested queries"),
2949        }
2950    }
2951    let create_view = AstStatement::<Raw>::CreateView(CreateViewStatement {
2952        if_exists: IfExistsBehavior::Error,
2953        temporary: false,
2954        definition: ViewDefinition {
2955            name: name.clone(),
2956            columns: columns.clone(),
2957            query,
2958        },
2959    })
2960    .to_ast_string_stable();
2961
2962    // We then create either a `CREATE INDEX` or a `CREATE DEFAULT INDEX`
2963    // statement, depending on whether we could obtain the number of
2964    // attributes from the original `SELECT`.
2965    let create_index = AstStatement::<Raw>::CreateIndex(CreateIndexStatement {
2966        name: None,
2967        in_cluster: None,
2968        on_name: RawItemName::Name(name.clone()),
2969        key_parts: if columns.len() == 0 {
2970            None
2971        } else {
2972            Some(
2973                columns
2974                    .iter()
2975                    .map(|ident| Expr::Identifier(vec![ident.clone()]))
2976                    .collect(),
2977            )
2978        },
2979        with_options: Vec::new(),
2980        if_not_exists: false,
2981    })
2982    .to_ast_string_stable();
2983
2984    // Assert if DISTINCT semantics are unchanged from view
2985    let distinct_unneeded = extra_columns.len() == 0
2986        || match distinct {
2987            None | Some(Distinct::On(_)) => true,
2988            Some(Distinct::EntireRow) => false,
2989        };
2990    let distinct = if distinct_unneeded { None } else { distinct };
2991
2992    // `SELECT [* | {projection}] FROM {name} [ORDER BY {view_order_by}]`
2993    let view_sql = AstStatement::<Raw>::Select(SelectStatement {
2994        query: Query {
2995            ctes: CteBlock::Simple(vec![]),
2996            body: SetExpr::Select(Box::new(Select {
2997                distinct,
2998                projection: if projection.len() == 0 {
2999                    vec![SelectItem::Wildcard]
3000                } else {
3001                    projection
3002                        .iter()
3003                        .map(|ident| SelectItem::Expr {
3004                            expr: Expr::Identifier(vec![ident.clone()]),
3005                            alias: None,
3006                        })
3007                        .collect()
3008                },
3009                from: vec![TableWithJoins {
3010                    relation: TableFactor::Table {
3011                        name: RawItemName::Name(name.clone()),
3012                        alias: None,
3013                    },
3014                    joins: vec![],
3015                }],
3016                selection: None,
3017                group_by: vec![],
3018                having: None,
3019                qualify: None,
3020                options: vec![],
3021            })),
3022            order_by: view_order_by,
3023            limit: None,
3024            offset: None,
3025        },
3026        as_of: query_as_of.clone(),
3027    })
3028    .to_ast_string_stable();
3029
3030    // `DROP VIEW {name}`
3031    let drop_view = AstStatement::<Raw>::DropObjects(DropObjectsStatement {
3032        object_type: ObjectType::View,
3033        if_exists: false,
3034        names: vec![UnresolvedObjectName::Item(name)],
3035        cascade: false,
3036    })
3037    .to_ast_string_stable();
3038
3039    (create_view, create_index, view_sql, drop_view)
3040}
3041
3042/// Analyzes the provided query `body` to derive the number of
3043/// attributes in the query. We only consider syntactic cues,
3044/// so we may end up deriving `None` for the number of attributes
3045/// as a conservative approximation.
3046fn derive_num_attributes(body: &SetExpr<Raw>) -> Option<usize> {
3047    let Some((projection, _)) = find_projection(body) else {
3048        return None;
3049    };
3050    derive_num_attributes_from_projection(projection)
3051}
3052
3053/// Analyzes a query's `ORDER BY` clause to derive an `ORDER BY`
3054/// clause that makes numeric references to any expressions in
3055/// the projection and generated-attribute references to expressions
3056/// that need to be added as extra columns to the projection list.
3057/// The rewritten `ORDER BY` clause is then usable when querying a
3058/// view that contains the same `SELECT` as the given query.
3059/// This function returns both the rewritten `ORDER BY` clause
3060/// as well as a list of extra columns that need to be added
3061/// to the query's projection for the `ORDER BY` clause to
3062/// succeed.
3063fn derive_order_by(
3064    body: &SetExpr<Raw>,
3065    order_by: &Vec<OrderByExpr<Raw>>,
3066) -> (
3067    Vec<OrderByExpr<Raw>>,
3068    Vec<SelectItem<Raw>>,
3069    Option<Distinct<Raw>>,
3070) {
3071    let Some((projection, distinct)) = find_projection(body) else {
3072        return (vec![], vec![], None);
3073    };
3074    let (view_order_by, extra_columns) = derive_order_by_from_projection(projection, order_by);
3075    (view_order_by, extra_columns, distinct.clone())
3076}
3077
3078/// Finds the projection list in a `SELECT` query body.
3079fn find_projection(body: &SetExpr<Raw>) -> Option<(&Vec<SelectItem<Raw>>, &Option<Distinct<Raw>>)> {
3080    // Iterate to peel off the query body until the query's
3081    // projection list is found.
3082    let mut set_expr = body;
3083    loop {
3084        match set_expr {
3085            SetExpr::Select(select) => {
3086                return Some((&select.projection, &select.distinct));
3087            }
3088            SetExpr::SetOperation { left, .. } => set_expr = left.as_ref(),
3089            SetExpr::Query(query) => set_expr = &query.body,
3090            _ => return None,
3091        }
3092    }
3093}
3094
3095/// Computes the number of attributes that are obtained by the
3096/// projection of a `SELECT` query. The projection may include
3097/// wildcards, in which case the analysis just returns `None`.
3098fn derive_num_attributes_from_projection(projection: &Vec<SelectItem<Raw>>) -> Option<usize> {
3099    let mut num_attributes = 0usize;
3100    for item in projection.iter() {
3101        let SelectItem::Expr { expr, .. } = item else {
3102            return None;
3103        };
3104        match expr {
3105            Expr::QualifiedWildcard(..) | Expr::WildcardAccess(..) => {
3106                return None;
3107            }
3108            _ => {
3109                num_attributes += 1;
3110            }
3111        }
3112    }
3113    Some(num_attributes)
3114}
3115
3116/// Computes an `ORDER BY` clause with only numeric references
3117/// from given projection and `ORDER BY` of a `SELECT` query.
3118/// If the derivation fails to match a given expression, the
3119/// matched prefix is returned. Note that this could be empty.
3120fn derive_order_by_from_projection(
3121    projection: &Vec<SelectItem<Raw>>,
3122    order_by: &Vec<OrderByExpr<Raw>>,
3123) -> (Vec<OrderByExpr<Raw>>, Vec<SelectItem<Raw>>) {
3124    let mut view_order_by: Vec<OrderByExpr<Raw>> = vec![];
3125    let mut extra_columns: Vec<SelectItem<Raw>> = vec![];
3126    for order_by_expr in order_by.iter() {
3127        let query_expr = &order_by_expr.expr;
3128        let view_expr = match query_expr {
3129            Expr::Value(mz_sql_parser::ast::Value::Number(_)) => query_expr.clone(),
3130            _ => {
3131                // Find expression in query projection, if we can.
3132                if let Some(i) = projection.iter().position(|item| match item {
3133                    SelectItem::Expr { expr, alias } => {
3134                        expr == query_expr
3135                            || match query_expr {
3136                                Expr::Identifier(ident) => {
3137                                    ident.len() == 1 && Some(&ident[0]) == alias.as_ref()
3138                                }
3139                                _ => false,
3140                            }
3141                    }
3142                    SelectItem::Wildcard => false,
3143                }) {
3144                    Expr::Value(mz_sql_parser::ast::Value::Number((i + 1).to_string()))
3145                } else {
3146                    // If the expression is not found in the
3147                    // projection, add extra column.
3148                    let ident = Ident::new_unchecked(format!(
3149                        "a{}",
3150                        (projection.len() + extra_columns.len() + 1)
3151                    ));
3152                    extra_columns.push(SelectItem::Expr {
3153                        expr: query_expr.clone(),
3154                        alias: Some(ident.clone()),
3155                    });
3156                    Expr::Identifier(vec![ident])
3157                }
3158            }
3159        };
3160        view_order_by.push(OrderByExpr {
3161            expr: view_expr,
3162            asc: order_by_expr.asc,
3163            nulls_last: order_by_expr.nulls_last,
3164        });
3165    }
3166    (view_order_by, extra_columns)
3167}
3168
3169/// Returns extra statements to execute after `stmt` is executed.
3170fn mutate(sql: &str) -> Vec<String> {
3171    let stmts = parser::parse_statements(sql).unwrap_or_default();
3172    let mut additional = Vec::new();
3173    for stmt in stmts {
3174        match stmt.ast {
3175            AstStatement::CreateTable(stmt) => additional.push(
3176                // CREATE TABLE -> CREATE INDEX. Specify all columns manually in case CREATE
3177                // DEFAULT INDEX ever goes away.
3178                AstStatement::<Raw>::CreateIndex(CreateIndexStatement {
3179                    name: None,
3180                    in_cluster: None,
3181                    on_name: RawItemName::Name(stmt.name.clone()),
3182                    key_parts: Some(
3183                        stmt.columns
3184                            .iter()
3185                            .map(|def| Expr::Identifier(vec![def.name.clone()]))
3186                            .collect(),
3187                    ),
3188                    with_options: Vec::new(),
3189                    if_not_exists: false,
3190                })
3191                .to_ast_string_stable(),
3192            ),
3193            _ => {}
3194        }
3195    }
3196    additional
3197}
3198
3199#[mz_ore::test]
3200#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
3201fn test_generate_view_sql() {
3202    let uuid = Uuid::parse_str("67e5504410b1426f9247bb680e5fe0c8").unwrap();
3203    let cases = vec![
3204        (("SELECT * FROM t", None, None),
3205        (
3206            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM "t""#.to_string(),
3207            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3208            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3209            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3210        )),
3211        (("SELECT a, b, c FROM t1, t2", Some(3), Some(vec![ColumnName::from("a"), ColumnName::from("b"), ColumnName::from("c")])),
3212        (
3213            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c") AS SELECT "a", "b", "c" FROM "t1", "t2""#.to_string(),
3214            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c")"#.to_string(),
3215            r#"SELECT "a", "b", "c" FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3216            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3217        )),
3218        (("SELECT a, b, c FROM t1, t2", Some(3), None),
3219        (
3220            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3") AS SELECT "a", "b", "c" FROM "t1", "t2""#.to_string(),
3221            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3222            r#"SELECT "a1", "a2", "a3" FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3223            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3224        )),
3225        // A case with ambiguity that is accepted by the function, illustrating that
3226        // our measures to dodge this issue are imperfect.
3227        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a)", None, None),
3228        (
3229            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a")"#.to_string(),
3230            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3231            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3232            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3233        )),
3234        (("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")])),
3235        (
3236            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(),
3237            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a", "b", "c", "d")"#.to_string(),
3238            r#"SELECT "a", "b", "c", "d" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1, 3, 4"#.to_string(),
3239            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3240        )),
3241        (("((SELECT 1 AS a UNION SELECT 2 AS b) UNION SELECT 3 AS c) ORDER BY a", Some(1), None),
3242        (
3243            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(),
3244            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1")"#.to_string(),
3245            r#"SELECT "a1" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1"#.to_string(),
3246            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3247        )),
3248        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a) ORDER BY 1", None, None),
3249        (
3250            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a") ORDER BY 1"#.to_string(),
3251            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3252            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1"#.to_string(),
3253            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3254        )),
3255        (("SELECT * FROM (SELECT a, sum(b) AS a FROM t GROUP BY a) ORDER BY a", None, None),
3256        (
3257            r#"CREATE VIEW "v67e5504410b1426f9247bb680e5fe0c8" AS SELECT * FROM (SELECT "a", "sum"("b") AS "a" FROM "t" GROUP BY "a") ORDER BY "a""#.to_string(),
3258            r#"CREATE DEFAULT INDEX ON "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3259            r#"SELECT * FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY "a""#.to_string(),
3260            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3261        )),
3262        (("SELECT a, sum(b) AS a FROM t GROUP BY a, c ORDER BY a, c", Some(2), None),
3263        (
3264            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(),
3265            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3266            r#"SELECT "a1", "a2" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY 1, "a3""#.to_string(),
3267            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3268        )),
3269        (("SELECT a, sum(b) AS a FROM t GROUP BY a, c ORDER BY c, a", Some(2), None),
3270        (
3271            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(),
3272            r#"CREATE INDEX ON "v67e5504410b1426f9247bb680e5fe0c8" ("a1", "a2", "a3")"#.to_string(),
3273            r#"SELECT "a1", "a2" FROM "v67e5504410b1426f9247bb680e5fe0c8" ORDER BY "a3", 1"#.to_string(),
3274            r#"DROP VIEW "v67e5504410b1426f9247bb680e5fe0c8""#.to_string(),
3275        )),
3276    ];
3277    for ((sql, num_attributes, expected_column_names), expected) in cases {
3278        let view_sql =
3279            generate_view_sql(sql, uuid.as_simple(), num_attributes, expected_column_names);
3280        assert_eq!(expected, view_sql);
3281    }
3282}
3283
3284#[mz_ore::test]
3285fn test_mutate() {
3286    let cases = vec![
3287        ("CREATE TABLE t ()", vec![r#"CREATE INDEX ON "t" ()"#]),
3288        (
3289            "CREATE TABLE t (a INT)",
3290            vec![r#"CREATE INDEX ON "t" ("a")"#],
3291        ),
3292        (
3293            "CREATE TABLE t (a INT, b TEXT)",
3294            vec![r#"CREATE INDEX ON "t" ("a", "b")"#],
3295        ),
3296        // Invalid syntax works, just returns nothing.
3297        ("BAD SYNTAX", Vec::new()),
3298    ];
3299    for (sql, expected) in cases {
3300        let stmts = mutate(sql);
3301        assert_eq!(expected, stmts, "sql: {sql}");
3302    }
3303}
3304
3305#[mz_ore::test]
3306fn test_strip_crdb_table_items() {
3307    let cases = vec![
3308        (
3309            "CREATE TABLE t (a INT, INDEX foo (a), b INT)",
3310            Some("CREATE TABLE t (a INT, b INT)"),
3311        ),
3312        (
3313            "CREATE TABLE t (a INT, INDEX(a))",
3314            Some("CREATE TABLE t (a INT)"),
3315        ),
3316        (
3317            "CREATE TABLE t (a INT, UNIQUE INDEX foo (a), INVERTED INDEX bar (a))",
3318            Some("CREATE TABLE t (a INT)"),
3319        ),
3320        (
3321            "CREATE TABLE t (k INT, v STRING, FAMILY \"primary\" (k, v))",
3322            Some("CREATE TABLE t (k INT, v STRING)"),
3323        ),
3324        // Partial and expression indexes have nested clauses.
3325        (
3326            "CREATE TABLE t (a INT, INDEX (a) WHERE a = 0, INDEX ((a + 1)))",
3327            Some("CREATE TABLE t (a INT)"),
3328        ),
3329        // Constraints are kept.
3330        (
3331            "CREATE TABLE t (a INT, UNIQUE (a), CHECK (a > 0))",
3332            Some("CREATE TABLE t (a INT, UNIQUE (a), CHECK (a > 0))"),
3333        ),
3334        // Column names that merely start with a stripped keyword are kept.
3335        (
3336            "CREATE TABLE t (index_col INT, \"index\" INT)",
3337            Some("CREATE TABLE t (index_col INT, \"index\" INT)"),
3338        ),
3339        // Parens inside strings do not confuse the item splitter.
3340        (
3341            "CREATE TABLE t (a TEXT DEFAULT 'a,(b', INDEX (a))",
3342            Some("CREATE TABLE t (a TEXT DEFAULT 'a,(b')"),
3343        ),
3344        ("SELECT 1", None),
3345    ];
3346    for (sql, expected) in cases {
3347        let stripped = strip_crdb_table_items(sql);
3348        assert_eq!(expected.map(|e| e.to_string()), stripped, "sql: {sql}");
3349    }
3350}