Skip to content

Commit 492c9e3

Browse files
authored
feat: support matching sqlstate with [statement|query] error (CCSSS) (#269)
1 parent 265f332 commit 492c9e3

File tree

16 files changed

+462
-45
lines changed

16 files changed

+462
-45
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,9 @@ jobs:
6565
profile: minimal
6666
toolchain: stable
6767
- name: Install semver-checks
68-
uses: actions-rs/cargo@v1
68+
uses: taiki-e/install-action@v2
6969
with:
70-
command: install
71-
args: cargo-semver-checks --version ^0.43 --locked
70+
tool: cargo-semver-checks
7271
- name: Check semver
7372
run: |
7473
cargo semver-checks check-release -p sqllogictest

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
## [0.29.0] - 2025-12-19
11+
12+
* parser/runner: Support matching expected failures by SQLSTATE via `statement|query error (<SQLSTATE>)`.
13+
1014
## [0.28.4] - 2025-09-04
1115

1216
* bin: support skip files with regex

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["sqllogictest", "sqllogictest-bin", "sqllogictest-engines", "tests"]
44

55
[workspace.package]
6-
version = "0.28.4"
6+
version = "0.29.0"
77
edition = "2021"
88
homepage = "https://github.com/risinglightdb/sqllogictest-rs"
99
keywords = ["sql", "database", "parser", "cli"]

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,12 @@ Seq Scan on t (cost=<slt:ignore> rows=<slt:ignore> width=<slt:ignore>)
8686
Filter: (x > 1)
8787
```
8888

89-
### Extension: Run a query/statement that should fail with the expacted error message
89+
### Extension: Run a query/statement that should fail with the expected error message
9090

9191
The syntax:
9292
- Do not check the error message: `[statement|query] error`
9393
- Single line error message (regexp match): `[statement|query] error <regex>`
94+
- Match SQLSTATE (exact match): `[statement|query] error (<SQLSTATE>)`
9495
- Multiline error message (exact match): Use `----`.
9596

9697
```text
@@ -99,6 +100,11 @@ The syntax:
99100
statement error Multiple object drop not supported
100101
DROP VIEW foo, bar;
101102
103+
# Ensure that the statement errors and that the SQLSTATE is exactly the expected one.
104+
# (This requires the engine to expose the SQLSTATE via `DB::error_sql_state` / `AsyncDB::error_sql_state`.)
105+
statement error (42P01)
106+
SELECT * FROM non_existent_table;
107+
102108
# The output error message must be the exact match of the expected one to pass the test,
103109
# except for the leading and trailing whitespaces.
104110
# Empty lines (not consecutive) are allowed in the expected error message. As a result, the message must end with 2 consecutive empty lines.

sqllogictest-bin/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ fancy-regex = "0.16"
2525
itertools = "0.13"
2626
quick-junit = { version = "0.5" }
2727
rand = "0.8"
28-
sqllogictest = { path = "../sqllogictest", version = "0.28" }
29-
sqllogictest-engines = { path = "../sqllogictest-engines", version = "0.28" }
28+
sqllogictest = { path = "../sqllogictest", version = "0.29" }
29+
sqllogictest-engines = { path = "../sqllogictest-engines", version = "0.29" }
3030
tokio = { version = "1", features = [
3131
"rt",
3232
"rt-multi-thread",

sqllogictest-bin/src/engines.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,17 @@ pub(crate) async fn connect(
7373
EngineConfig::MySql => Engines::MySql(
7474
MySql::connect(config.into())
7575
.await
76-
.map_err(|e| EnginesError(e.into()))?,
76+
.map_err(EnginesError::without_state)?,
7777
),
7878
EngineConfig::Postgres => Engines::Postgres(
7979
PostgresSimple::connect(config.into())
8080
.await
81-
.map_err(|e| EnginesError(e.into()))?,
81+
.map_err(EnginesError::without_state)?,
8282
),
8383
EngineConfig::PostgresExtended => Engines::PostgresExtended(
8484
PostgresExtended::connect(config.into())
8585
.await
86-
.map_err(|e| EnginesError(e.into()))?,
86+
.map_err(EnginesError::without_state)?,
8787
),
8888
EngineConfig::External(cmd_tmpl) => {
8989
let (host, port) = config.random_addr();
@@ -98,24 +98,36 @@ pub(crate) async fn connect(
9898
Engines::External(
9999
ExternalDriver::connect(cmd)
100100
.await
101-
.map_err(|e| EnginesError(e.into()))?,
101+
.map_err(EnginesError::without_state)?,
102102
)
103103
}
104104
})
105105
}
106106

107107
#[derive(Debug)]
108-
pub(crate) struct EnginesError(anyhow::Error);
108+
pub(crate) struct EnginesError {
109+
error: anyhow::Error,
110+
sqlstate: Option<String>,
111+
}
112+
113+
impl EnginesError {
114+
fn without_state(error: impl Into<anyhow::Error>) -> Self {
115+
Self {
116+
error: error.into(),
117+
sqlstate: None,
118+
}
119+
}
120+
}
109121

110122
impl Display for EnginesError {
111123
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112-
self.0.fmt(f)
124+
self.error.fmt(f)
113125
}
114126
}
115127

116128
impl std::error::Error for EnginesError {
117129
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118-
self.0.source()
130+
self.error.source()
119131
}
120132
}
121133

@@ -130,16 +142,21 @@ macro_rules! dispatch_engines {
130142
}};
131143
}
132144

145+
fn error_sql_state<E: AsyncDB>(_engine: &E, error: &E::Error) -> Option<String> {
146+
E::error_sql_state(error)
147+
}
148+
133149
#[async_trait]
134150
impl AsyncDB for Engines {
135151
type Error = EnginesError;
136152
type ColumnType = DefaultColumnType;
137153

138154
async fn run(&mut self, sql: &str) -> Result<DBOutput<Self::ColumnType>, Self::Error> {
139155
dispatch_engines!(self, e, {
140-
e.run(sql)
141-
.await
142-
.map_err(|e| EnginesError(anyhow::Error::from(e)))
156+
e.run(sql).await.map_err(|error| EnginesError {
157+
sqlstate: error_sql_state(e, &error),
158+
error: anyhow::Error::from(error),
159+
})
143160
})
144161
}
145162

@@ -158,4 +175,8 @@ impl AsyncDB for Engines {
158175
async fn shutdown(&mut self) {
159176
dispatch_engines!(self, e, { e.shutdown().await })
160177
}
178+
179+
fn error_sql_state(err: &Self::Error) -> Option<String> {
180+
err.sqlstate.clone()
181+
}
161182
}

sqllogictest-engines/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ postgres-types = { version = "0.2.8", features = ["derive", "with-chrono-0_4"] }
2020
rust_decimal = { version = "1.36.0", features = ["tokio-pg"] }
2121
serde = { version = "1", features = ["derive"] }
2222
serde_json = "1"
23-
sqllogictest = { path = "../sqllogictest", version = "0.28" }
23+
sqllogictest = { path = "../sqllogictest", version = "0.29" }
2424
thiserror = "2"
2525
tokio = { version = "1", features = [
2626
"rt",

sqllogictest-engines/src/mysql.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,12 @@ impl sqllogictest::AsyncDB for MySql {
8181
async fn run_command(command: Command) -> std::io::Result<std::process::Output> {
8282
tokio::process::Command::from(command).output().await
8383
}
84+
85+
fn error_sql_state(err: &Self::Error) -> Option<String> {
86+
if let mysql_async::Error::Server(err) = err {
87+
Some(err.state.clone())
88+
} else {
89+
None
90+
}
91+
}
8492
}

sqllogictest-engines/src/postgres/extended.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,8 @@ impl sqllogictest::AsyncDB for Postgres<Extended> {
326326
async fn run_command(command: Command) -> std::io::Result<std::process::Output> {
327327
tokio::process::Command::from(command).output().await
328328
}
329+
330+
fn error_sql_state(err: &Self::Error) -> Option<String> {
331+
err.code().map(|s| s.code().to_owned())
332+
}
329333
}

0 commit comments

Comments
 (0)