diff --git a/CHANGELOG.md b/CHANGELOG.md index 372fc82..3343948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [0.19.0] - 2023-11-11 + +* parser: refactor `expect` field in sqllogictest parser to make it easier to work with. + ## [0.18.0] - 2023-11-08 * Support matching multiline error message under `----` for both `statement error` and `query error`. diff --git a/Cargo.lock b/Cargo.lock index 126521b..e2b729d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "sqllogictest" -version = "0.18.0" +version = "0.19.0" dependencies = [ "async-trait", "educe", @@ -1426,7 +1426,7 @@ dependencies = [ [[package]] name = "sqllogictest-bin" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "async-trait", @@ -1447,7 +1447,7 @@ dependencies = [ [[package]] name = "sqllogictest-engines" -version = "0.18.0" +version = "0.19.0" dependencies = [ "async-trait", "bytes", diff --git a/Cargo.toml b/Cargo.toml index aebe372..bc9370a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["sqllogictest", "sqllogictest-bin", "sqllogictest-engines", "tests"] [workspace.package] -version = "0.18.0" +version = "0.19.0" edition = "2021" homepage = "https://github.com/risinglightdb/sqllogictest-rs" keywords = ["sql", "database", "parser", "cli"] diff --git a/sqllogictest-bin/Cargo.toml b/sqllogictest-bin/Cargo.toml index af04e20..f602c29 100644 --- a/sqllogictest-bin/Cargo.toml +++ b/sqllogictest-bin/Cargo.toml @@ -24,8 +24,8 @@ glob = "0.3" itertools = "0.11" quick-junit = { version = "0.3" } rand = "0.8" -sqllogictest = { path = "../sqllogictest", version = "0.18" } -sqllogictest-engines = { path = "../sqllogictest-engines", version = "0.18" } +sqllogictest = { path = "../sqllogictest", version = "0.19" } +sqllogictest-engines = { path = "../sqllogictest-engines", version = "0.19" } tokio = { version = "1", features = [ "rt", "rt-multi-thread", diff --git a/sqllogictest-engines/Cargo.toml b/sqllogictest-engines/Cargo.toml index 8530c97..3c8d26a 100644 --- a/sqllogictest-engines/Cargo.toml +++ b/sqllogictest-engines/Cargo.toml @@ -19,7 +19,7 @@ postgres-types = { version = "0.2.5", features = ["derive", "with-chrono-0_4"] } rust_decimal = { version = "1.30.0", features = ["tokio-pg"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -sqllogictest = { path = "../sqllogictest", version = "0.18" } +sqllogictest = { path = "../sqllogictest", version = "0.19" } thiserror = "1" tokio = { version = "1", features = [ "rt", diff --git a/sqllogictest/src/parser.rs b/sqllogictest/src/parser.rs index 965dc62..ac2f5ab 100644 --- a/sqllogictest/src/parser.rs +++ b/sqllogictest/src/parser.rs @@ -6,7 +6,6 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; -use educe::Educe; use itertools::Itertools; use regex::Regex; @@ -69,9 +68,45 @@ impl Location { } } +/// Expectation for a statement. +#[derive(Debug, Clone, PartialEq)] +pub enum StatementExpect { + /// Statement should succeed. + Ok, + /// Statement should succeed and affect the given number of rows. + Count(u64), + /// Statement should fail with the given error message. + Error(ExpectedError), +} + +/// Expectation for a query. +#[derive(Debug, Clone, PartialEq)] +pub enum QueryExpect { + /// Query should succeed and return the given results. + Results { + types: Vec, + sort_mode: Option, + label: Option, + results: Vec, + }, + /// Query should fail with the given error message. + Error(ExpectedError), +} + +impl QueryExpect { + /// Creates a new [`QueryExpect`] with empty results. + fn empty_results() -> Self { + Self::Results { + types: Vec::new(), + sort_mode: None, + label: None, + results: Vec::new(), + } + } +} + /// A single directive in a sqllogictest file. -#[derive(Debug, Clone, Educe)] -#[educe(PartialEq)] +#[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Record { /// An include copies all records from another files. @@ -85,13 +120,9 @@ pub enum Record { loc: Location, conditions: Vec, connection: Connection, - /// The SQL command is expected to fail with an error messages that matches the given - /// regex. If the regex is an empty string, any error message is accepted. - expected_error: Option, /// The SQL command. sql: String, - /// Expected rows affected. - expected_count: Option, + expected: StatementExpect, }, /// A query is an SQL command from which we expect to receive results. The result set might be /// empty. @@ -99,16 +130,9 @@ pub enum Record { loc: Location, conditions: Vec, connection: Connection, - expected_types: Vec, - sort_mode: Option, - label: Option, - /// The SQL command is expected to fail with an error messages that matches the given - /// regex. If the regex is an empty string, any error message is accepted. - expected_error: Option, /// The SQL command. sql: String, - /// The expected results. - expected_results: Vec, + expected: QueryExpect, }, /// A system command is an external command that is to be executed by the shell. Currently it /// must succeed and the output is ignored. @@ -178,22 +202,20 @@ impl std::fmt::Display for Record { loc: _, conditions: _, connection: _, - expected_error, sql, - expected_count, + expected, } => { write!(f, "statement ")?; - match (expected_count, expected_error) { - (None, None) => write!(f, "ok")?, - (None, Some(err)) => err.fmt_inline(f)?, - (Some(cnt), None) => write!(f, "count {cnt}")?, - (Some(_), Some(_)) => unreachable!(), + match expected { + StatementExpect::Ok => write!(f, "ok")?, + StatementExpect::Count(cnt) => write!(f, "count {cnt}")?, + StatementExpect::Error(err) => err.fmt_inline(f)?, } writeln!(f)?; // statement always end with a blank line writeln!(f, "{sql}")?; - if let Some(err) = expected_error { + if let StatementExpect::Error(err) = expected { err.fmt_multiline(f)?; } Ok(()) @@ -202,39 +224,43 @@ impl std::fmt::Display for Record { loc: _, conditions: _, connection: _, - expected_types, - sort_mode, - label, - expected_error, sql, - expected_results, + expected, } => { write!(f, "query ")?; - if let Some(err) = expected_error { - err.fmt_inline(f)?; - } else { - write!(f, "{}", expected_types.iter().map(|c| c.to_char()).join(""))?; - if let Some(sort_mode) = sort_mode { - write!(f, " {}", sort_mode.as_str())?; - } - if let Some(label) = label { - write!(f, " {label}")?; + match expected { + QueryExpect::Results { + types, + sort_mode, + label, + .. + } => { + write!(f, "{}", types.iter().map(|c| c.to_char()).join(""))?; + if let Some(sort_mode) = sort_mode { + write!(f, " {}", sort_mode.as_str())?; + } + if let Some(label) = label { + write!(f, " {label}")?; + } } + QueryExpect::Error(err) => err.fmt_inline(f)?, } writeln!(f)?; writeln!(f, "{sql}")?; - if let Some(err) = expected_error { - err.fmt_multiline(f) - } else { - write!(f, "{}", RESULTS_DELIMITER)?; + match expected { + QueryExpect::Results { results, .. } => { + write!(f, "{}", RESULTS_DELIMITER)?; - for result in expected_results { - write!(f, "\n{result}")?; + for result in results { + write!(f, "\n{result}")?; + } + // query always ends with a blank line + writeln!(f)? } - // query always ends with a blank line - writeln!(f) + QueryExpect::Error(err) => err.fmt_multiline(f)?, } + Ok(()) } Record::System { loc: _, @@ -481,7 +507,7 @@ impl Connection { } /// Whether to apply sorting before checking the results of a query. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum SortMode { /// The default option. The results appear in exactly the order in which they were received /// from the database engine. @@ -658,29 +684,27 @@ fn parse_inner(loc: &Location, script: &str) -> Result { - let mut expected_count = None; - let mut expected_error = None; - match res { - ["ok"] => {} + let mut expected = match res { + ["ok"] => StatementExpect::Ok, ["error", tokens @ ..] => { - expected_error = Some( - ExpectedError::parse_inline_tokens(tokens) - .map_err(|e| e.at(loc.clone()))?, - ); + let error = ExpectedError::parse_inline_tokens(tokens) + .map_err(|e| e.at(loc.clone()))?; + StatementExpect::Error(error) } ["count", count_str] => { - expected_count = Some(count_str.parse::().map_err(|_| { + let count = count_str.parse::().map_err(|_| { ParseErrorKind::InvalidNumber((*count_str).into()).at(loc.clone()) - })?); + })?; + StatementExpect::Count(count) } _ => return Err(ParseErrorKind::InvalidLine(line.into()).at(loc)), }; let (sql, has_results) = parse_lines(&mut lines, &loc, Some(RESULTS_DELIMITER))?; if has_results { - if let Some(e) = &expected_error { + if let StatementExpect::Error(e) = &mut expected { // If no inline error message is specified, it might be a multiline error. if e.is_empty() { - expected_error = Some(parse_multiline_error(&mut lines)); + *e = parse_multiline_error(&mut lines); } else { return Err(ParseErrorKind::DuplicatedErrorMessage.at(loc.clone())); } @@ -693,60 +717,63 @@ fn parse_inner(loc: &Location, script: &str) -> Result { - let mut expected_types = vec![]; - let mut sort_mode = None; - let mut label = None; - let mut expected_error = None; - match res { + let mut expected = match res { ["error", tokens @ ..] => { - expected_error = Some( - ExpectedError::parse_inline_tokens(tokens) - .map_err(|e| e.at(loc.clone()))?, - ); + let error = ExpectedError::parse_inline_tokens(tokens) + .map_err(|e| e.at(loc.clone()))?; + QueryExpect::Error(error) } [type_str, res @ ..] => { - expected_types = type_str + let types = type_str .chars() .map(|ch| { T::from_char(ch) .ok_or_else(|| ParseErrorKind::InvalidType(ch).at(loc.clone())) }) .try_collect()?; - sort_mode = res + let sort_mode = res .first() .map(|&s| SortMode::try_from_str(s)) .transpose() .map_err(|e| e.at(loc.clone()))?; - label = res.get(1).map(|s| s.to_string()); + let label = res.get(1).map(|s| s.to_string()); + QueryExpect::Results { + types, + sort_mode, + label, + results: Vec::new(), + } } - [] => {} - } + [] => QueryExpect::empty_results(), + }; // The SQL for the query is found on second an subsequent lines of the record // up to first line of the form "----" or until the end of the record. let (sql, has_result) = parse_lines(&mut lines, &loc, Some(RESULTS_DELIMITER))?; - // Lines following the "----" are expected results of the query, one value per line. - let mut expected_results = vec![]; if has_result { - if let Some(e) = &expected_error { - // If no inline error message is specified, it might be a multiline error. - if e.is_empty() { - expected_error = Some(parse_multiline_error(&mut lines)); - } else { - return Err(ParseErrorKind::DuplicatedErrorMessage.at(loc.clone())); + match &mut expected { + // Lines following the "----" are expected results of the query, one value + // per line. + QueryExpect::Results { results, .. } => { + for (_, line) in &mut lines { + if line.is_empty() { + break; + } + results.push(line.to_string()); + } } - } else { - for (_, line) in &mut lines { - if line.is_empty() { - break; + // If no inline error message is specified, it might be a multiline error. + QueryExpect::Error(e) => { + if e.is_empty() { + *e = parse_multiline_error(&mut lines); + } else { + return Err(ParseErrorKind::DuplicatedErrorMessage.at(loc.clone())); } - expected_results.push(line.to_string()); } } } @@ -754,12 +781,8 @@ fn parse_inner(loc: &Location, script: &str) -> Result { @@ -988,12 +1011,8 @@ select * from foo; loc: Location::new("", 1), conditions: vec![], connection: Connection::Default, - expected_types: vec![], - sort_mode: None, - label: None, - expected_error: None, sql: "select * from foo;".to_string(), - expected_results: vec![], + expected: QueryExpect::empty_results(), }] ); } diff --git a/sqllogictest/src/runner.rs b/sqllogictest/src/runner.rs index 7dbd370..b26ce4f 100644 --- a/sqllogictest/src/runner.rs +++ b/sqllogictest/src/runner.rs @@ -544,8 +544,7 @@ impl> Runner { sql, // compare result in run_async - expected_error: _, - expected_count: _, + expected: _, loc: _, } => { let sql = match self.may_substitute(sql) { @@ -630,16 +629,10 @@ impl> Runner { conditions, connection, sql, - sort_mode, // compare result in run_async - expected_types: _, - expected_error: _, - expected_results: _, + expected, loc: _, - - // not handle yet, - label: _, } => { let sql = match self.may_substitute(sql) { Ok(sql) => sql, @@ -682,7 +675,12 @@ impl> Runner { } }; - match sort_mode.as_ref().or(self.sort_mode.as_ref()) { + let sort_mode = match expected { + QueryExpect::Results { sort_mode, .. } => sort_mode, + QueryExpect::Error(_) => None, + } + .or(self.sort_mode); + match sort_mode { None | Some(SortMode::NoSort) => {} Some(SortMode::RowSort) => { rows.sort_unstable(); @@ -754,14 +752,11 @@ impl> Runner { // Tolerate the mismatched return type... ( Record::Statement { - sql, - expected_error, - loc, - .. + sql, expected, loc, .. }, RecordOutput::Query { error: None, .. }, ) => { - if expected_error.is_some() { + if let StatementExpect::Error(_) = expected { return Err(TestErrorKind::Ok { sql, kind: RecordKind::Query, @@ -771,61 +766,56 @@ impl> Runner { } ( Record::Query { - expected_results, - loc, - sql, - expected_error, - .. + loc, sql, expected, .. }, RecordOutput::Statement { error: None, .. }, - ) => { - if expected_error.is_some() { + ) => match expected { + QueryExpect::Error(_) => { return Err(TestErrorKind::Ok { sql, kind: RecordKind::Query, } - .at(loc)); + .at(loc)) } - if !expected_results.is_empty() { + QueryExpect::Results { results, .. } if !results.is_empty() => { return Err(TestErrorKind::QueryResultMismatch { sql, - expected: expected_results.join("\n"), + expected: results.join("\n"), actual: "".to_string(), } - .at(loc)); + .at(loc)) } - } + QueryExpect::Results { .. } => {} + }, ( Record::Statement { loc, connection: _, conditions: _, - expected_error, sql, - expected_count, + expected, }, RecordOutput::Statement { count, error }, - ) => match (error, expected_error) { - (None, Some(_)) => { + ) => match (error, expected) { + (None, StatementExpect::Error(_)) => { return Err(TestErrorKind::Ok { sql, kind: RecordKind::Statement, } .at(loc)) } - (None, None) => { - if let Some(expected_count) = expected_count { - if expected_count != count { - return Err(TestErrorKind::StatementResultMismatch { - sql, - expected: expected_count, - actual: format!("affected {count} rows"), - } - .at(loc)); + (None, StatementExpect::Count(expected_count)) => { + if expected_count != count { + return Err(TestErrorKind::StatementResultMismatch { + sql, + expected: expected_count, + actual: format!("affected {count} rows"), } + .at(loc)); } } - (Some(e), Some(expected_error)) => { + (None, StatementExpect::Ok) => {} + (Some(e), StatementExpect::Error(expected_error)) => { if !expected_error.is_match(&e.to_string()) { return Err(TestErrorKind::ErrorMismatch { sql, @@ -836,7 +826,7 @@ impl> Runner { .at(loc)); } } - (Some(e), None) => { + (Some(e), StatementExpect::Count(_) | StatementExpect::Ok) => { return Err(TestErrorKind::Fail { sql, err: Arc::new(e), @@ -850,25 +840,20 @@ impl> Runner { loc, conditions: _, connection: _, - expected_types, - sort_mode: _, - label: _, - expected_error, sql, - expected_results, + expected, }, RecordOutput::Query { types, rows, error }, ) => { - match (error, expected_error) { - (None, Some(_)) => { + match (error, expected) { + (None, QueryExpect::Error(_)) => { return Err(TestErrorKind::Ok { sql, kind: RecordKind::Query, } .at(loc)); } - (None, None) => {} - (Some(e), Some(expected_error)) => { + (Some(e), QueryExpect::Error(expected_error)) => { if !expected_error.is_match(&e.to_string()) { return Err(TestErrorKind::ErrorMismatch { sql, @@ -878,9 +863,8 @@ impl> Runner { } .at(loc)); } - return Ok(()); } - (Some(e), None) => { + (Some(e), QueryExpect::Results { .. }) => { return Err(TestErrorKind::Fail { sql, err: Arc::new(e), @@ -888,29 +872,37 @@ impl> Runner { } .at(loc)); } - }; - - if !(self.column_type_validator)(&types, &expected_types) { - return Err(TestErrorKind::QueryResultColumnsMismatch { - sql, - expected: expected_types.iter().map(|c| c.to_char()).join(""), - actual: types.iter().map(|c| c.to_char()).join(""), - } - .at(loc)); - } + ( + None, + QueryExpect::Results { + types: expected_types, + results: expected_results, + .. + }, + ) => { + if !(self.column_type_validator)(&types, &expected_types) { + return Err(TestErrorKind::QueryResultColumnsMismatch { + sql, + expected: expected_types.iter().map(|c| c.to_char()).join(""), + actual: types.iter().map(|c| c.to_char()).join(""), + } + .at(loc)); + } - if !(self.validator)(&rows, &expected_results) { - let output_rows = rows - .into_iter() - .map(|strs| strs.iter().join(" ")) - .collect_vec(); - return Err(TestErrorKind::QueryResultMismatch { - sql, - expected: expected_results.join("\n"), - actual: output_rows.join("\n"), + if !(self.validator)(&rows, &expected_results) { + let output_rows = rows + .into_iter() + .map(|strs| strs.iter().join(" ")) + .collect_vec(); + return Err(TestErrorKind::QueryResultMismatch { + sql, + expected: expected_results.join("\n"), + actual: output_rows.join("\n"), + } + .at(loc)); + } } - .at(loc)); - } + }; } ( Record::System { @@ -1222,7 +1214,7 @@ impl> Runner { /// Updates the specified [`Record`] with the [`QueryOutput`] produced /// by a Database, returning `Some(new_record)`. /// -/// If an update is not supported, returns `None` +/// If an update is not supported or not necessary, returns `None` pub fn update_record_with_output( record: &Record, record_output: &RecordOutput, @@ -1239,8 +1231,7 @@ pub fn update_record_with_output( loc, conditions, connection, - expected_error: None, - expected_count, + expected: expected @ (StatementExpect::Ok | StatementExpect::Count(_)), }, RecordOutput::Query { error: None, .. }, ) => { @@ -1253,11 +1244,10 @@ pub fn update_record_with_output( Some(Record::Statement { sql, - expected_error: None, loc, conditions, connection, - expected_count, + expected, }) } // query, statement @@ -1267,16 +1257,15 @@ pub fn update_record_with_output( loc, conditions, connection, - .. + expected: _, }, RecordOutput::Statement { error: None, .. }, ) => Some(Record::Statement { sql, - expected_error: None, loc, conditions, connection, - expected_count: None, + expected: StatementExpect::Ok, }), // statement, statement ( @@ -1284,41 +1273,45 @@ pub fn update_record_with_output( loc, conditions, connection, - expected_error, sql, - expected_count, + expected, }, RecordOutput::Statement { count, error }, - ) => match (error, expected_error) { + ) => match (error, expected) { // Ok - (None, _) => Some(Record::Statement { + (None, expected) => Some(Record::Statement { sql, - expected_error: None, loc, conditions, connection, - expected_count: expected_count.map(|_| *count), + expected: match expected { + StatementExpect::Count(_) => StatementExpect::Count(*count), + StatementExpect::Error(_) | StatementExpect::Ok => StatementExpect::Ok, + }, }), // Error match - (Some(e), Some(expected_error)) if expected_error.is_match(&e.to_string()) => { + (Some(e), StatementExpect::Error(expected_error)) + if expected_error.is_match(&e.to_string()) => + { + None + } + // Error mismatch, update expected error + (Some(e), r) => { + let reference = match &r { + StatementExpect::Error(e) => Some(e), + StatementExpect::Count(_) | StatementExpect::Ok => None, + }; Some(Record::Statement { sql, - expected_error: Some(expected_error), + expected: StatementExpect::Error(ExpectedError::from_actual_error( + reference, + &e.to_string(), + )), loc, conditions, connection, - expected_count: None, }) } - // Error mismatch, update expected error - (Some(e), r) => Some(Record::Statement { - sql, - expected_error: Some(ExpectedError::from_actual_error(r.as_ref(), &e.to_string())), - loc, - conditions, - connection, - expected_count: None, - }), }, // query, query ( @@ -1326,76 +1319,75 @@ pub fn update_record_with_output( loc, conditions, connection, - expected_types, - sort_mode, - label, - expected_error, sql, - expected_results, + expected, }, RecordOutput::Query { types, rows, error }, - ) => { - match (error, expected_error) { - (None, _) => {} - // Error match - (Some(e), Some(expected_error)) if expected_error.is_match(&e.to_string()) => { - return Some(Record::Query { - sql, - expected_error: Some(expected_error), - loc, - conditions, - connection, - expected_types: vec![], - sort_mode, - label, - expected_results: vec![], - }); - } - // Error mismatch - (Some(e), r) => { - return Some(Record::Query { - sql, - expected_error: Some(ExpectedError::from_actual_error( - r.as_ref(), - &e.to_string(), - )), - loc, - conditions, - connection, - expected_types: vec![], - sort_mode, - label, - expected_results: vec![], - }); - } - }; - - let results = if validator(rows, &expected_results) { - // If validation is successful, we respect the original file's expected results. - expected_results - } else { - rows.iter().map(|cols| cols.join(col_separator)).collect() - }; - - let types = if column_type_validator(types, &expected_types) { - // If validation is successful, we respect the original file's expected types. - expected_types - } else { - types.clone() - }; - - Some(Record::Query { - sql, - expected_error: None, - loc, - conditions, - connection, - expected_types: types, - sort_mode, - label, - expected_results: results, - }) - } + ) => match (error, expected) { + // Error match + (Some(e), QueryExpect::Error(expected_error)) + if expected_error.is_match(&e.to_string()) => + { + None + } + // Error mismatch + (Some(e), r) => { + let reference = match &r { + QueryExpect::Error(e) => Some(e), + QueryExpect::Results { .. } => None, + }; + Some(Record::Query { + sql, + expected: QueryExpect::Error(ExpectedError::from_actual_error( + reference, + &e.to_string(), + )), + loc, + conditions, + connection, + }) + } + (None, expected) => { + let results = match &expected { + // If validation is successful, we respect the original file's expected results. + QueryExpect::Results { + results: expected_results, + .. + } if validator(rows, expected_results) => expected_results.clone(), + _ => rows.iter().map(|cols| cols.join(col_separator)).collect(), + }; + let types = match &expected { + // If validation is successful, we respect the original file's expected types. + QueryExpect::Results { + types: expected_types, + .. + } if column_type_validator(types, expected_types) => expected_types.clone(), + _ => types.clone(), + }; + Some(Record::Query { + sql, + loc, + conditions, + connection, + expected: match expected { + QueryExpect::Results { + sort_mode, label, .. + } => QueryExpect::Results { + results, + types, + sort_mode, + label, + }, + QueryExpect::Error(_) => QueryExpect::Results { + results, + types, + sort_mode: None, + label: None, + }, + }, + }) + } + }, // No update possible, return the original record _ => None, @@ -1628,11 +1620,9 @@ Caused by: // Model a run that produced an error message record_output: statement_output_error("foo"), - // Input didn't have an expected error, so output is not to expect the message - expected: Some( - "statement error\n\ - insert into foo values(2);", - ), + // Input didn't have an expected error, so output is not to expect the message, then no + // update + expected: None, } .run() } @@ -1754,11 +1744,8 @@ Caused by: "The operation (inser) is not supported. Did you mean [insert]?", ), - // expect the output includes foo - expected: Some( - "statement error TestError: The operation \\([a-z]+\\) is not supported.*\n\ - inser into foo values(2);", - ), + // no update expected + expected: None, } .run() } @@ -1794,11 +1781,8 @@ Caused by: "The operation (selec) is not supported. Did you mean [select]?", ), - // expect the output includes foo - expected: Some( - "query error TestError: The operation \\([a-z]+\\) is not supported.*\n\ - selec *;", - ), + // no update expected + expected: None, } .run() }