Mock any database

This commit is contained in:
Billy Chan 2021-07-14 17:32:52 +08:00 committed by Chris Tsang
parent b4abf15169
commit d59ee1e298
9 changed files with 128 additions and 55 deletions

View File

@ -2,7 +2,7 @@ mod entity;
use entity::*;
use sea_orm::{entity::*, error::*, MockDatabase, MockExecResult, Transaction};
use sea_orm::{entity::*, error::*, MockDatabase, MockExecResult, Syntax, Transaction};
#[async_std::test]
async fn test_insert() -> Result<(), DbErr> {
@ -11,7 +11,7 @@ async fn test_insert() -> Result<(), DbErr> {
rows_affected: 1,
};
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_exec_results(vec![exec_result.clone()])
.into_connection();
@ -27,6 +27,7 @@ async fn test_insert() -> Result<(), DbErr> {
assert_eq!(
db.into_transaction_log(),
vec![Transaction::from_sql_and_values(
Syntax::Postgres,
r#"INSERT INTO "cake" ("name") VALUES ($1)"#,
vec!["Apple Pie".into()]
)]
@ -42,7 +43,7 @@ async fn test_select() -> Result<(), DbErr> {
filling_id: 3,
}];
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_query_results(vec![query_results.clone()])
.into_connection();
@ -52,7 +53,9 @@ async fn test_select() -> Result<(), DbErr> {
assert_eq!(
db.into_transaction_log(),
vec![Transaction::from_sql_and_values([
vec![Transaction::from_sql_and_values(
Syntax::Postgres,
[
r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#,
r#"WHERE "cake_filling"."cake_id" = $1 AND "cake_filling"."filling_id" = $2"#,
].join(" ").as_str(),
@ -70,7 +73,7 @@ async fn test_update() -> Result<(), DbErr> {
rows_affected: 1,
};
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_exec_results(vec![exec_result.clone()])
.into_connection();
@ -87,6 +90,7 @@ async fn test_update() -> Result<(), DbErr> {
assert_eq!(
db.into_transaction_log(),
vec![Transaction::from_sql_and_values(
Syntax::Postgres,
r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2"#,
vec!["Orange".into(), 1i32.into()]
)]
@ -102,7 +106,7 @@ async fn test_delete() -> Result<(), DbErr> {
rows_affected: 1,
};
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_exec_results(vec![exec_result.clone()])
.into_connection();
@ -118,6 +122,7 @@ async fn test_delete() -> Result<(), DbErr> {
assert_eq!(
db.into_transaction_log(),
vec![Transaction::from_sql_and_values(
Syntax::Postgres,
r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#,
vec![3i32.into()]
)]

View File

@ -60,7 +60,7 @@ impl DatabaseConnection {
#[cfg(feature = "sqlx-sqlite")]
DatabaseConnection::SqlxSqlitePoolConnection(_) => QueryBuilderBackend::Sqlite,
#[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(_) => QueryBuilderBackend::Postgres,
DatabaseConnection::MockDatabaseConnection(conn) => conn.get_query_builder_backend(),
DatabaseConnection::Disconnected => panic!("Disconnected"),
}
}
@ -72,7 +72,7 @@ impl DatabaseConnection {
#[cfg(feature = "sqlx-sqlite")]
DatabaseConnection::SqlxSqlitePoolConnection(_) => SchemaBuilderBackend::Sqlite,
#[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(_) => SchemaBuilderBackend::Postgres,
DatabaseConnection::MockDatabaseConnection(conn) => conn.get_schema_builder_backend(),
DatabaseConnection::Disconnected => panic!("Disconnected"),
}
}

View File

@ -1,13 +1,14 @@
use crate::{
error::*, DatabaseConnection, EntityTrait, ExecResult, ExecResultHolder, Iden, Iterable,
MockDatabaseConnection, MockDatabaseTrait, ModelTrait, QueryResult, QueryResultRow, Statement,
Transaction,
Syntax, Transaction,
};
use sea_query::{Value, ValueType};
use std::collections::BTreeMap;
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct MockDatabase {
syntax: Syntax,
transaction_log: Vec<Transaction>,
exec_results: Vec<MockExecResult>,
query_results: Vec<Vec<MockRow>>,
@ -42,8 +43,13 @@ where
}
impl MockDatabase {
pub fn new() -> Self {
Default::default()
pub fn new(syntax: Syntax) -> Self {
Self {
syntax,
transaction_log: Vec::new(),
exec_results: Vec::new(),
query_results: Vec::new(),
}
}
pub fn into_connection(self) -> DatabaseConnection {
@ -96,6 +102,10 @@ impl MockDatabaseTrait for MockDatabase {
fn drain_transaction_log(&mut self) -> Vec<Transaction> {
std::mem::take(&mut self.transaction_log)
}
fn get_syntax(&self) -> Syntax {
self.syntax
}
}
impl MockRow {

View File

@ -1,4 +1,4 @@
use crate::QueryBuilderWithSyntax;
use crate::{QueryBuilderBackend, QueryBuilderWithSyntax, SchemaBuilderBackend};
use sea_query::{
inject_parameters, MysqlQueryBuilder, PostgresQueryBuilder, QueryBuilder, SqliteQueryBuilder,
Values,
@ -63,6 +63,22 @@ impl Syntax {
Self::Sqlite => Box::new(SqliteQueryBuilder),
}
}
pub fn get_query_builder_backend(&self) -> QueryBuilderBackend {
match self {
Self::MySql => QueryBuilderBackend::MySql,
Self::Postgres => QueryBuilderBackend::Postgres,
Self::Sqlite => QueryBuilderBackend::Sqlite,
}
}
pub fn get_schema_builder_backend(&self) -> SchemaBuilderBackend {
match self {
Self::MySql => SchemaBuilderBackend::MySql,
Self::Postgres => SchemaBuilderBackend::Postgres,
Self::Sqlite => SchemaBuilderBackend::Sqlite,
}
}
}
impl QueryBuilderWithSyntax for MysqlQueryBuilder {

View File

@ -7,12 +7,12 @@ pub struct Transaction {
}
impl Transaction {
pub fn from_sql_and_values<I>(sql: &str, values: I) -> Self
pub fn from_sql_and_values<I>(syntax: Syntax, sql: &str, values: I) -> Self
where
I: IntoIterator<Item = Value>,
{
Self::one(Statement::from_string_values_tuple(
Syntax::Postgres,
syntax,
(sql.to_string(), Values(values.into_iter().collect())),
))
}

View File

@ -1,6 +1,6 @@
use crate::{
debug_print, error::*, DatabaseConnection, ExecResult, MockDatabase, QueryResult, Statement,
Transaction,
debug_print, error::*, DatabaseConnection, ExecResult, MockDatabase, QueryBuilderBackend,
QueryResult, SchemaBuilderBackend, Statement, Syntax, Transaction,
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
@ -20,17 +20,41 @@ pub trait MockDatabaseTrait: Send {
fn query(&mut self, counter: usize, stmt: Statement) -> Result<Vec<QueryResult>, DbErr>;
fn drain_transaction_log(&mut self) -> Vec<Transaction>;
fn get_syntax(&self) -> Syntax;
}
impl MockDatabaseConnector {
pub fn accepts(string: &str) -> bool {
string.starts_with("mock://")
#[cfg(feature = "sqlx-mysql")]
if crate::SqlxMySqlConnector::accepts(string) {
return true;
}
#[cfg(feature = "sqlx-sqlite")]
if crate::SqlxSqliteConnector::accepts(string) {
return true;
}
false
}
pub async fn connect(_string: &str) -> Result<DatabaseConnection, DbErr> {
pub async fn connect(string: &str) -> Result<DatabaseConnection, DbErr> {
macro_rules! connect_mock_db {
( $syntax: expr ) => {
Ok(DatabaseConnection::MockDatabaseConnection(
MockDatabaseConnection::new(MockDatabase::new()),
MockDatabaseConnection::new(MockDatabase::new($syntax)),
))
};
}
#[cfg(feature = "sqlx-mysql")]
if crate::SqlxMySqlConnector::accepts(string) {
return connect_mock_db!(Syntax::MySql);
}
#[cfg(feature = "sqlx-sqlite")]
if crate::SqlxSqliteConnector::accepts(string) {
return connect_mock_db!(Syntax::Sqlite);
}
connect_mock_db!(Syntax::Postgres)
}
}
@ -67,4 +91,20 @@ impl MockDatabaseConnection {
let counter = self.counter.fetch_add(1, Ordering::SeqCst);
self.mocker.lock().unwrap().query(counter, statement)
}
pub fn get_query_builder_backend(&self) -> QueryBuilderBackend {
self.mocker
.lock()
.unwrap()
.get_syntax()
.get_query_builder_backend()
}
pub fn get_schema_builder_backend(&self) -> SchemaBuilderBackend {
self.mocker
.lock()
.unwrap()
.get_syntax()
.get_schema_builder_backend()
}
}

View File

@ -70,9 +70,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_query_results(vec![
/// # vec![
/// # cake::Model {
@ -126,10 +126,10 @@ pub trait EntityTrait: EntityName {
/// db.into_transaction_log(),
/// vec![
/// Transaction::from_sql_and_values(
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()]
/// Syntax::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()]
/// ),
/// Transaction::from_sql_and_values(
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
/// Syntax::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
/// ),
/// ]);
/// ```
@ -143,9 +143,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_query_results(vec![
/// # vec![
/// # cake::Model {
@ -174,15 +174,15 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()]
/// Syntax::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()]
/// )]);
/// ```
/// Find by composite key
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_query_results(vec![
/// # vec![
/// # cake_filling::Model {
@ -210,7 +210,9 @@ pub trait EntityTrait: EntityName {
///
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values([
/// vec![Transaction::from_sql_and_values(
/// Syntax::Postgres,
/// [
/// r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#,
/// r#"WHERE "cake_filling"."cake_id" = $1 AND "cake_filling"."filling_id" = $2"#,
/// ].join(" ").as_str(),
@ -243,9 +245,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 15,
@ -274,7 +276,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"INSERT INTO "cake" ("name") VALUES ($1)"#, vec!["Apple Pie".into()]
/// Syntax::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1)"#, vec!["Apple Pie".into()]
/// )]);
/// ```
fn insert<A>(model: A) -> Insert<A>
@ -290,9 +292,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 28,
@ -325,7 +327,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"INSERT INTO "cake" ("name") VALUES ($1), ($2)"#,
/// Syntax::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1), ($2)"#,
/// vec!["Apple Pie".into(), "Orange Scone".into()]
/// )]);
/// ```
@ -345,9 +347,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 0,
@ -377,7 +379,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2"#, vec!["Orange".into(), 1i32.into()]
/// Syntax::Postgres, r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2"#, vec!["Orange".into(), 1i32.into()]
/// )]);
/// ```
fn update<A>(model: A) -> UpdateOne<A>
@ -395,9 +397,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 0,
@ -424,7 +426,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Null, "%Apple%".into()]
/// Syntax::Postgres, r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Null, "%Apple%".into()]
/// )]);
/// ```
fn update_many() -> UpdateMany<Self> {
@ -439,9 +441,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 0,
@ -469,7 +471,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, vec![3i32.into()]
/// Syntax::Postgres, r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, vec![3i32.into()]
/// )]);
/// ```
fn delete<A>(model: A) -> DeleteOne<A>
@ -487,9 +489,9 @@ pub trait EntityTrait: EntityName {
///
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*};
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, Syntax};
/// #
/// # let db = MockDatabase::new()
/// # let db = MockDatabase::new(Syntax::Postgres)
/// # .append_exec_results(vec![
/// # MockExecResult {
/// # last_insert_id: 0,
@ -515,7 +517,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// r#"DELETE FROM "fruit" WHERE "fruit"."name" LIKE $1"#, vec!["%Apple%".into()]
/// Syntax::Postgres, r#"DELETE FROM "fruit" WHERE "fruit"."name" LIKE $1"#, vec!["%Apple%".into()]
/// )]);
/// ```
fn delete_many() -> DeleteMany<Self> {

View File

@ -103,7 +103,7 @@ where
mod tests {
use crate::entity::prelude::*;
use crate::tests_cfg::*;
use crate::{DatabaseConnection, MockDatabase, Transaction};
use crate::{DatabaseConnection, MockDatabase, Syntax, Transaction};
use futures::TryStreamExt;
use sea_query::{Alias, Expr, SelectStatement, Value};
@ -129,7 +129,7 @@ mod tests {
let page3 = Vec::<fruit::Model>::new();
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_query_results(vec![page1.clone(), page2.clone(), page3.clone()])
.into_connection();
@ -138,7 +138,7 @@ mod tests {
fn setup_num_rows() -> (DatabaseConnection, i32) {
let num_rows = 3;
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_query_results(vec![vec![maplit::btreemap! {
"num_rows" => Into::<Value>::into(num_rows),
}]])

View File

@ -102,12 +102,12 @@ impl FromQueryResult for JsonValue {
#[cfg(feature = "mock")]
mod tests {
use crate::tests_cfg::cake;
use crate::{entity::*, MockDatabase};
use crate::{entity::*, MockDatabase, Syntax};
use sea_query::Value;
#[async_std::test]
async fn to_json_1() {
let db = MockDatabase::new()
let db = MockDatabase::new(Syntax::Postgres)
.append_query_results(vec![vec![maplit::btreemap! {
"id" => Into::<Value>::into(128), "name" => Into::<Value>::into("apple")
}]])