Skip to main content

risingwave_simulation/
slt.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cmp::min;
16use std::path::Path;
17use std::sync::Arc;
18use std::time::Duration;
19
20use anyhow::Result;
21use rand::{RngExt as _, rng as thread_rng};
22use sqllogictest::substitution::well_known;
23use sqllogictest::{
24    Condition, ParallelTestError, Partitioner, QueryExpect, Record, StatementExpect,
25};
26
27use crate::client::RisingWave;
28use crate::cluster::Cluster;
29use crate::parse::extract_sql_command;
30use crate::slt::background_ddl_mode::*;
31use crate::slt::runner::{random_vnode_count, run_kill_not_allowed, run_no_kill};
32use crate::slt::slt_env::{Env, Opts};
33use crate::slt::vnode_mode::*;
34use crate::utils::TimedExt;
35use crate::{evaluate_skip, wait_or_retry_background_ddl};
36
37// retry a maximum times until it succeed
38const MAX_RETRY: usize = 10;
39
40#[derive(Debug, PartialEq, Eq)]
41pub enum SqlCmd {
42    /// Other create statements.
43    Create {
44        is_create_table_as: bool,
45    },
46    /// Create Materialized views
47    CreateMaterializedView {
48        name: String,
49    },
50    /// Set background ddl
51    SetBackgroundDdl {
52        enable: bool,
53    },
54    Drop,
55    Dml,
56    Flush,
57    Others,
58}
59
60impl SqlCmd {
61    fn allow_kill(&self) -> bool {
62        matches!(
63            self,
64            SqlCmd::Create {
65                // `create table as` is also not atomic in our system.
66                is_create_table_as: false,
67                ..
68            } | SqlCmd::CreateMaterializedView { .. }
69                | SqlCmd::Drop
70        )
71        // We won't kill during insert/update/delete/alter since the atomicity is not guaranteed.
72        // TODO: For `SqlCmd::Alter`, since table fragment and catalog commit for table schema change
73        // are not transactional, we can't kill during `alter table add/drop columns` for now, will
74        // remove it until transactional commit of table fragment and catalog is supported.
75    }
76
77    fn is_create(&self) -> bool {
78        matches!(
79            self,
80            SqlCmd::Create { .. } | SqlCmd::CreateMaterializedView { .. }
81        )
82    }
83}
84
85const KILL_IGNORE_FILES: &[&str] = &[
86    // TPCH queries are too slow for recovery.
87    "tpch_snapshot.slt",
88    "tpch_upstream.slt",
89    // Drop is not retryable in search path test.
90    "search_path.slt",
91    // background CREATE SINK held at rate limit 0 can't be retried after a kill
92    "alter_mv/alter_mv_concurrency_check.slt",
93    // Transaction statements are not retryable.
94    "transaction/now.slt",
95    "transaction/read_only_multi_conn.slt",
96    "transaction/read_only.slt",
97    "transaction/tolerance.slt",
98    "transaction/cursor.slt",
99    "transaction/cursor_multi_conn.slt",
100];
101
102/// Randomly set DDL statements to use `background_ddl`
103mod background_ddl_mode {
104    use anyhow::bail;
105    use rand::RngExt as _;
106    use rand::rngs::ChaCha20Rng;
107    use sqllogictest::{Condition, Record, StatementExpect};
108
109    use crate::client::RisingWave;
110    use crate::slt::SqlCmd;
111    use crate::slt::slt_env::Env;
112
113    /// Wait for background mv to finish creating
114    pub(super) async fn wait_background_mv_finished(mview_name: &str) -> anyhow::Result<()> {
115        let Ok(rw) = RisingWave::connect("frontend".into(), "dev".into()).await else {
116            bail!("failed to connect to frontend for {mview_name}");
117        };
118        let client = rw.pg_client();
119        if client.simple_query("WAIT;").await.is_err() {
120            bail!("failed to wait for background mv to finish creating for {mview_name}");
121        }
122
123        let Ok(result) = client
124            .query(
125                "select count(*) from pg_matviews where matviewname=$1;",
126                &[&mview_name],
127            )
128            .await
129        else {
130            bail!("failed to query pg_matviews for {mview_name}");
131        };
132
133        match result[0].try_get::<_, i64>(0) {
134            Ok(1) => Ok(()),
135            r => bail!("expected 1 row in pg_matviews, got {r:#?} instead for {mview_name}"),
136        }
137    }
138
139    // TODO(kwannoel): move rng, `background_ddl` to `Env` struct
140    pub(super) async fn set_background_ddl<D, M, T>(
141        tester: &mut sqllogictest::Runner<D, M>,
142        env: &Env,
143        record: &Record<T>,
144        cmd: &SqlCmd,
145        manual_background_ddl_enabled: bool,
146        rng: &mut ChaCha20Rng,
147        background_ddl_enabled: &mut bool,
148    ) where
149        D: sqllogictest::AsyncDB<ColumnType = T>,
150        M: sqllogictest::MakeConnection<Conn = D>,
151        T: sqllogictest::ColumnType,
152    {
153        if let Record::Statement {
154            loc,
155            conditions,
156            connection,
157            ..
158        } = &record
159            && matches!(cmd, SqlCmd::CreateMaterializedView { .. })
160            && !manual_background_ddl_enabled
161            && conditions.iter().all(|c| {
162                *c != Condition::SkipIf {
163                    label: "madsim".to_owned(),
164                }
165            })
166            && env.background_ddl_rate() > 0.0
167        {
168            let background_ddl_setting = rng.random_bool(env.background_ddl_rate());
169            let set_background_ddl = Record::Statement {
170                loc: loc.clone(),
171                conditions: conditions.clone(),
172                connection: connection.clone(),
173                expected: StatementExpect::Ok,
174                sql: format!("SET BACKGROUND_DDL={background_ddl_setting};"),
175                retry: None,
176            };
177            tester.run_async(set_background_ddl).await.unwrap();
178            *background_ddl_enabled = background_ddl_setting;
179        };
180    }
181
182    // NOTE(kwannoel): only applicable to mvs currently
183    #[macro_export]
184    macro_rules! wait_or_retry_background_ddl {
185        ($record:expr, $name:expr, $iteration:expr) => {
186            let record = $record;
187            let i = $iteration;
188            tracing::debug!(iteration = i, "Retry for background ddl");
189            match wait_background_mv_finished($name).await {
190                Ok(_) => {
191                    tracing::debug!(
192                        iteration = i,
193                        "Record with background_ddl {:?} finished",
194                        record
195                    );
196                    break;
197                }
198                Err(err) => {
199                    tracing::error!(
200                        iteration = i,
201                        ?err,
202                        "failed to wait for background mv to finish creating"
203                    );
204                    if i >= MAX_RETRY {
205                        panic!("failed to run test after retry {i} times, error={err:#?}");
206                    }
207                    continue;
208                }
209            }
210        };
211    }
212}
213
214mod vnode_mode {
215    use std::env;
216    use std::hash::{DefaultHasher, Hash, Hasher};
217    use std::sync::LazyLock;
218
219    use anyhow::bail;
220    use sqllogictest::Partitioner;
221
222    // Copied from sqllogictest-bin.
223    #[derive(Clone)]
224    pub(super) struct HashPartitioner {
225        count: u64,
226        id: u64,
227    }
228
229    impl HashPartitioner {
230        pub(super) fn new(count: u64, id: u64) -> anyhow::Result<Self> {
231            if count == 0 {
232                bail!("partition count must be greater than zero");
233            }
234            if id >= count {
235                bail!("partition id (zero-based) must be less than count");
236            }
237            Ok(Self { count, id })
238        }
239    }
240
241    impl Partitioner for HashPartitioner {
242        fn matches(&self, file_name: &str) -> bool {
243            let mut hasher = DefaultHasher::new();
244            file_name.hash(&mut hasher);
245            hasher.finish() % self.count == self.id
246        }
247    }
248
249    pub(super) static PARTITIONER: LazyLock<Option<HashPartitioner>> = LazyLock::new(|| {
250        let count = env::var("BUILDKITE_PARALLEL_JOB_COUNT")
251            .ok()?
252            .parse::<u64>()
253            .unwrap();
254        let id = env::var("BUILDKITE_PARALLEL_JOB")
255            .ok()?
256            .parse::<u64>()
257            .unwrap();
258        Some(HashPartitioner::new(count, id).unwrap())
259    });
260}
261
262pub mod slt_env {
263    use rand::SeedableRng;
264    use rand::rngs::ChaCha20Rng;
265
266    use crate::cluster::KillOpts;
267
268    pub struct Opts {
269        pub kill_opts: KillOpts,
270        /// Probability of `background_ddl` being set to true per ddl record.
271        pub background_ddl_rate: f64,
272        /// Set vnode count (`STREAMING_MAX_PARALLELISM`) to random value before running DDL.
273        pub random_vnode_count: bool,
274    }
275
276    pub(super) struct Env {
277        opts: Opts,
278    }
279
280    impl Env {
281        pub fn new(opts: Opts) -> Self {
282            Self { opts }
283        }
284
285        pub fn get_rng() -> ChaCha20Rng {
286            let seed = std::env::var("MADSIM_TEST_SEED")
287                .unwrap_or("0".to_owned())
288                .parse::<u64>()
289                .unwrap();
290            ChaCha20Rng::seed_from_u64(seed)
291        }
292
293        pub fn background_ddl_rate(&self) -> f64 {
294            self.opts.background_ddl_rate
295        }
296
297        pub fn kill(&self) -> bool {
298            self.opts.kill_opts.kill_compute
299                || self.opts.kill_opts.kill_meta
300                || self.opts.kill_opts.kill_frontend
301                || self.opts.kill_opts.kill_compactor
302        }
303
304        pub fn random_vnode_count(&self) -> bool {
305            self.opts.random_vnode_count
306        }
307
308        pub fn kill_opts(&self) -> KillOpts {
309            self.opts.kill_opts
310        }
311
312        pub fn kill_rate(&self) -> f64 {
313            self.opts.kill_opts.kill_rate as f64
314        }
315    }
316}
317
318mod runner {
319    use std::time::Duration;
320
321    use rand::prelude::IteratorRandom;
322    use rand::rng as thread_rng;
323    use sqllogictest::{Record, StatementExpect, TestError};
324
325    use crate::slt::slt_env::Env;
326    use crate::slt::{MAX_RETRY, SqlCmd};
327    use crate::utils::TimedExt;
328    #[macro_export]
329    macro_rules! evaluate_skip {
330        ($env:expr, $path:expr) => {
331            if let Some(partitioner) = PARTITIONER.as_ref()
332                && !partitioner.matches($path.to_str().unwrap())
333            {
334                println!("[skip partition] {}", $path.display());
335                continue;
336            } else if $env.kill() && KILL_IGNORE_FILES.iter().any(|s| $path.ends_with(s)) {
337                println!("[skip kill] {}", $path.display());
338                continue;
339            } else {
340                println!("[run] {}", $path.display());
341            }
342        };
343    }
344
345    pub(super) fn random_vnode_count<T: sqllogictest::ColumnType>(
346        env: &Env,
347        records: &[Record<T>],
348    ) -> bool {
349        env.random_vnode_count()
350            && records.iter().all(|record| {
351                if let Record::Statement { sql, .. } | Record::Query { sql, .. } = record
352                    && sql.to_lowercase().contains("parallelism")
353                {
354                    println!("[RANDOM VNODE COUNT] skip: {}", sql);
355                    false
356                } else {
357                    true
358                }
359            })
360    }
361
362    /// `kill` is totally disabled.
363    pub(super) async fn run_no_kill<D, M, T>(
364        tester: &mut sqllogictest::Runner<D, M>,
365        random_vnode_count: bool,
366        cmd: &SqlCmd,
367        record: Record<T>,
368    ) -> Result<(), TestError>
369    where
370        D: sqllogictest::AsyncDB<ColumnType = T>,
371        M: sqllogictest::MakeConnection<Conn = D>,
372        T: sqllogictest::ColumnType,
373    {
374        // Set random vnode count if needed.
375        if random_vnode_count
376            && cmd.is_create()
377            && let Record::Statement {
378                loc,
379                conditions,
380                connection,
381                ..
382            } = &record
383        {
384            let vnode_count = (2..=64) // small
385                .chain(224..=288) // normal
386                .chain(992..=1056) // 1024 affects row id gen behavior
387                .choose(&mut thread_rng())
388                .unwrap();
389            let sql = format!("SET STREAMING_MAX_PARALLELISM = {vnode_count};");
390            println!("[RANDOM VNODE COUNT] set: {vnode_count}");
391            let set_random_vnode_count = Record::Statement {
392                loc: loc.clone(),
393                conditions: conditions.clone(),
394                connection: connection.clone(),
395                sql,
396                expected: StatementExpect::Ok,
397                retry: None,
398            };
399            tester.run_async(set_random_vnode_count).await.unwrap();
400            println!("[RANDOM VNODE COUNT] run: {record}");
401        }
402
403        tester.run_async(record).await.map(|_| ())
404    }
405
406    /// Used when `kill` is not allowed for some specific commands
407    pub(super) async fn run_kill_not_allowed<D, M, T>(
408        tester: &mut sqllogictest::Runner<D, M>,
409        record: Record<T>,
410    ) where
411        D: sqllogictest::AsyncDB<ColumnType = T>,
412        M: sqllogictest::MakeConnection<Conn = D>,
413        T: sqllogictest::ColumnType,
414    {
415        for i in 0usize.. {
416            let delay = Duration::from_secs(1 << i);
417            if let Err(err) = tester
418                .run_async(record.clone())
419                .timed(|_res, elapsed| {
420                    tracing::debug!("Record {:?} finished in {:?}", record, elapsed)
421                })
422                .await
423            {
424                let err_string = err.to_string().to_ascii_lowercase();
425                // cluster could be still under recovering if killed before, retry if
426                // meets `no reader for dml in table with id {}`.
427                let allowed_errs = [
428                    "no reader for dml in table",
429                    "error reading a body from connection: broken pipe",
430                    "failed to inject barrier",
431                    "get error from control stream",
432                    "cluster is under recovering",
433                    "streaming vnode mapping has not been initialized",
434                    "streaming vnode mapping not found",
435                ];
436                let should_retry = i < MAX_RETRY
437                    && allowed_errs
438                        .iter()
439                        .any(|allowed_err| err_string.contains(&allowed_err.to_ascii_lowercase()));
440                if !should_retry {
441                    panic!("{}", err);
442                }
443                tracing::error!("failed to run test: {err}\nretry after {delay:?}");
444            } else {
445                break;
446            }
447            tokio::time::sleep(delay).await;
448        }
449    }
450}
451
452/// Run the sqllogictest files in `glob`.
453pub async fn run_slt_task(cluster: Arc<Cluster>, glob: &str, opts: Opts) {
454    let env = slt_env::Env::new(opts);
455    tracing::info!("background_ddl_rate: {}", env.background_ddl_rate());
456    let mut rng = Env::get_rng();
457    let files = glob::glob(glob).expect("failed to read glob pattern");
458    for file in files {
459        // use a session per file
460        let mut tester =
461            sqllogictest::Runner::new(|| RisingWave::connect("frontend".into(), "dev".into()));
462        tester.add_label("madsim");
463        tester.set_var(well_known::DATABASE.to_owned(), "dev".to_owned());
464
465        let file = file.unwrap();
466        let path = file.as_path();
467
468        evaluate_skip!(env, path);
469
470        // XXX: hack for kafka source test
471        let tempfile = (path.ends_with("kafka.slt") || path.ends_with("kafka_batch.slt"))
472            .then(|| hack_kafka_test(path));
473        let path = tempfile.as_ref().map(|p| p.path()).unwrap_or(path);
474
475        // NOTE(kwannoel): For background ddl
476        let mut background_ddl_enabled = false;
477
478        // If background ddl is set to true within the test case, prevent random setting of background_ddl to true.
479        // We can revert it back to false only if we encounter a record that sets background_ddl to false.
480        let mut manual_background_ddl_enabled = false;
481
482        let records = sqllogictest::parse_file(path).expect("failed to parse file");
483        let random_vnode_count = random_vnode_count(&env, &records);
484
485        for record in records {
486            // uncomment to print metrics for task counts
487            // let metrics = madsim::runtime::Handle::current().metrics();
488            // println!("{:#?}", metrics);
489            // println!("{}", metrics.num_tasks_by_node_by_spawn());
490            if let sqllogictest::Record::Halt { .. } = record {
491                break;
492            }
493
494            let cmd = match &record {
495                sqllogictest::Record::Statement {
496                    sql, conditions, ..
497                }
498                | sqllogictest::Record::Query {
499                    sql, conditions, ..
500                } if conditions
501                    .iter()
502                    .all(|c| !matches!(c, Condition::SkipIf{ label } if label == "madsim"))
503                    && !conditions
504                        .iter()
505                        .any(|c| matches!(c, Condition::OnlyIf{ label} if label != "madsim" )) =>
506                {
507                    extract_sql_command(sql).unwrap_or(SqlCmd::Others)
508                }
509                _ => SqlCmd::Others,
510            };
511
512            // For normal records.
513            if !env.kill() {
514                match run_no_kill(&mut tester, random_vnode_count, &cmd, record).await {
515                    Ok(_) => continue,
516                    Err(e) => panic!("{}", e),
517                }
518            }
519
520            // For kill enabled.
521            tracing::debug!(?cmd, "Running");
522
523            if let SqlCmd::SetBackgroundDdl { enable } = cmd {
524                manual_background_ddl_enabled = enable;
525                background_ddl_enabled = enable;
526            }
527
528            // For each background ddl compatible statement, provide a chance for background_ddl=true.
529            set_background_ddl(
530                &mut tester,
531                &env,
532                &record,
533                &cmd,
534                manual_background_ddl_enabled,
535                &mut rng,
536                &mut background_ddl_enabled,
537            )
538            .await;
539
540            if !cmd.allow_kill() {
541                run_kill_not_allowed(&mut tester, record.clone()).await;
542                continue;
543            }
544
545            let should_kill = thread_rng().random_bool(env.kill_rate());
546            // spawn a background task to kill nodes
547            let handle = if should_kill {
548                let cluster = cluster.clone();
549                let opts = env.kill_opts();
550                Some(tokio::spawn(async move {
551                    let t = thread_rng().random_range(Duration::default()..Duration::from_secs(1));
552                    tokio::time::sleep(t).await;
553                    cluster.kill_node(&opts).await;
554                    tokio::time::sleep(Duration::from_secs(15)).await;
555                }))
556            } else {
557                None
558            };
559
560            for i in 0usize.. {
561                tracing::debug!(iteration = i, "retry count");
562                let delay = Duration::from_secs(min(1 << i, 10));
563                if i > 0 {
564                    tokio::time::sleep(delay).await;
565                }
566                match tester
567                    .run_async(record.clone())
568                    .timed(|_res, elapsed| {
569                        tracing::debug!("Record {:?} finished in {:?}", record, elapsed)
570                    })
571                    .await
572                {
573                    Ok(_) => {
574                        // For background ddl
575                        if let SqlCmd::CreateMaterializedView { ref name } = cmd
576                            && background_ddl_enabled
577                            && !matches!(
578                                record,
579                                Record::Statement {
580                                    expected: StatementExpect::Error(_),
581                                    ..
582                                } | Record::Query {
583                                    expected: QueryExpect::Error(_),
584                                    ..
585                                }
586                            )
587                        {
588                            wait_or_retry_background_ddl!(&record, name, i);
589                        }
590                        break;
591                    }
592                    Err(e) => {
593                        match cmd {
594                            // allow 'table exists' error when retry CREATE statement
595                            SqlCmd::Create {
596                                is_create_table_as: false,
597                            }
598                            | SqlCmd::CreateMaterializedView { .. }
599                                if i != 0
600                                    && let e = e.to_string()
601                                    // It should not be a gRPC request to meta error,
602                                    // otherwise it means that the catalog is not yet populated to fe.
603                                    && !e.contains("gRPC request to meta service failed")
604                                    && e.contains("exists")
605                                    && !e.contains("still being created")
606                                    && e.contains("Catalog error") =>
607                            {
608                                break;
609                            }
610                            // allow 'not found' error when retry DROP statement
611                            SqlCmd::Drop
612                                if i != 0
613                                    && e.to_string().contains("not found")
614                                    && e.to_string().contains("Catalog error") =>
615                            {
616                                break;
617                            }
618
619                            // Keep i >= MAX_RETRY for other errors. Since these errors indicate that the MV might not yet be created.
620                            _ if i >= MAX_RETRY => {
621                                panic!("failed to run test after retry {i} times: {e}")
622                            }
623                            SqlCmd::CreateMaterializedView { ref name }
624                                if i != 0
625                                    && e.to_string().contains("table is in creating procedure")
626                                    && background_ddl_enabled =>
627                            {
628                                wait_or_retry_background_ddl!(&record, name, i);
629                            }
630                            _ => tracing::error!(
631                                iteration = i,
632                                "failed to run test: {e}\nretry after {delay:?}"
633                            ),
634                        }
635                    }
636                }
637            }
638            if let SqlCmd::SetBackgroundDdl { enable } = cmd {
639                background_ddl_enabled = enable;
640            };
641            if let Some(handle) = handle {
642                handle.await.unwrap();
643            }
644        }
645    }
646}
647
648pub async fn run_parallel_slt_task(glob: &str, jobs: usize) -> Result<(), ParallelTestError> {
649    let mut tester =
650        sqllogictest::Runner::new(|| RisingWave::connect("frontend".into(), "dev".into()));
651    tester.add_label("madsim");
652
653    if let Some(partitioner) = PARTITIONER.as_ref() {
654        tester.with_partitioner(partitioner.clone());
655    }
656
657    tester
658        .run_parallel_async(
659            glob,
660            vec!["frontend".into()],
661            |host, dbname| async move { RisingWave::connect(host, dbname).await.unwrap() },
662            jobs,
663        )
664        .await
665        .map_err(|e| panic!("{e}"))
666}
667
668/// Replace some strings in kafka.slt and write to a new temp file.
669fn hack_kafka_test(path: &Path) -> tempfile::NamedTempFile {
670    let content = std::fs::read_to_string(path).expect("failed to read file");
671    let simple_avsc_full_path =
672        std::fs::canonicalize("src/connector/src/test_data/simple-schema.avsc")
673            .expect("failed to get schema path");
674    let complex_avsc_full_path =
675        std::fs::canonicalize("src/connector/src/test_data/complex-schema.avsc")
676            .expect("failed to get schema path");
677    let json_schema_full_path =
678        std::fs::canonicalize("src/connector/src/test_data/complex-schema.json")
679            .expect("failed to get schema path");
680    let content = content
681        .replace("127.0.0.1:29092", "192.168.11.1:29092")
682        .replace("localhost:29092", "192.168.11.1:29092")
683        .replace(
684            "/risingwave/avro-simple-schema.avsc",
685            simple_avsc_full_path.to_str().unwrap(),
686        )
687        .replace(
688            "/risingwave/avro-complex-schema.avsc",
689            complex_avsc_full_path.to_str().unwrap(),
690        )
691        .replace(
692            "/risingwave/json-complex-schema",
693            json_schema_full_path.to_str().unwrap(),
694        );
695    let file = tempfile::NamedTempFile::new().expect("failed to create temp file");
696    std::fs::write(file.path(), content).expect("failed to write file");
697    println!("created a temp file for kafka test: {:?}", file.path());
698    file
699}
700
701#[cfg(test)]
702mod tests {
703    use std::fmt::Debug;
704
705    use expect_test::{Expect, expect};
706
707    use super::*;
708
709    fn check(actual: impl Debug, expect: Expect) {
710        let actual = format!("{:#?}", actual);
711        expect.assert_eq(&actual);
712    }
713
714    #[test]
715    fn test_extract_sql_command() {
716        check(
717            extract_sql_command("create  table  t as select 1;"),
718            expect![[r#"
719                Ok(
720                    Create {
721                        is_create_table_as: true,
722                    },
723                )"#]],
724        );
725        check(
726            extract_sql_command("  create table  t (a int);"),
727            expect![[r#"
728                Ok(
729                    Create {
730                        is_create_table_as: false,
731                    },
732                )"#]],
733        );
734        check(
735            extract_sql_command(" create materialized   view  m_1 as select 1;"),
736            expect![[r#"
737                Ok(
738                    CreateMaterializedView {
739                        name: "m_1",
740                    },
741                )"#]],
742        );
743        check(
744            extract_sql_command("set background_ddl= true;"),
745            expect![[r#"
746                Ok(
747                    SetBackgroundDdl {
748                        enable: true,
749                    },
750                )"#]],
751        );
752        check(
753            extract_sql_command("SET BACKGROUND_DDL=true;"),
754            expect![[r#"
755                Ok(
756                    SetBackgroundDdl {
757                        enable: true,
758                    },
759                )"#]],
760        );
761        check(
762            extract_sql_command("CREATE MATERIALIZED VIEW if not exists m_1 as select 1;"),
763            expect![[r#"
764                Ok(
765                    CreateMaterializedView {
766                        name: "m_1",
767                    },
768                )"#]],
769        )
770    }
771}