mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 06:24:16 +00:00
feat: better database errors (#2109)
* feat(core): create error kind enum * feat(core): add error kind for postgres * feat(core): add error kind for sqlite * feat(core): add error kind for mysql * test(postgres): add error tests * test(sqlite): add error tests * test(mysql): add error tests * fix(tests): fix tests rebasing * refac(errors): add `ErrorKind::Other` variant
This commit is contained in:
parent
771ab80a62
commit
c09532864d
15 changed files with 451 additions and 67 deletions
3
.github/workflows/sqlx.yml
vendored
3
.github/workflows/sqlx.yml
vendored
|
@ -271,6 +271,7 @@ jobs:
|
|||
--features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }}
|
||||
env:
|
||||
DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled
|
||||
RUSTFLAGS: --cfg mysql_${{ matrix.mysql }}
|
||||
|
||||
# MySQL 5.7 supports TLS but not TLSv1.3 as required by RusTLS.
|
||||
- uses: actions-rs/cargo@v1
|
||||
|
@ -282,6 +283,7 @@ jobs:
|
|||
--features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }}
|
||||
env:
|
||||
DATABASE_URL: mysql://root:password@localhost:3306/sqlx
|
||||
RUSTFLAGS: --cfg mysql_${{ matrix.mysql }}
|
||||
|
||||
mariadb:
|
||||
name: MariaDB
|
||||
|
@ -322,3 +324,4 @@ jobs:
|
|||
--features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }}
|
||||
env:
|
||||
DATABASE_URL: mysql://root:password@localhost:3306/sqlx
|
||||
RUSTFLAGS: --cfg mariadb_${{ matrix.mariadb }}
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -223,6 +223,11 @@ name = "sqlite-derives"
|
|||
path = "tests/sqlite/derives.rs"
|
||||
required-features = ["sqlite", "macros"]
|
||||
|
||||
[[test]]
|
||||
name = "sqlite-error"
|
||||
path = "tests/sqlite/error.rs"
|
||||
required-features = ["sqlite"]
|
||||
|
||||
[[test]]
|
||||
name = "sqlite-sqlcipher"
|
||||
path = "tests/sqlite/sqlcipher.rs"
|
||||
|
@ -262,6 +267,11 @@ name = "mysql-macros"
|
|||
path = "tests/mysql/macros.rs"
|
||||
required-features = ["mysql", "macros"]
|
||||
|
||||
[[test]]
|
||||
name = "mysql-error"
|
||||
path = "tests/mysql/error.rs"
|
||||
required-features = ["mysql"]
|
||||
|
||||
[[test]]
|
||||
name = "mysql-test-attr"
|
||||
path = "tests/mysql/test-attr.rs"
|
||||
|
@ -301,6 +311,11 @@ name = "postgres-derives"
|
|||
path = "tests/postgres/derives.rs"
|
||||
required-features = ["postgres", "macros"]
|
||||
|
||||
[[test]]
|
||||
name = "postgres-error"
|
||||
path = "tests/postgres/error.rs"
|
||||
required-features = ["postgres"]
|
||||
|
||||
[[test]]
|
||||
name = "postgres-test-attr"
|
||||
path = "tests/postgres/test-attr.rs"
|
||||
|
|
|
@ -160,6 +160,25 @@ pub fn mismatched_types<DB: Database, T: Type<DB>>(ty: &DB::TypeInfo) -> BoxDynE
|
|||
.into()
|
||||
}
|
||||
|
||||
/// The error kind.
|
||||
///
|
||||
/// This enum is to be used to identify frequent errors that can be handled by the program.
|
||||
/// Although it currently only supports constraint violations, the type may grow in the future.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrorKind {
|
||||
/// Unique/primary key constraint violation.
|
||||
UniqueViolation,
|
||||
/// Foreign key constraint violation.
|
||||
ForeignKeyViolation,
|
||||
/// Not-null constraint violation.
|
||||
NotNullViolation,
|
||||
/// Check constraint violation.
|
||||
CheckViolation,
|
||||
/// An unmapped error.
|
||||
Other,
|
||||
}
|
||||
|
||||
/// An error that was returned from the database.
|
||||
pub trait DatabaseError: 'static + Send + Sync + StdError {
|
||||
/// The primary, human-readable error message.
|
||||
|
@ -192,6 +211,27 @@ pub trait DatabaseError: 'static + Send + Sync + StdError {
|
|||
fn constraint(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the kind of the error, if supported.
|
||||
///
|
||||
/// ### Note
|
||||
/// Not all back-ends behave the same when reporting the error code.
|
||||
fn kind(&self) -> ErrorKind;
|
||||
|
||||
/// Returns whether the error kind is a violation of a unique/primary key constraint.
|
||||
fn is_unique_violation(&self) -> bool {
|
||||
matches!(self.kind(), ErrorKind::UniqueViolation)
|
||||
}
|
||||
|
||||
/// Returns whether the error kind is a violation of a foreign key.
|
||||
fn is_foreign_key_violation(&self) -> bool {
|
||||
matches!(self.kind(), ErrorKind::ForeignKeyViolation)
|
||||
}
|
||||
|
||||
/// Returns whether the error kind is a violation of a check.
|
||||
fn is_check_violation(&self) -> bool {
|
||||
matches!(self.kind(), ErrorKind::CheckViolation)
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn DatabaseError {
|
||||
|
|
|
@ -78,4 +78,80 @@ impl DatabaseError for MySqlDatabaseError {
|
|||
fn into_error(self: Box<Self>) -> Box<dyn StdError + Send + Sync + 'static> {
|
||||
self
|
||||
}
|
||||
|
||||
fn kind(&self) -> ErrorKind {
|
||||
match self.number() {
|
||||
error_codes::ER_DUP_KEY
|
||||
| error_codes::ER_DUP_ENTRY
|
||||
| error_codes::ER_DUP_UNIQUE
|
||||
| error_codes::ER_DUP_ENTRY_WITH_KEY_NAME
|
||||
| error_codes::ER_DUP_UNKNOWN_IN_INDEX => ErrorKind::UniqueViolation,
|
||||
|
||||
error_codes::ER_NO_REFERENCED_ROW
|
||||
| error_codes::ER_NO_REFERENCED_ROW_2
|
||||
| error_codes::ER_ROW_IS_REFERENCED
|
||||
| error_codes::ER_ROW_IS_REFERENCED_2
|
||||
| error_codes::ER_FK_COLUMN_NOT_NULL
|
||||
| error_codes::ER_FK_CANNOT_DELETE_PARENT => ErrorKind::ForeignKeyViolation,
|
||||
|
||||
error_codes::ER_BAD_NULL_ERROR | error_codes::ER_NO_DEFAULT_FOR_FIELD => {
|
||||
ErrorKind::NotNullViolation
|
||||
}
|
||||
|
||||
error_codes::ER_CHECK_CONSTRAINT_VIOLATED => ErrorKind::CheckViolation,
|
||||
|
||||
_ => ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The MySQL server uses SQLSTATEs as a generic error category,
|
||||
/// and returns a `error_code` instead within the error packet.
|
||||
///
|
||||
/// For reference: <https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html>.
|
||||
pub(crate) mod error_codes {
|
||||
/// Caused when a DDL operation creates duplicated keys.
|
||||
pub const ER_DUP_KEY: u16 = 1022;
|
||||
/// Caused when a DML operation tries create a duplicated entry for a key,
|
||||
/// be it a unique or primary one.
|
||||
pub const ER_DUP_ENTRY: u16 = 1062;
|
||||
/// Similar to `ER_DUP_ENTRY`, but only present in NDB clusters.
|
||||
///
|
||||
/// See: <https://github.com/mysql/mysql-server/blob/fbdaa4def30d269bc4de5b85de61de34b11c0afc/mysql-test/suite/stress/include/ddl7.inc#L68>.
|
||||
pub const ER_DUP_UNIQUE: u16 = 1169;
|
||||
/// Similar to `ER_DUP_ENTRY`, but with a formatted string message.
|
||||
///
|
||||
/// See: <https://bugs.mysql.com/bug.php?id=46976>.
|
||||
pub const ER_DUP_ENTRY_WITH_KEY_NAME: u16 = 1586;
|
||||
/// Caused when a DDL operation to add a unique index fails,
|
||||
/// because duplicate items were created by concurrent DML operations.
|
||||
/// When this happens, the key is unknown, so the server can't use `ER_DUP_KEY`.
|
||||
///
|
||||
/// For example: an `INSERT` operation creates duplicate `name` fields when `ALTER`ing a table and making `name` unique.
|
||||
pub const ER_DUP_UNKNOWN_IN_INDEX: u16 = 1859;
|
||||
|
||||
/// Caused when inserting an entry with a column with a value that does not reference a foreign row.
|
||||
pub const ER_NO_REFERENCED_ROW: u16 = 1216;
|
||||
/// Caused when deleting a row that is referenced in other tables.
|
||||
pub const ER_ROW_IS_REFERENCED: u16 = 1217;
|
||||
/// Caused when deleting a row that is referenced in other tables.
|
||||
/// This differs from `ER_ROW_IS_REFERENCED` in that the error message contains the affected constraint.
|
||||
pub const ER_ROW_IS_REFERENCED_2: u16 = 1451;
|
||||
/// Caused when inserting an entry with a column with a value that does not reference a foreign row.
|
||||
/// This differs from `ER_NO_REFERENCED_ROW` in that the error message contains the affected constraint.
|
||||
pub const ER_NO_REFERENCED_ROW_2: u16 = 1452;
|
||||
/// Caused when creating a FK with `ON DELETE SET NULL` or `ON UPDATE SET NULL` to a column that is `NOT NULL`, or vice-versa.
|
||||
pub const ER_FK_COLUMN_NOT_NULL: u16 = 1830;
|
||||
/// Removed in 5.7.3.
|
||||
pub const ER_FK_CANNOT_DELETE_PARENT: u16 = 1834;
|
||||
|
||||
/// Caused when inserting a NULL value to a column marked as NOT NULL.
|
||||
pub const ER_BAD_NULL_ERROR: u16 = 1048;
|
||||
/// Caused when inserting a DEFAULT value to a column marked as NOT NULL, which also doesn't have a default value set.
|
||||
pub const ER_NO_DEFAULT_FOR_FIELD: u16 = 1364;
|
||||
|
||||
/// Caused when a check constraint is violated.
|
||||
///
|
||||
/// Only available after 8.0.16.
|
||||
pub const ER_CHECK_CONSTRAINT_VIOLATED: u16 = 3819;
|
||||
}
|
||||
|
|
|
@ -203,4 +203,26 @@ impl DatabaseError for PgDatabaseError {
|
|||
fn constraint(&self) -> Option<&str> {
|
||||
self.constraint()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ErrorKind {
|
||||
match self.code() {
|
||||
error_codes::UNIQUE_VIOLATION => ErrorKind::UniqueViolation,
|
||||
error_codes::FOREIGN_KEY_VIOLATION => ErrorKind::ForeignKeyViolation,
|
||||
error_codes::NOT_NULL_VIOLATION => ErrorKind::NotNullViolation,
|
||||
error_codes::CHECK_VIOLATION => ErrorKind::CheckViolation,
|
||||
_ => ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For reference: <https://www.postgresql.org/docs/current/errcodes-appendix.html>
|
||||
pub(crate) mod error_codes {
|
||||
/// Caused when a unique or primary key is violated.
|
||||
pub const UNIQUE_VIOLATION: &str = "23505";
|
||||
/// Caused when a foreign key is violated.
|
||||
pub const FOREIGN_KEY_VIOLATION: &str = "23503";
|
||||
/// Caused when a column marked as NOT NULL received a null value.
|
||||
pub const NOT_NULL_VIOLATION: &str = "23502";
|
||||
/// Caused when a check constraint is violated.
|
||||
pub const CHECK_VIOLATION: &str = "23514";
|
||||
}
|
||||
|
|
|
@ -4,7 +4,11 @@ use std::fmt::{self, Display, Formatter};
|
|||
use std::os::raw::c_int;
|
||||
use std::{borrow::Cow, str::from_utf8_unchecked};
|
||||
|
||||
use libsqlite3_sys::{sqlite3, sqlite3_errmsg, sqlite3_extended_errcode};
|
||||
use libsqlite3_sys::{
|
||||
sqlite3, sqlite3_errmsg, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK,
|
||||
SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY,
|
||||
SQLITE_CONSTRAINT_UNIQUE,
|
||||
};
|
||||
|
||||
pub(crate) use sqlx_core::error::*;
|
||||
|
||||
|
@ -82,4 +86,14 @@ impl DatabaseError for SqliteError {
|
|||
fn into_error(self: Box<Self>) -> Box<dyn StdError + Send + Sync + 'static> {
|
||||
self
|
||||
}
|
||||
|
||||
fn kind(&self) -> ErrorKind {
|
||||
match self.code {
|
||||
SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => ErrorKind::UniqueViolation,
|
||||
SQLITE_CONSTRAINT_FOREIGNKEY => ErrorKind::ForeignKeyViolation,
|
||||
SQLITE_CONSTRAINT_NOTNULL => ErrorKind::NotNullViolation,
|
||||
SQLITE_CONSTRAINT_CHECK => ErrorKind::CheckViolation,
|
||||
_ => ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,44 +173,3 @@ services:
|
|||
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
|
||||
command: >
|
||||
-c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
|
||||
#
|
||||
# Microsoft SQL Server (MSSQL)
|
||||
# https://hub.docker.com/_/microsoft-mssql-server
|
||||
#
|
||||
|
||||
mssql_2019:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: mssql/Dockerfile
|
||||
args:
|
||||
VERSION: 2019-latest
|
||||
ports:
|
||||
- 1433
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: Password123!
|
||||
|
||||
mssql_2017:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: mssql/mssql-2017.dockerfile
|
||||
args:
|
||||
VERSION: 2017-latest
|
||||
ports:
|
||||
- 1433
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: Password123!
|
||||
|
||||
#
|
||||
# IBM Db2
|
||||
# https://hub.docker.com/r/ibmcom/db2
|
||||
#
|
||||
|
||||
db2_11:
|
||||
image: ibmcom/db2:11.5.1.0-CN1
|
||||
privileged: true
|
||||
environment:
|
||||
LICENSE: accept
|
||||
DB2INST1_PASSWORD: password
|
||||
DBNAME: sqlx
|
||||
|
|
77
tests/mysql/error.rs
Normal file
77
tests/mysql/error.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use sqlx::{error::ErrorKind, mysql::MySql, Connection};
|
||||
use sqlx_test::new;
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_unique_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<MySql>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
sqlx::query("INSERT INTO tweet(id, text, owner_id) VALUES (1, 'Foo', 1)")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet VALUES (1, NOW(), 'Foo', 1);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::UniqueViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_foreign_key_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<MySql>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO tweet_reply (tweet_id, text) VALUES (1, 'Reply!');")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::ForeignKeyViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_not_null_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<MySql>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet (text) VALUES (null);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::NotNullViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(mysql_8)]
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_check_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<MySql>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO products VALUES (1, 'Product 1', 0);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::CheckViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -6,3 +6,19 @@ CREATE TABLE tweet
|
|||
text TEXT NOT NULL,
|
||||
owner_id BIGINT
|
||||
);
|
||||
|
||||
CREATE TABLE tweet_reply
|
||||
(
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
tweet_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
text TEXT NOT NULL,
|
||||
owner_id BIGINT,
|
||||
CONSTRAINT tweet_id_fk FOREIGN KEY (tweet_id) REFERENCES tweet(id)
|
||||
);
|
||||
|
||||
CREATE TABLE products (
|
||||
product_no INTEGER,
|
||||
name TEXT,
|
||||
price NUMERIC CHECK (price > 0)
|
||||
);
|
||||
|
|
76
tests/postgres/error.rs
Normal file
76
tests/postgres/error.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use sqlx::{error::ErrorKind, postgres::Postgres, Connection};
|
||||
use sqlx_test::new;
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_unique_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Postgres>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
sqlx::query("INSERT INTO tweet(id, text, owner_id) VALUES (1, 'Foo', 1);")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet VALUES (1, NOW(), 'Foo', 1);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::UniqueViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_foreign_key_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Postgres>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO tweet_reply (tweet_id, text) VALUES (1, 'Reply!');")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::ForeignKeyViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_not_null_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Postgres>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet (text) VALUES (null);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::NotNullViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_check_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Postgres>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO products VALUES (1, 'Product 1', 0);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::CheckViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -21,6 +21,15 @@ CREATE TABLE tweet
|
|||
owner_id BIGINT
|
||||
);
|
||||
|
||||
CREATE TABLE tweet_reply
|
||||
(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tweet_id BIGINT NOT NULL REFERENCES tweet(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
text TEXT NOT NULL,
|
||||
owner_id BIGINT
|
||||
);
|
||||
|
||||
CREATE TYPE float_range AS RANGE
|
||||
(
|
||||
subtype = float8,
|
||||
|
|
72
tests/sqlite/error.rs
Normal file
72
tests/sqlite/error.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use sqlx::{error::ErrorKind, query, sqlite::Sqlite, Connection, Executor, Transaction};
|
||||
use sqlx_test::new;
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_unique_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Sqlite>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet VALUES (1, 'Foo', true, 1);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::UniqueViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_foreign_key_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Sqlite>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO tweet_reply (id, tweet_id, text) VALUES (2, 2, 'Reply!');")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::ForeignKeyViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_not_null_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Sqlite>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> = sqlx::query("INSERT INTO tweet (text) VALUES (null);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::NotNullViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx_macros::test]
|
||||
async fn it_fails_with_check_violation() -> anyhow::Result<()> {
|
||||
let mut conn = new::<Sqlite>().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
let res: Result<_, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO products VALUES (1, 'Product 1', 0);")
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let err = res.unwrap_err();
|
||||
|
||||
let err = err.into_database_error().unwrap();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::CheckViolation);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -8,6 +8,16 @@ CREATE TABLE tweet (
|
|||
INSERT INTO tweet(id, text, owner_id)
|
||||
VALUES (1, '#sqlx is pretty cool!', 1);
|
||||
--
|
||||
CREATE TABLE tweet_reply (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
tweet_id BIGINT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
owner_id BIGINT,
|
||||
CONSTRAINT tweet_id_fk FOREIGN KEY (tweet_id) REFERENCES tweet(id)
|
||||
);
|
||||
INSERT INTO tweet_reply(id, tweet_id, text, owner_id)
|
||||
VALUES (1, 1, 'Yeah! #sqlx is indeed pretty cool!', 1);
|
||||
--
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
|
@ -18,3 +28,10 @@ VALUES (1, 'Herp Derpinson', 1);
|
|||
CREATE VIEW accounts_view as
|
||||
SELECT *
|
||||
FROM accounts;
|
||||
--
|
||||
CREATE TABLE products (
|
||||
product_no INTEGER,
|
||||
name TEXT,
|
||||
price NUMERIC,
|
||||
CONSTRAINT price_greater_than_zero CHECK (price > 0)
|
||||
);
|
||||
|
|
Binary file not shown.
38
tests/x.py
38
tests/x.py
|
@ -127,10 +127,10 @@ for path in glob(os.path.join(os.path.dirname(__file__), "target/**/*.gc*"), rec
|
|||
# check
|
||||
#
|
||||
|
||||
for runtime in ["async-std", "tokio", "actix"]:
|
||||
for tls in ["native-tls", "rustls"]:
|
||||
for runtime in ["async-std", "tokio"]:
|
||||
for tls in ["native-tls", "rustls", "none"]:
|
||||
run(
|
||||
f"cargo c --no-default-features --features all-databases,all-types,offline,macros,runtime-{runtime}-{tls}",
|
||||
f"cargo c --no-default-features --features all-databases,_unstable-all-types,macros,runtime-{runtime},tls-{tls}",
|
||||
comment="check with async-std",
|
||||
tag=f"check_{runtime}_{tls}"
|
||||
)
|
||||
|
@ -139,10 +139,10 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
# unit test
|
||||
#
|
||||
|
||||
for runtime in ["async-std", "tokio", "actix"]:
|
||||
for tls in ["native-tls", "rustls"]:
|
||||
for runtime in ["async-std", "tokio"]:
|
||||
for tls in ["native-tls", "rustls", "none"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml --features all-databases,all-types,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml --features json,offline,migrate,_rt-{runtime},_tls-{tls}",
|
||||
comment="unit test core",
|
||||
tag=f"unit_{runtime}_{tls}"
|
||||
)
|
||||
|
@ -151,15 +151,15 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
# integration tests
|
||||
#
|
||||
|
||||
for runtime in ["async-std", "tokio", "actix"]:
|
||||
for tls in ["native-tls", "rustls"]:
|
||||
for runtime in ["async-std", "tokio"]:
|
||||
for tls in ["native-tls", "rustls", "none"]:
|
||||
|
||||
#
|
||||
# sqlite
|
||||
#
|
||||
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,sqlite,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --features macros,any,_unstable-all-types,sqlite,runtime-{runtime},tls-{tls}",
|
||||
comment=f"test sqlite",
|
||||
service="sqlite",
|
||||
tag=f"sqlite" if runtime == "async-std" else f"sqlite_{runtime}",
|
||||
|
@ -171,7 +171,7 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
|
||||
for version in ["14", "13", "12", "11", "10"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,postgres,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --features macros,any,unstable-all-types,postgres,runtime-{runtime},tls-{tls}",
|
||||
comment=f"test postgres {version}",
|
||||
service=f"postgres_{version}",
|
||||
tag=f"postgres_{version}" if runtime == "async-std" else f"postgres_{version}_{runtime}",
|
||||
|
@ -180,7 +180,7 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
## +ssl
|
||||
for version in ["14", "13", "12", "11", "10"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,postgres,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --features macros,any,_unstable-all-types,postgres,runtime-{runtime},tls-{tls}",
|
||||
comment=f"test postgres {version} ssl",
|
||||
database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt",
|
||||
service=f"postgres_{version}",
|
||||
|
@ -193,7 +193,7 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
|
||||
for version in ["8", "5_7"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,mysql,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --features macros,any,_unstable-all-types,mysql,runtime-{runtime},tls-{tls}",
|
||||
comment=f"test mysql {version}",
|
||||
service=f"mysql_{version}",
|
||||
tag=f"mysql_{version}" if runtime == "async-std" else f"mysql_{version}_{runtime}",
|
||||
|
@ -205,23 +205,11 @@ for runtime in ["async-std", "tokio", "actix"]:
|
|||
|
||||
for version in ["10_6", "10_5", "10_4", "10_3", "10_2"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,mysql,runtime-{runtime}-{tls}",
|
||||
f"cargo test --no-default-features --features macros,any,_unstable-all-types,mysql,runtime-{runtime},tls-{tls}",
|
||||
comment=f"test mariadb {version}",
|
||||
service=f"mariadb_{version}",
|
||||
tag=f"mariadb_{version}" if runtime == "async-std" else f"mariadb_{version}_{runtime}",
|
||||
)
|
||||
|
||||
#
|
||||
# mssql
|
||||
#
|
||||
|
||||
for version in ["2019", "2017"]:
|
||||
run(
|
||||
f"cargo test --no-default-features --features macros,offline,any,all-types,mssql,runtime-{runtime}-{tls}",
|
||||
comment=f"test mssql {version}",
|
||||
service=f"mssql_{version}",
|
||||
tag=f"mssql_{version}" if runtime == "async-std" else f"mssql_{version}_{runtime}",
|
||||
)
|
||||
|
||||
# TODO: Use [grcov] if available
|
||||
# ~/.cargo/bin/grcov tests/.cache/target/debug -s sqlx-core/ -t html --llvm --branch -o ./target/debug/coverage
|
||||
|
|
Loading…
Reference in a new issue