1use std::collections::{BTreeMap, BTreeSet};
11use std::future::Future;
12
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::sync::LazyLock;
16use std::time::Duration;
17use std::{env, fs};
18
19use anyhow::{Context, anyhow, bail};
20use async_trait::async_trait;
21use aws_credential_types::provider::ProvideCredentials;
22use aws_types::SdkConfig;
23use futures::future::FutureExt;
24use itertools::Itertools;
25use mz_adapter::catalog::{Catalog, ConnCatalog};
26use mz_adapter::session::Session;
27use mz_build_info::BuildInfo;
28use mz_catalog::config::ClusterReplicaSizeMap;
29use mz_catalog::durable::BootstrapArgs;
30use mz_ccsr::SubjectVersion;
31use mz_kafka_util::client::{MzClientContext, create_new_client_config_simple};
32use mz_ore::error::ErrorExt;
33use mz_ore::metrics::MetricsRegistry;
34use mz_ore::now::SYSTEM_TIME;
35use mz_ore::retry::Retry;
36use mz_ore::task;
37use mz_ore::url::SensitiveUrl;
38use mz_persist_client::cache::PersistClientCache;
39use mz_persist_client::cfg::PersistConfig;
40use mz_persist_client::rpc::PubSubClientConnection;
41use mz_persist_client::{PersistClient, PersistLocation};
42use mz_postgres_util::{
43 Sql, batch_execute as pg_batch_execute, query as pg_query, query_one as pg_query_one,
44 sql as pg_sql,
45};
46use mz_sql::catalog::EnvironmentId;
47use mz_tls_util::make_tls;
48use rdkafka::ClientConfig;
49use rdkafka::producer::Producer;
50use regex::{Captures, Regex};
51use semver::Version;
52use tokio_postgres::CancelToken;
53use tokio_postgres::error::{DbError, SqlState};
54use tracing::info;
55use url::Url;
56
57use crate::error::PosError;
58use crate::parser::{
59 Command, PosCommand, SqlExpectedError, SqlOutput, VersionConstraint, validate_ident,
60};
61use crate::util;
62use crate::util::postgres::postgres_client;
63
64pub mod consistency;
65
66mod duckdb;
67mod file;
68mod fivetran;
69mod glue;
70mod http;
71mod kafka;
72mod mysql;
73mod nop;
74mod persist;
75mod postgres;
76mod protobuf;
77mod psql;
78mod s3;
79mod schema_registry;
80mod set;
81mod skip_end;
82mod skip_if;
83mod sleep;
84mod sql;
85mod sql_server;
86mod version_check;
87mod webhook;
88
89pub(crate) async fn verify_kafka_topics_exhausted(state: &State) -> Result<(), anyhow::Error> {
90 kafka::verify_topics_exhausted(state).await
91}
92
93#[derive(Debug, Clone)]
95pub struct Config {
96 pub arg_vars: BTreeMap<String, String>,
102 pub seed: Option<String>,
104 pub reset: bool,
107 pub temp_dir: Option<String>,
112 pub source: Option<String>,
114 pub default_timeout: Duration,
116 pub default_max_tries: usize,
118 pub initial_backoff: Duration,
122 pub backoff_factor: f64,
126 pub consistency_checks: consistency::Level,
128 pub check_statement_logging: bool,
131 pub rewrite_results: bool,
133
134 pub materialize_pgconfig: tokio_postgres::Config,
138 pub materialize_internal_pgconfig: tokio_postgres::Config,
141 pub materialize_use_https: bool,
143 pub materialize_http_port: u16,
146 pub materialize_internal_http_port: u16,
149 pub materialize_password_sql_port: u16,
152 pub materialize_sasl_sql_port: u16,
155 pub materialize_params: Vec<(String, String)>,
157 pub materialize_catalog_config: Option<CatalogConfig>,
159 pub build_info: &'static BuildInfo,
161 pub materialize_cluster_replica_sizes: ClusterReplicaSizeMap,
163
164 pub persist_consensus_url: Option<SensitiveUrl>,
167 pub persist_blob_url: Option<SensitiveUrl>,
169
170 pub kafka_addr: String,
173 pub kafka_default_partitions: usize,
175 pub kafka_opts: Vec<(String, String)>,
178 pub schema_registry_url: Url,
180 pub cert_path: Option<String>,
185 pub cert_password: Option<String>,
187 pub ccsr_username: Option<String>,
190 pub ccsr_password: Option<String>,
193
194 pub aws_config: SdkConfig,
197 pub aws_account: String,
199
200 pub fivetran_destination_url: String,
203 pub fivetran_destination_files_path: String,
205}
206
207pub struct MaterializeState {
208 catalog_config: Option<CatalogConfig>,
209
210 sql_addr: String,
211 use_https: bool,
212 http_addr: String,
213 internal_sql_addr: String,
214 internal_http_addr: String,
215 password_sql_addr: String,
216 sasl_sql_addr: String,
217 user: String,
218 pgclient: tokio_postgres::Client,
219 environment_id: EnvironmentId,
220 bootstrap_args: BootstrapArgs,
221}
222
223pub struct State {
224 pub config: Config,
226
227 arg_vars: BTreeMap<String, String>,
229 cmd_vars: BTreeMap<String, String>,
230 seed: String,
231 temp_path: PathBuf,
232 _tempfile: Option<tempfile::TempDir>,
233 default_timeout: Duration,
234 timeout: Duration,
235 max_tries: usize,
236 initial_backoff: Duration,
237 backoff_factor: f64,
238 consistency_checks: consistency::Level,
239 check_statement_logging: bool,
240 consistency_checks_adhoc_skip: bool,
241 regex: Option<Regex>,
242 regex_replacement: String,
243 error_line_count: usize,
244 error_string: String,
245
246 materialize: MaterializeState,
248
249 persist_consensus_url: Option<SensitiveUrl>,
251 persist_blob_url: Option<SensitiveUrl>,
252 build_info: &'static BuildInfo,
253 persist_clients: PersistClientCache,
254
255 schema_registry_url: Url,
257 ccsr_client: mz_ccsr::Client,
258 kafka_addr: String,
259 kafka_admin: rdkafka::admin::AdminClient<MzClientContext>,
260 kafka_admin_opts: rdkafka::admin::AdminOptions,
261 kafka_config: ClientConfig,
262 kafka_default_partitions: usize,
263 kafka_producer: rdkafka::producer::FutureProducer<MzClientContext>,
264 kafka_topics: BTreeMap<String, usize>,
265 kafka_verify_topics: BTreeSet<String>,
267
268 aws_account: String,
270 aws_config: SdkConfig,
271
272 pub duckdb_clients: BTreeMap<String, std::sync::Arc<std::sync::Mutex<::duckdb::Connection>>>,
274 mysql_clients: BTreeMap<String, mysql_async::Conn>,
275 postgres_clients: BTreeMap<String, tokio_postgres::Client>,
276 sql_server_clients: BTreeMap<String, mz_sql_server_util::Client>,
277 background_tasks: Vec<BackgroundTask>,
280
281 fivetran_destination_url: String,
283 fivetran_destination_files_path: String,
284
285 rewrite_results: bool,
287 pub rewrites: Vec<Rewrite>,
289 pub rewrite_pos_start: usize,
291 pub rewrite_pos_end: usize,
293}
294
295pub struct Rewrite {
296 pub content: String,
297 pub start: usize,
298 pub end: usize,
299}
300
301pub(crate) struct BackgroundTask {
312 desc: String,
313 handle: task::JoinHandle<Result<(), anyhow::Error>>,
314 cancel_token: CancelToken,
315 url: String,
318}
319
320async fn cancel_background_query(cancel_token: &CancelToken, url: &str, timeout: Duration) {
325 let tls = match tokio_postgres::Config::from_str(url)
326 .map_err(anyhow::Error::from)
327 .and_then(|config| make_tls(&config).map_err(anyhow::Error::from))
328 {
329 Ok(tls) => tls,
330 Err(e) => {
331 tracing::warn!("could not build TLS connector to cancel background query: {e}");
332 return;
333 }
334 };
335 match tokio::time::timeout(timeout, cancel_token.cancel_query(tls)).await {
336 Ok(Ok(())) => {}
337 Ok(Err(e)) => tracing::warn!("cancel request for background query failed: {e}"),
338 Err(_) => tracing::warn!("cancel request for background query timed out"),
339 }
340}
341
342impl State {
343 pub async fn initialize_cmd_vars(&mut self) -> Result<(), anyhow::Error> {
344 self.cmd_vars
345 .insert("testdrive.kafka-addr".into(), self.kafka_addr.clone());
346 self.cmd_vars.insert(
347 "testdrive.schema-registry-url".into(),
348 self.schema_registry_url.to_string(),
349 );
350 self.cmd_vars
351 .insert("testdrive.seed".into(), self.seed.clone());
352 self.cmd_vars.insert(
353 "testdrive.temp-dir".into(),
354 self.temp_path.display().to_string(),
355 );
356 self.cmd_vars
357 .insert("testdrive.aws-region".into(), self.aws_region().into());
358 self.cmd_vars
359 .insert("testdrive.aws-endpoint".into(), self.aws_endpoint().into());
360 self.cmd_vars
361 .insert("testdrive.aws-account".into(), self.aws_account.clone());
362 {
363 let aws_credentials = self
364 .aws_config
365 .credentials_provider()
366 .ok_or_else(|| anyhow!("no AWS credentials provider configured"))?
367 .provide_credentials()
368 .await
369 .context("fetching AWS credentials")?;
370 self.cmd_vars.insert(
371 "testdrive.aws-access-key-id".into(),
372 aws_credentials.access_key_id().to_owned(),
373 );
374 self.cmd_vars.insert(
375 "testdrive.aws-secret-access-key".into(),
376 aws_credentials.secret_access_key().to_owned(),
377 );
378 self.cmd_vars.insert(
379 "testdrive.aws-token".into(),
380 aws_credentials
381 .session_token()
382 .map(|token| token.to_owned())
383 .unwrap_or_else(String::new),
384 );
385 }
386 self.cmd_vars.insert(
387 "testdrive.materialize-environment-id".into(),
388 self.materialize.environment_id.to_string(),
389 );
390 self.cmd_vars.insert(
391 "testdrive.materialize-sql-addr".into(),
392 self.materialize.sql_addr.clone(),
393 );
394 self.cmd_vars.insert(
395 "testdrive.materialize-internal-sql-addr".into(),
396 self.materialize.internal_sql_addr.clone(),
397 );
398 self.cmd_vars.insert(
399 "testdrive.materialize-password-sql-addr".into(),
400 self.materialize.password_sql_addr.clone(),
401 );
402 self.cmd_vars.insert(
403 "testdrive.materialize-sasl-sql-addr".into(),
404 self.materialize.sasl_sql_addr.clone(),
405 );
406 self.cmd_vars.insert(
407 "testdrive.materialize-user".into(),
408 self.materialize.user.clone(),
409 );
410 self.cmd_vars.insert(
411 "testdrive.fivetran-destination-url".into(),
412 self.fivetran_destination_url.clone(),
413 );
414 self.cmd_vars.insert(
415 "testdrive.fivetran-destination-files-path".into(),
416 self.fivetran_destination_files_path.clone(),
417 );
418
419 for (key, value) in env::vars() {
420 self.cmd_vars.insert(format!("env.{}", key), value);
421 }
422
423 for (key, value) in &self.arg_vars {
424 validate_ident(key)?;
425 self.cmd_vars
426 .insert(format!("arg.{}", key), value.to_string());
427 }
428
429 Ok(())
430 }
431 pub async fn with_catalog_copy<F, T>(
434 &self,
435 system_parameter_defaults: BTreeMap<String, String>,
436 build_info: &'static BuildInfo,
437 bootstrap_args: &BootstrapArgs,
438 enable_expression_cache_override: Option<bool>,
439 f: F,
440 ) -> Result<Option<T>, anyhow::Error>
441 where
442 F: FnOnce(ConnCatalog) -> T,
443 {
444 async fn persist_client(
445 persist_consensus_url: SensitiveUrl,
446 persist_blob_url: SensitiveUrl,
447 persist_clients: &PersistClientCache,
448 ) -> Result<PersistClient, anyhow::Error> {
449 let persist_location = PersistLocation {
450 blob_uri: persist_blob_url,
451 consensus_uri: persist_consensus_url,
452 };
453 Ok(persist_clients.open(persist_location).await?)
454 }
455
456 if let Some(CatalogConfig {
457 persist_consensus_url,
458 persist_blob_url,
459 }) = &self.materialize.catalog_config
460 {
461 let persist_client = persist_client(
462 persist_consensus_url.clone(),
463 persist_blob_url.clone(),
464 &self.persist_clients,
465 )
466 .await?;
467 let catalog = Catalog::open_debug_read_only_persist_catalog_config(
468 persist_client,
469 SYSTEM_TIME.clone(),
470 self.materialize.environment_id.clone(),
471 system_parameter_defaults,
472 build_info,
473 bootstrap_args,
474 enable_expression_cache_override,
475 )
476 .await?;
477 let res = f(catalog.for_session(&Session::dummy()));
478 catalog.expire().await;
479 Ok(Some(res))
480 } else {
481 Ok(None)
482 }
483 }
484
485 pub fn aws_endpoint(&self) -> &str {
486 self.aws_config.endpoint_url().unwrap_or("")
487 }
488
489 pub fn aws_region(&self) -> &str {
490 self.aws_config.region().map(|r| r.as_ref()).unwrap_or("")
491 }
492
493 pub fn clear_skip_consistency_checks(&mut self) -> bool {
496 std::mem::replace(&mut self.consistency_checks_adhoc_skip, false)
497 }
498
499 pub(crate) async fn join_background_tasks(&mut self) -> Vec<anyhow::Error> {
504 let mut errors = Vec::new();
505 for BackgroundTask {
506 desc,
507 mut handle,
508 cancel_token,
509 url,
510 } in self.background_tasks.drain(..)
511 {
512 match tokio::time::timeout(self.default_timeout, &mut handle).await {
517 Ok(Ok(())) => {}
518 Ok(Err(e)) => errors.push(e.context(format!("background query failed: {desc}"))),
519 Err(_) => {
520 cancel_background_query(&cancel_token, &url, self.default_timeout).await;
524 handle.abort_and_wait().await;
525 errors.push(anyhow!(
526 "background query did not complete before the end of the file: {desc}"
527 ));
528 }
529 }
530 }
531 errors
532 }
533
534 pub async fn reset_materialize(&self) -> Result<(), anyhow::Error> {
535 let (inner_client, _) = postgres_client(
536 &format!(
537 "postgres://mz_system:materialize@{}",
538 self.materialize.internal_sql_addr
539 ),
540 self.default_timeout,
541 )
542 .await?;
543
544 let version = pg_query_one(&inner_client, pg_sql!("SELECT mz_version_num()"), &[])
545 .await
546 .context("getting version of materialize")
547 .map(|row| row.get::<_, i32>(0))?;
548
549 let semver = pg_query_one(
550 &inner_client,
551 pg_sql!("SELECT right(split_part(mz_version(), ' ', 1), -1)"),
552 &[],
553 )
554 .await
555 .context("getting semver of materialize")
556 .map(|row| row.get::<_, String>(0))?
557 .parse::<semver::Version>()
558 .context("parsing semver of materialize")?;
559
560 pg_batch_execute(&inner_client, pg_sql!("ALTER SYSTEM RESET ALL"))
561 .await
562 .context("resetting materialize state: ALTER SYSTEM RESET ALL")?;
563
564 {
566 let rename_version = Version::parse("0.128.0-dev.1").expect("known to be valid");
567 let enable_unsafe_functions = if semver >= rename_version {
568 "unsafe_enable_unsafe_functions"
569 } else {
570 "enable_unsafe_functions"
571 };
572 let res = pg_batch_execute(
573 &inner_client,
574 pg_sql!(
575 "ALTER SYSTEM SET {} = on",
576 Sql::ident(enable_unsafe_functions)
577 ),
578 )
579 .await
580 .context("enabling dangerous functions");
581 if let Err(e) = res {
582 match e.root_cause().downcast_ref::<DbError>() {
583 Some(e) if *e.code() == SqlState::CANT_CHANGE_RUNTIME_PARAM => {
584 info!(
585 "can't enable unsafe functions because the server is safe mode; \
586 testdrive scripts will fail if they use unsafe functions",
587 );
588 }
589 _ => return Err(e),
590 }
591 }
592 }
593
594 for row in pg_query(&inner_client, pg_sql!("SHOW DATABASES"), &[])
595 .await
596 .context("resetting materialize state: SHOW DATABASES")?
597 {
598 let db_name: String = row.get(0);
599 if db_name.starts_with("testdrive_no_reset_") {
600 continue;
601 }
602 let drop_database = pg_sql!("DROP DATABASE {}", Sql::ident(&db_name));
603 sql::print_query(drop_database.as_str(), None);
604 pg_batch_execute(&inner_client, drop_database)
605 .await
606 .context(format!(
607 "resetting materialize state: DROP DATABASE {}",
608 db_name,
609 ))?;
610 }
611
612 let inactive_user_clusters = "
614 WITH
615 active_user_clusters AS
616 (
617 SELECT DISTINCT cluster_id, object_id
618 FROM
619 (
620 SELECT cluster_id, id FROM mz_catalog.mz_sources
621 UNION ALL SELECT cluster_id, id FROM mz_catalog.mz_sinks
622 UNION ALL
623 SELECT cluster_id, id
624 FROM mz_catalog.mz_materialized_views
625 UNION ALL
626 SELECT cluster_id, id FROM mz_catalog.mz_indexes
627 UNION ALL
628 SELECT cluster_id, id
629 FROM mz_internal.mz_subscriptions
630 )
631 AS t (cluster_id, object_id)
632 WHERE cluster_id IS NOT NULL AND object_id LIKE 'u%'
633 )
634 SELECT name
635 FROM mz_catalog.mz_clusters
636 WHERE
637 id NOT IN ( SELECT cluster_id FROM active_user_clusters ) AND id LIKE 'u%'
638 AND
639 owner_id LIKE 'u%';";
640
641 let inactive_clusters = pg_query(&inner_client, Sql::new(inactive_user_clusters), &[])
642 .await
643 .context("resetting materialize state: inactive_user_clusters")?;
644
645 if !inactive_clusters.is_empty() {
646 println!("cleaning up user clusters from previous tests...")
647 }
648
649 for cluster_name in inactive_clusters {
650 let cluster_name: String = cluster_name.get(0);
651 if cluster_name.starts_with("testdrive_no_reset_") {
652 continue;
653 }
654 let drop_cluster = pg_sql!("DROP CLUSTER {}", Sql::ident(&cluster_name));
655 sql::print_query(drop_cluster.as_str(), None);
656 pg_batch_execute(&inner_client, drop_cluster)
657 .await
658 .context(format!(
659 "resetting materialize state: DROP CLUSTER {}",
660 cluster_name,
661 ))?;
662 }
663
664 pg_batch_execute(&inner_client, pg_sql!("CREATE DATABASE materialize"))
665 .await
666 .context("resetting materialize state: CREATE DATABASE materialize")?;
667
668 if let Ok(rows) = pg_query(&inner_client, pg_sql!("SELECT name FROM mz_roles"), &[]).await {
672 for row in rows {
673 let role_name: String = row.get(0);
674 if role_name == self.materialize.user || role_name.starts_with("mz_") {
675 continue;
676 }
677 let drop_role = pg_sql!("DROP ROLE {}", Sql::ident(&role_name));
678 sql::print_query(drop_role.as_str(), None);
679 pg_batch_execute(&inner_client, drop_role)
680 .await
681 .context(format!(
682 "resetting materialize state: DROP ROLE {}",
683 role_name,
684 ))?;
685 }
686 }
687
688 pg_batch_execute(
690 &inner_client,
691 pg_sql!(
692 "GRANT ALL PRIVILEGES ON SYSTEM TO {}",
693 Sql::ident(&self.materialize.user)
694 ),
695 )
696 .await?;
697
698 pg_batch_execute(
700 &inner_client,
701 pg_sql!("GRANT USAGE ON DATABASE materialize TO PUBLIC"),
702 )
703 .await?;
704 pg_batch_execute(
705 &inner_client,
706 pg_sql!(
707 "GRANT ALL PRIVILEGES ON DATABASE materialize TO {}",
708 Sql::ident(&self.materialize.user)
709 ),
710 )
711 .await?;
712 pg_batch_execute(
713 &inner_client,
714 pg_sql!(
715 "GRANT ALL PRIVILEGES ON SCHEMA materialize.public TO {}",
716 Sql::ident(&self.materialize.user)
717 ),
718 )
719 .await?;
720
721 let cluster = match version {
722 ..=8199 => "default",
723 8200.. => "quickstart",
724 };
725 pg_batch_execute(
726 &inner_client,
727 pg_sql!("GRANT USAGE ON CLUSTER {} TO PUBLIC", Sql::ident(cluster)),
728 )
729 .await?;
730 pg_batch_execute(
731 &inner_client,
732 pg_sql!(
733 "GRANT ALL PRIVILEGES ON CLUSTER {} TO {}",
734 Sql::ident(cluster),
735 Sql::ident(&self.materialize.user)
736 ),
737 )
738 .await?;
739
740 Ok(())
741 }
742
743 pub async fn reset_kafka(&self) -> Result<(), anyhow::Error> {
745 use rdkafka::types::RDKafkaErrorCode;
746 let mut errors: Vec<anyhow::Error> = Vec::new();
747
748 let metadata = self.kafka_producer.client().fetch_metadata(
749 None,
750 Some(std::cmp::max(Duration::from_secs(1), self.default_timeout)),
751 )?;
752
753 let testdrive_topics: Vec<_> = metadata
754 .topics()
755 .iter()
756 .filter_map(|t| {
757 if t.name().starts_with("testdrive-") {
758 Some(t.name())
759 } else {
760 None
761 }
762 })
763 .collect();
764
765 if !testdrive_topics.is_empty() {
766 match self
767 .kafka_admin
768 .delete_topics(&testdrive_topics, &self.kafka_admin_opts)
769 .await
770 {
771 Ok(res) => {
772 if res.len() != testdrive_topics.len() {
773 errors.push(anyhow!(
774 "kafka topic deletion returned {} results, but exactly {} expected",
775 res.len(),
776 testdrive_topics.len()
777 ));
778 }
779 for (res, topic) in res.iter().zip_eq(testdrive_topics.iter()) {
780 match res {
781 Ok(_) | Err((_, RDKafkaErrorCode::UnknownTopicOrPartition)) => (),
782 Err((_, err)) => {
783 errors.push(anyhow!("unable to delete {}: {}", topic, err));
784 }
785 }
786 }
787 }
788 Err(e) => {
789 errors.push(e.into());
790 }
791 };
792 }
793
794 let schema_registry_errors = self.reset_schema_registry().await;
795
796 errors.extend(schema_registry_errors);
797 if errors.is_empty() {
798 Ok(())
799 } else {
800 bail!(
801 "deleting Kafka topics: {} errors: {}",
802 errors.len(),
803 errors
804 .into_iter()
805 .map(|e| e.to_string_with_causes())
806 .join("\n")
807 );
808 }
809 }
810
811 #[allow(clippy::disallowed_types)]
812 async fn reset_schema_registry(&self) -> Vec<anyhow::Error> {
813 use std::collections::HashMap;
814
815 let mut errors = Vec::new();
816 match self
817 .ccsr_client
818 .list_subjects()
819 .await
820 .context("listing schema registry subjects")
821 {
822 Ok(subjects) => {
823 let testdrive_subjects: Vec<_> = subjects
824 .into_iter()
825 .filter(|s| s.starts_with("testdrive-"))
826 .collect();
827
828 let mut graphs: HashMap<SubjectVersion, Vec<SubjectVersion>> = HashMap::new();
830
831 for subject in &testdrive_subjects {
832 match self.ccsr_client.get_subject_with_references(subject).await {
833 Ok((subj, refs)) => {
834 let refs: Vec<_> = refs
836 .into_iter()
837 .filter(|r| r.subject.starts_with("testdrive-"))
838 .collect();
839 graphs.insert(
840 SubjectVersion {
841 subject: subj.name,
842 version: subj.version,
843 },
844 refs,
845 );
846 }
847 Err(mz_ccsr::GetBySubjectError::SubjectNotFound) => {
848 }
850 Err(e) => {
851 errors.push(anyhow::anyhow!(
852 "failed to get references for subject {}: {}",
853 subject,
854 e
855 ));
856 }
857 }
858 }
859
860 let subjects_to_delete: Vec<_> = match mz_ccsr::topological_sort(&graphs) {
863 Ok(ordered) => {
864 let mut subjects: Vec<_> = ordered.into_iter().collect();
865 subjects.sort_by(|a, b| a.1.cmp(&b.1));
866 subjects.into_iter().map(|(s, _)| s.clone()).collect()
867 }
868 Err(_) => {
869 tracing::info!("Cycle detected, attempting to delete anyway");
870 graphs.into_keys().collect()
872 }
873 };
874
875 for subject in subjects_to_delete {
876 match self.ccsr_client.delete_subject(&subject.subject).await {
877 Ok(()) | Err(mz_ccsr::DeleteError::SubjectNotFound) => (),
878 Err(e) => errors.push(e.into()),
879 }
880 }
881 }
882 Err(e) => {
883 errors.push(e);
884 }
885 }
886 errors
887 }
888}
889
890#[derive(Debug, Clone)]
892pub struct CatalogConfig {
893 pub persist_consensus_url: SensitiveUrl,
895 pub persist_blob_url: SensitiveUrl,
897}
898
899pub enum ControlFlow {
900 Continue,
901 SkipBegin,
902 SkipEnd,
903}
904
905#[async_trait]
906pub(crate) trait Run {
907 async fn run(self, state: &mut State) -> Result<ControlFlow, PosError>;
908}
909
910#[async_trait]
911impl Run for PosCommand {
912 async fn run(self, state: &mut State) -> Result<ControlFlow, PosError> {
913 macro_rules! handle_version {
914 ($version_constraint:expr) => {
915 match $version_constraint {
916 Some(VersionConstraint { min, max }) => {
917 match version_check::run_version_check(min, max, state).await {
918 Ok(true) => return Ok(ControlFlow::Continue),
919 Ok(false) => {}
920 Err(err) => return Err(PosError::new(err, self.pos)),
921 }
922 }
923 None => {}
924 }
925 };
926 }
927
928 let wrap_err = |e| PosError::new(e, self.pos);
929 let ignore_prefix = match &self.command {
932 Command::Builtin(builtin, _) => Some(builtin.name.clone()),
933 _ => None,
934 };
935 let subst = |msg: &str, vars: &BTreeMap<String, String>| {
936 substitute_vars(msg, vars, &ignore_prefix, false).map_err(wrap_err)
937 };
938 let subst_re = |msg: &str, vars: &BTreeMap<String, String>| {
939 substitute_vars(msg, vars, &ignore_prefix, true).map_err(wrap_err)
940 };
941
942 let r = match self.command {
943 Command::Builtin(mut builtin, version_constraint) => {
944 handle_version!(version_constraint);
945 for val in builtin.args.values_mut() {
946 *val = subst(val, &state.cmd_vars)?;
947 }
948 for line in &mut builtin.input {
949 *line = subst(line, &state.cmd_vars)?;
950 }
951 match builtin.name.as_ref() {
952 "check-consistency" => consistency::run_consistency_checks(state).await,
953 "skip-consistency-checks" => {
954 consistency::skip_consistency_checks(builtin, state)
955 }
956 "check-shard-tombstone" => {
957 consistency::run_check_shard_tombstone(builtin, state).await
958 }
959 "duckdb-execute" => duckdb::run_execute(builtin, state).await,
960 "duckdb-query" => duckdb::run_query(builtin, state).await,
961 "fivetran-destination" => {
962 fivetran::run_destination_command(builtin, state).await
963 }
964 "file-append" => file::run_append(builtin, state).await,
965 "file-delete" => file::run_delete(builtin, state).await,
966 "glue-create-schema" => glue::run_create_schema(builtin, state).await,
967 "glue-verify-compatibility" => {
968 glue::run_verify_compatibility(builtin, state).await
969 }
970 "http-request" => http::run_request(builtin, state).await,
971 "kafka-add-partitions" => kafka::run_add_partitions(builtin, state).await,
972 "kafka-create-topic" => kafka::run_create_topic(builtin, state).await,
973 "kafka-wait-topic" => kafka::run_wait_topic(builtin, state).await,
974 "kafka-delete-records" => kafka::run_delete_records(builtin, state).await,
975 "kafka-delete-topic-flaky" => kafka::run_delete_topic(builtin, state).await,
976 "kafka-ingest" => kafka::run_ingest(builtin, state).await,
977 "kafka-verify-data" => kafka::run_verify_data(builtin, state).await,
978 "kafka-verify-commit" => kafka::run_verify_commit(builtin, state).await,
979 "kafka-verify-topic" => kafka::run_verify_topic(builtin, state).await,
980 "mysql-connect" => mysql::run_connect(builtin, state).await,
981 "mysql-execute" => mysql::run_execute(builtin, state).await,
982 "nop" => nop::run_nop(),
983 "postgres-connect" => postgres::run_connect(builtin, state).await,
984 "postgres-execute" => postgres::run_execute(builtin, state).await,
985 "postgres-verify-slot" => postgres::run_verify_slot(builtin, state).await,
986 "protobuf-compile-descriptors" => {
987 protobuf::run_compile_descriptors(builtin, state).await
988 }
989 "psql-execute" => psql::run_execute(builtin, state).await,
990 "s3-verify-data" => s3::run_verify_data(builtin, state).await,
991 "s3-verify-keys" => s3::run_verify_keys(builtin, state).await,
992 "s3-file-upload" => s3::run_upload(builtin, state).await,
993 "s3-set-presigned-url" => s3::run_set_presigned_url(builtin, state).await,
994 "s3-upload-parquet-types" => s3::run_upload_parquet_types(builtin, state).await,
995 "s3-upload-parquet-unsorted-map" => {
996 s3::run_upload_parquet_unsorted_map(builtin, state).await
997 }
998 "schema-registry-publish" => schema_registry::run_publish(builtin, state).await,
999 "schema-registry-verify" => schema_registry::run_verify(builtin, state).await,
1000 "schema-registry-wait" => schema_registry::run_wait(builtin, state).await,
1001 "skip-if" => skip_if::run_skip_if(builtin, state).await,
1002 "skip-end" => skip_end::run_skip_end(),
1003 "sql-server-connect" => sql_server::run_connect(builtin, state).await,
1004 "sql-server-execute" => sql_server::run_execute(builtin, state).await,
1005 "sql-server-set-from-sql" => sql_server::run_set_from_sql(builtin, state).await,
1006 "persist-force-compaction" => {
1007 persist::run_force_compaction(builtin, state).await
1008 }
1009 "random-sleep" => sleep::run_random_sleep(builtin),
1010 "set-regex" => set::run_regex_set(builtin, state),
1011 "unset-regex" => set::run_regex_unset(builtin, state),
1012 "set-sql-timeout" => set::run_sql_timeout(builtin, state),
1013 "set-max-tries" => set::run_max_tries(builtin, state),
1014 "sleep-is-probably-flaky-i-have-justified-my-need-with-a-comment" => {
1015 sleep::run_sleep(builtin)
1016 }
1017 "set" => set::set_vars(builtin, state),
1018 "set-arg-default" => set::run_set_arg_default(builtin, state),
1019 "set-from-sql" => set::run_set_from_sql(builtin, state).await,
1020 "set-from-file" => set::run_set_from_file(builtin, state).await,
1021 "webhook-append" => webhook::run_append(builtin, state).await,
1022 _ => {
1023 return Err(PosError::new(
1024 anyhow!("unknown built-in command {}", builtin.name),
1025 self.pos,
1026 ));
1027 }
1028 }
1029 }
1030 Command::Sql(mut sql, version_constraint) => {
1031 handle_version!(version_constraint);
1032 sql.query = subst(&sql.query, &state.cmd_vars)?;
1033 if let SqlOutput::Full { expected_rows, .. } = &mut sql.expected_output {
1034 for row in expected_rows {
1035 for col in row {
1036 *col = subst(col, &state.cmd_vars)?;
1037 }
1038 }
1039 }
1040 sql::run_sql(sql, state).await
1041 }
1042 Command::FailSql(mut sql, version_constraint) => {
1043 handle_version!(version_constraint);
1044 sql.query = subst(&sql.query, &state.cmd_vars)?;
1045 sql.expected_error = match &sql.expected_error {
1046 SqlExpectedError::Contains(s) => {
1047 SqlExpectedError::Contains(subst(s, &state.cmd_vars)?)
1048 }
1049 SqlExpectedError::Exact(s) => {
1050 SqlExpectedError::Exact(subst(s, &state.cmd_vars)?)
1051 }
1052 SqlExpectedError::Regex(s) => {
1053 SqlExpectedError::Regex(subst_re(s, &state.cmd_vars)?)
1054 }
1055 SqlExpectedError::Timeout => SqlExpectedError::Timeout,
1056 };
1057 sql.expected_detail = match sql.expected_detail {
1058 Some(s) => Some(subst(&s, &state.cmd_vars)?),
1059 None => None,
1060 };
1061 sql.expected_hint = match sql.expected_hint {
1062 Some(s) => Some(subst(&s, &state.cmd_vars)?),
1063 None => None,
1064 };
1065 sql::run_fail_sql(sql, state).await
1066 }
1067 };
1068
1069 r.map_err(wrap_err)
1070 }
1071}
1072
1073fn substitute_vars(
1075 msg: &str,
1076 vars: &BTreeMap<String, String>,
1077 ignore_prefix: &Option<String>,
1078 regex_escape: bool,
1079) -> Result<String, anyhow::Error> {
1080 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]+)\}").unwrap());
1081 let mut err = None;
1082 let out = RE.replace_all(msg, |caps: &Captures| {
1083 let name = &caps[1];
1084 if let Some(ignore_prefix) = &ignore_prefix {
1085 if name.starts_with(format!("{}.", ignore_prefix).as_str()) {
1086 return caps.get(0).unwrap().as_str().to_string();
1088 }
1089 }
1090
1091 if let Some(val) = vars.get(name) {
1092 if regex_escape {
1093 regex::escape(val)
1094 } else {
1095 val.to_string()
1096 }
1097 } else {
1098 err = Some(anyhow!("unknown variable: {}", name));
1099 "#VAR-MISSING#".to_string()
1100 }
1101 });
1102 match err {
1103 Some(err) => Err(err),
1104 None => Ok(out.into_owned()),
1105 }
1106}
1107
1108pub async fn create_state(
1116 config: &Config,
1117) -> Result<(State, impl Future<Output = Result<(), anyhow::Error>>), anyhow::Error> {
1118 let seed = config
1119 .seed
1120 .clone()
1121 .unwrap_or_else(|| format!("{:010}", rand::random::<u32>()));
1122
1123 let (_tempfile, temp_path) = match &config.temp_dir {
1124 Some(temp_dir) => {
1125 fs::create_dir_all(temp_dir).context("creating temporary directory")?;
1126 (None, PathBuf::from(&temp_dir))
1127 }
1128 _ => {
1129 let tempfile_handle = tempfile::tempdir().context("creating temporary directory")?;
1132 let temp_path = tempfile_handle.path().to_path_buf();
1133 (Some(tempfile_handle), temp_path)
1134 }
1135 };
1136
1137 let materialize_catalog_config = config.materialize_catalog_config.clone();
1138
1139 let materialize_url = util::postgres::config_url(&config.materialize_pgconfig)?;
1140 info!("Connecting to {}", materialize_url.as_str());
1141 let (pgclient, pgconn) = Retry::default()
1142 .max_duration(config.default_timeout)
1143 .retry_async_canceling(|_| async move {
1144 let mut pgconfig = config.materialize_pgconfig.clone();
1145 pgconfig.connect_timeout(config.default_timeout);
1146 let tls = make_tls(&pgconfig)?;
1147 pgconfig.connect(tls).await.map_err(|e| anyhow!(e))
1148 })
1149 .await?;
1150
1151 let pgconn_task =
1152 task::spawn(|| "pgconn_task", pgconn).map(|join| join.context("running SQL connection"));
1153
1154 let materialize_state =
1155 create_materialize_state(&config, materialize_catalog_config, pgclient).await?;
1156
1157 let schema_registry_url = config.schema_registry_url.to_owned();
1158
1159 let ccsr_client = {
1160 let mut ccsr_config = mz_ccsr::ClientConfig::new(schema_registry_url.clone());
1161
1162 if let Some(cert_path) = &config.cert_path {
1163 let cert = fs::read(cert_path).context("reading cert")?;
1164 let pass = config.cert_password.as_deref().unwrap_or("").to_owned();
1165 let ident = mz_ccsr::tls::Identity::from_pkcs12_der(cert, pass)
1166 .context("reading keystore file as pkcs12")?;
1167 ccsr_config = ccsr_config.identity(ident);
1168 }
1169
1170 if let Some(ccsr_username) = &config.ccsr_username {
1171 ccsr_config = ccsr_config.auth(ccsr_username.clone(), config.ccsr_password.clone());
1172 }
1173
1174 ccsr_config.build().context("Creating CCSR client")?
1175 };
1176
1177 let (kafka_addr, kafka_admin, kafka_admin_opts, kafka_producer, kafka_topics, kafka_config) = {
1178 use rdkafka::admin::{AdminClient, AdminOptions};
1179 use rdkafka::producer::FutureProducer;
1180
1181 let mut kafka_config = create_new_client_config_simple();
1182 kafka_config.set("bootstrap.servers", &config.kafka_addr);
1183 kafka_config.set("group.id", "materialize-testdrive");
1184 kafka_config.set("auto.offset.reset", "earliest");
1185 kafka_config.set("isolation.level", "read_committed");
1186 if let Some(cert_path) = &config.cert_path {
1187 kafka_config.set("security.protocol", "ssl");
1188 kafka_config.set("ssl.keystore.location", cert_path);
1189 if let Some(cert_password) = &config.cert_password {
1190 kafka_config.set("ssl.keystore.password", cert_password);
1191 }
1192 }
1193 kafka_config.set("message.max.bytes", "15728640");
1194
1195 for (key, value) in &config.kafka_opts {
1196 kafka_config.set(key, value);
1197 }
1198
1199 let admin: AdminClient<_> = kafka_config
1200 .create_with_context(MzClientContext::default())
1201 .with_context(|| format!("opening Kafka connection: {}", config.kafka_addr))?;
1202
1203 let admin_opts = AdminOptions::new().operation_timeout(Some(config.default_timeout));
1204
1205 let producer: FutureProducer<_> = kafka_config
1206 .create_with_context(MzClientContext::default())
1207 .with_context(|| format!("opening Kafka producer connection: {}", config.kafka_addr))?;
1208
1209 let topics = BTreeMap::new();
1210
1211 (
1212 config.kafka_addr.to_owned(),
1213 admin,
1214 admin_opts,
1215 producer,
1216 topics,
1217 kafka_config,
1218 )
1219 };
1220
1221 let mut state = State {
1222 config: config.clone(),
1223
1224 arg_vars: config.arg_vars.clone(),
1226 cmd_vars: BTreeMap::new(),
1227 seed,
1228 temp_path,
1229 _tempfile,
1230 default_timeout: config.default_timeout,
1231 timeout: config.default_timeout,
1232 max_tries: config.default_max_tries,
1233 initial_backoff: config.initial_backoff,
1234 backoff_factor: config.backoff_factor,
1235 consistency_checks: config.consistency_checks,
1236 check_statement_logging: config.check_statement_logging,
1237 consistency_checks_adhoc_skip: false,
1238 regex: None,
1239 regex_replacement: set::DEFAULT_REGEX_REPLACEMENT.into(),
1240 rewrite_results: config.rewrite_results,
1241 error_line_count: 0,
1242 error_string: "".to_string(),
1243
1244 materialize: materialize_state,
1246
1247 persist_consensus_url: config.persist_consensus_url.clone(),
1249 persist_blob_url: config.persist_blob_url.clone(),
1250 build_info: config.build_info,
1251 persist_clients: PersistClientCache::new(
1252 PersistConfig::new_default_configs(config.build_info, SYSTEM_TIME.clone()),
1253 &MetricsRegistry::new(),
1254 |_, _| PubSubClientConnection::noop(),
1255 ),
1256
1257 schema_registry_url,
1259 ccsr_client,
1260 kafka_addr,
1261 kafka_admin,
1262 kafka_admin_opts,
1263 kafka_config,
1264 kafka_default_partitions: config.kafka_default_partitions,
1265 kafka_producer,
1266 kafka_topics,
1267 kafka_verify_topics: BTreeSet::new(),
1268
1269 aws_account: config.aws_account.clone(),
1271 aws_config: config.aws_config.clone(),
1272
1273 duckdb_clients: BTreeMap::new(),
1275 mysql_clients: BTreeMap::new(),
1276 postgres_clients: BTreeMap::new(),
1277 sql_server_clients: BTreeMap::new(),
1278 background_tasks: Vec::new(),
1279
1280 fivetran_destination_url: config.fivetran_destination_url.clone(),
1282 fivetran_destination_files_path: config.fivetran_destination_files_path.clone(),
1283
1284 rewrites: Vec::new(),
1285 rewrite_pos_start: 0,
1286 rewrite_pos_end: 0,
1287 };
1288 state.initialize_cmd_vars().await?;
1289 Ok((state, pgconn_task))
1290}
1291
1292async fn create_materialize_state(
1293 config: &&Config,
1294 materialize_catalog_config: Option<CatalogConfig>,
1295 pgclient: tokio_postgres::Client,
1296) -> Result<MaterializeState, anyhow::Error> {
1297 let materialize_url = util::postgres::config_url(&config.materialize_pgconfig)?;
1298 let materialize_internal_url =
1299 util::postgres::config_url(&config.materialize_internal_pgconfig)?;
1300
1301 for (key, value) in &config.materialize_params {
1302 #[allow(clippy::disallowed_methods)]
1304 pgclient
1305 .batch_execute(&format!("SET {key} = {value}"))
1306 .await
1307 .context("setting session parameter")?;
1308 }
1309
1310 let materialize_user = config
1311 .materialize_pgconfig
1312 .get_user()
1313 .expect("testdrive URL must contain user")
1314 .to_string();
1315
1316 let materialize_sql_addr = format!(
1317 "{}:{}",
1318 materialize_url.host_str().unwrap(),
1319 materialize_url.port().unwrap()
1320 );
1321 let materialize_http_addr = format!(
1322 "{}:{}",
1323 materialize_url.host_str().unwrap(),
1324 config.materialize_http_port
1325 );
1326 let materialize_internal_sql_addr = format!(
1327 "{}:{}",
1328 materialize_internal_url.host_str().unwrap(),
1329 materialize_internal_url.port().unwrap()
1330 );
1331 let materialize_password_sql_addr = format!(
1332 "{}:{}",
1333 materialize_url.host_str().unwrap(),
1334 config.materialize_password_sql_port
1335 );
1336 let materialize_sasl_sql_addr = format!(
1337 "{}:{}",
1338 materialize_url.host_str().unwrap(),
1339 config.materialize_sasl_sql_port
1340 );
1341 let materialize_internal_http_addr = format!(
1342 "{}:{}",
1343 materialize_internal_url.host_str().unwrap(),
1344 config.materialize_internal_http_port
1345 );
1346 let environment_id = pg_query_one(&pgclient, pg_sql!("SELECT mz_environment_id()"), &[])
1347 .await?
1348 .get::<_, String>(0)
1349 .parse()
1350 .context("parsing environment ID")?;
1351
1352 let bootstrap_args = BootstrapArgs {
1353 cluster_replica_size_map: config.materialize_cluster_replica_sizes.clone(),
1354 default_cluster_replica_size: "ABC".to_string(),
1355 default_cluster_replication_factor: 1,
1356 bootstrap_role: None,
1357 };
1358
1359 let materialize_state = MaterializeState {
1360 catalog_config: materialize_catalog_config,
1361 sql_addr: materialize_sql_addr,
1362 use_https: config.materialize_use_https,
1363 http_addr: materialize_http_addr,
1364 internal_sql_addr: materialize_internal_sql_addr,
1365 internal_http_addr: materialize_internal_http_addr,
1366 password_sql_addr: materialize_password_sql_addr,
1367 sasl_sql_addr: materialize_sasl_sql_addr,
1368 user: materialize_user,
1369 pgclient,
1370 environment_id,
1371 bootstrap_args,
1372 };
1373
1374 Ok(materialize_state)
1375}