1use 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 fn err_msg(&self) -> Option<String> {
206 match self {
207 Outcome::Unsupported { error, .. }
208 | Outcome::ParseFailure { error, .. }
209 | Outcome::PlanFailure { error, .. } => {
210 let err_str = error.to_string_with_causes();
213 let err_str = err_str.split('\n').next().unwrap();
214 let err_str = err_str.strip_prefix("db error: ERROR: ").unwrap_or(err_str);
217 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 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 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 client: tokio_postgres::Client,
473 system_client: tokio_postgres::Client,
474 clients: BTreeMap<String, tokio_postgres::Client>,
475 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(|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
661fn 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
672fn 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 #[allow(clippy::as_conversions)]
704 (Type::Integer, Value::Float4(f)) => format!("{}", f as i64),
705 #[allow(clippy::as_conversions)]
707 (Type::Integer, Value::Float8(f)) => format!("{}", f as i64),
708 (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 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 (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 (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 (_, 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
802fn 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
814fn 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 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 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 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 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 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 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 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 #[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 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 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 #[allow(clippy::disallowed_methods)]
1494 async fn ensure_fixed_features(&self) -> Result<(), anyhow::Error> {
1495 self.system_client
1499 .execute("ALTER SYSTEM SET enable_reduce_mfp_fusion = on", &[])
1500 .await?;
1501
1502 self.system_client
1504 .execute("ALTER SYSTEM SET unsafe_enable_unsafe_functions = on", &[])
1505 .await?;
1506 Ok(())
1507 }
1508
1509 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 #[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 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 return Ok(Outcome::Success);
1680 }
1681
1682 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(row) = rows.get(0) {
1958 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 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 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 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 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 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 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 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 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 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 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 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 pub persist_dir: TempDir,
2341 pub replicas: usize,
2342 pub replica_size: String,
2343}
2344
2345const 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
2413const INCONSISTENT_VIEW_OUTCOME_WARNING_REGEXPS: [&str; 9] = [
2416 "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 r#"column "[\w\?]+" specified more than once"#,
2430 r#"column "(\w+\.)?\w+" does not exist"#,
2431];
2432
2433fn 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 runner.replacements.clear();
2462
2463 let mut outcomes = Outcomes::default();
2464 let mut parser = crate::parser::Parser::new(source, input);
2465 let mut in_transaction = false;
2468 writeln!(runner.config.stdout, "--- {}", source);
2469
2470 for record in parser.parse_records()? {
2471 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 if !runner.config.quiet && !outcome.success() {
2486 if !runner.config.verbose {
2487 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 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 Mode::Cockroach => {
2571 if i != 0 {
2572 buf.append("\n");
2573 }
2574
2575 if row.len() == 0 {
2576 } else if row.len() == 1 {
2578 if multiline {
2581 buf.append(&row[0]);
2582 } else {
2583 buf.append(&row[0].replace('\n', "⏎"))
2584 }
2585 } else {
2586 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 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 (
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#[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 #[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 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 #[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 #[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
2860fn 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 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 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 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 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 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 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 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 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
3042fn 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
3053fn 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
3078fn find_projection(body: &SetExpr<Raw>) -> Option<(&Vec<SelectItem<Raw>>, &Option<Distinct<Raw>>)> {
3080 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
3095fn 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
3116fn 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 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 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
3169fn 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 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)] fn 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 (("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 ("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 (
3326 "CREATE TABLE t (a INT, INDEX (a) WHERE a = 0, INDEX ((a + 1)))",
3327 Some("CREATE TABLE t (a INT)"),
3328 ),
3329 (
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 (
3336 "CREATE TABLE t (index_col INT, \"index\" INT)",
3337 Some("CREATE TABLE t (index_col INT, \"index\" INT)"),
3338 ),
3339 (
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}