Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Returning #292

Merged
merged 33 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c5468eb
Use "marlon-sousa/sea-query"
billy1624 Nov 5, 2021
c39a3b8
Insert with returning for Postgres
billy1624 Nov 5, 2021
52ff943
Docs
billy1624 Nov 5, 2021
a977572
Update with returning for Postgres
billy1624 Nov 5, 2021
50605c7
FIXME: breaking behaviors
billy1624 Nov 5, 2021
6238736
Handle "None of the database rows are affected" for Postgres
billy1624 Nov 8, 2021
2f7cffa
Fix test cases
billy1624 Nov 8, 2021
732d080
Update docs
billy1624 Nov 8, 2021
0eafacc
Try returning on MariaDB
billy1624 Nov 8, 2021
80c0d69
Merge remote-tracking branch 'origin/master' into returning
billy1624 Nov 8, 2021
30f43b6
Fixup
billy1624 Nov 8, 2021
2f0ac4c
Fixup
billy1624 Nov 8, 2021
afdb1af
This will fail loll
billy1624 Nov 8, 2021
1723206
This will fail loll
billy1624 Nov 8, 2021
3e6423a
This will fail loll
billy1624 Nov 8, 2021
30a50ca
Try
billy1624 Nov 9, 2021
429b920
Fixup
billy1624 Nov 9, 2021
24fab66
Try
billy1624 Nov 9, 2021
8020ae1
Fixup
billy1624 Nov 9, 2021
533c3cf
Try
billy1624 Nov 9, 2021
ec637b2
Returning support for SQLite
billy1624 Nov 9, 2021
c1fae1b
Debug print
billy1624 Nov 9, 2021
cc035d7
Refactoring
billy1624 Nov 9, 2021
66c23c8
Revert MySQL & SQLite returning support
billy1624 Nov 10, 2021
257a893
Use `sea-query` master
billy1624 Nov 10, 2021
4d44827
Docs
billy1624 Nov 11, 2021
fd50ffd
Merge remote-tracking branch 'origin/master' into returning
billy1624 Nov 16, 2021
d5de8b1
Should fail
billy1624 Nov 16, 2021
9655805
Will fail, as expected
billy1624 Nov 16, 2021
4c147a2
Rewrite doctests
billy1624 Nov 16, 2021
f9d04fc
Hotfix - separate counter for mock exec & query
billy1624 Nov 16, 2021
7298fde
Rewrite doctests
billy1624 Nov 16, 2021
42404eb
Fixup
billy1624 Nov 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ jobs:
name: Examples
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
path: [basic, actix_example, actix4_example, axum_example, rocket_example]
Expand All @@ -312,6 +313,7 @@ jobs:
if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-issues == 'true') }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
path: [86, 249, 262]
Expand Down Expand Up @@ -350,6 +352,7 @@ jobs:
env:
DATABASE_URL: "sqlite::memory:"
strategy:
fail-fast: false
matrix:
runtime: [async-std, actix, tokio]
tls: [native-tls, rustls]
Expand Down Expand Up @@ -392,6 +395,7 @@ jobs:
env:
DATABASE_URL: "mysql://root:@localhost"
strategy:
fail-fast: false
matrix:
version: [8.0, 5.7]
runtime: [async-std, actix, tokio]
Expand Down Expand Up @@ -452,8 +456,9 @@ jobs:
env:
DATABASE_URL: "mysql://root:@localhost"
strategy:
fail-fast: false
matrix:
version: [10.6]
version: [10.6, 10.5, 10.4]
runtime: [async-std, actix, tokio]
tls: [native-tls]
services:
Expand Down Expand Up @@ -512,8 +517,9 @@ jobs:
env:
DATABASE_URL: "postgres://root:root@localhost"
strategy:
fail-fast: false
matrix:
version: [13.3, 12.7, 11.12, 10.17, 9.6.22]
version: [13, 12, 11, 10, 9]
runtime: [tokio]
tls: [native-tls]
services:
Expand Down
6 changes: 6 additions & 0 deletions src/database/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ pub trait ConnectionTrait<'a>: Sync {
T: Send,
E: std::error::Error + Send;

/// Check if the connection supports `RETURNING` syntax on insert and update
fn support_returning(&self) -> bool {
let db_backend = self.get_database_backend();
db_backend.support_returning()
}

/// Check if the connection is a test connection for the Mock database
fn is_mock_connection(&self) -> bool {
false
Expand Down
5 changes: 5 additions & 0 deletions src/database/db_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ impl DbBackend {
Self::Sqlite => Box::new(SqliteQueryBuilder),
}
}

/// Check if the database supports `RETURNING` syntax on insert and update
pub fn support_returning(&self) -> bool {
matches!(self, Self::Postgres)
tyt2y3 marked this conversation as resolved.
Show resolved Hide resolved
}
}

#[cfg(test)]
Expand Down
10 changes: 3 additions & 7 deletions src/entity/active_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,16 @@ pub trait ActiveModelTrait: Clone + Debug {
C: ConnectionTrait<'a>,
{
let am = ActiveModelBehavior::before_save(self, true)?;
let res = <Self::Entity as EntityTrait>::insert(am).exec(db).await?;
let found = <Self::Entity as EntityTrait>::find_by_id(res.last_insert_id)
.one(db)
let am = <Self::Entity as EntityTrait>::insert(am)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SO we need new test cases here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases added

.exec_with_returning(db)
.await?;
let am = match found {
Some(model) => model.into_active_model(),
None => return Err(DbErr::Exec("Failed to find inserted item".to_owned())),
};
ActiveModelBehavior::after_save(am, true)
}

/// Perform the `UPDATE` operation on an ActiveModel
async fn update<'a, C>(self, db: &'a C) -> Result<Self, DbErr>
where
<Self::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
Self: ActiveModelBehavior + 'a,
C: ConnectionTrait<'a>,
{
Expand Down
2 changes: 1 addition & 1 deletion src/entity/base_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2 AND "fruit"."name" LIKE $3"#,
/// DbBackend::Postgres, r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2 AND "fruit"."name" LIKE $3 RETURNING "id", "name", "cake_id""#,
tyt2y3 marked this conversation as resolved.
Show resolved Hide resolved
/// vec!["Orange".into(), 1i32.into(), "%orange%".into()]
/// )]);
/// ```
Expand Down
100 changes: 84 additions & 16 deletions src/executor/insert.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{
error::*, ActiveModelTrait, ConnectionTrait, DbBackend, EntityTrait, Insert, PrimaryKeyTrait,
Statement, TryFromU64,
error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, Insert, IntoActiveModel,
Iterable, PrimaryKeyTrait, SelectModel, SelectorRaw, Statement, TryFromU64,
};
use sea_query::{
Alias, Expr, FromValueTuple, Iden, InsertStatement, IntoColumnRef, Query, ValueTuple,
};
use sea_query::{FromValueTuple, InsertStatement, ValueTuple};
use std::{future::Future, marker::PhantomData};

/// Defines a structure to perform INSERT operations in an ActiveModel
Expand Down Expand Up @@ -39,18 +41,28 @@ where
{
// so that self is dropped before entering await
let mut query = self.query;
if db.get_database_backend() == DbBackend::Postgres {
use crate::{sea_query::Query, Iterable};
if <A::Entity as EntityTrait>::PrimaryKey::iter().count() > 0 {
query.returning(
Query::select()
.columns(<A::Entity as EntityTrait>::PrimaryKey::iter())
.take(),
);
}
if db.support_returning() && <A::Entity as EntityTrait>::PrimaryKey::iter().count() > 0 {
let mut returning = Query::select();
returning.columns(
<A::Entity as EntityTrait>::PrimaryKey::iter().map(|c| c.into_column_ref()),
);
query.returning(returning);
}
Inserter::<A>::new(self.primary_key, query).exec(db)
}

/// Execute an insert operation and return the inserted model (use `RETURNING` syntax if database supported)
pub fn exec_with_returning<'a, C>(
self,
db: &'a C,
) -> impl Future<Output = Result<A, DbErr>> + '_
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
C: ConnectionTrait<'a>,
A: 'a,
{
Inserter::<A>::new(self.primary_key, self.query).exec_with_returning(db)
}
}

impl<A> Inserter<A>
Expand All @@ -75,6 +87,19 @@ where
let builder = db.get_database_backend();
exec_insert(self.primary_key, builder.build(&self.query), db)
}

/// Execute an insert operation and return the inserted model (use `RETURNING` syntax if database supported)
pub fn exec_with_returning<'a, C>(
self,
db: &'a C,
) -> impl Future<Output = Result<A, DbErr>> + '_
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
C: ConnectionTrait<'a>,
A: 'a,
{
exec_insert_with_returning(self.primary_key, self.query, db)
}
}

async fn exec_insert<'a, A, C>(
Expand All @@ -88,16 +113,15 @@ where
{
type PrimaryKey<A> = <<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey;
type ValueTypeOf<A> = <PrimaryKey<A> as PrimaryKeyTrait>::ValueType;
let last_insert_id_opt = match db.get_database_backend() {
DbBackend::Postgres => {
use crate::{sea_query::Iden, Iterable};
let last_insert_id_opt = match db.support_returning() {
true => {
let cols = PrimaryKey::<A>::iter()
.map(|col| col.to_string())
.collect::<Vec<_>>();
let res = db.query_one(statement).await?.unwrap();
res.try_get_many("", cols.as_ref()).ok()
}
_ => {
false => {
let last_insert_id = db.execute(statement).await?.last_insert_id();
ValueTypeOf::<A>::try_from_u64(last_insert_id).ok()
}
Expand All @@ -111,3 +135,47 @@ where
};
Ok(InsertResult { last_insert_id })
}

async fn exec_insert_with_returning<'a, A, C>(
primary_key: Option<ValueTuple>,
mut insert_statement: InsertStatement,
db: &'a C,
) -> Result<A, DbErr>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
C: ConnectionTrait<'a>,
A: ActiveModelTrait,
{
let db_backend = db.get_database_backend();
let found = match db.support_returning() {
true => {
let mut returning = Query::select();
returning.exprs(<A::Entity as EntityTrait>::Column::iter().map(|c| {
let col = Expr::col(c);
let col_def = ColumnTrait::def(&c);
let col_type = col_def.get_column_type();
match col_type.get_enum_name() {
Some(_) => col.as_enum(Alias::new("text")),
None => col.into(),
}
}));
insert_statement.returning(returning);
SelectorRaw::<SelectModel<<A::Entity as EntityTrait>::Model>>::from_statement(
db_backend.build(&insert_statement),
)
.one(db)
.await?
}
false => {
let insert_res =
exec_insert::<A, _>(primary_key, db_backend.build(&insert_statement), db).await?;
<A::Entity as EntityTrait>::find_by_id(insert_res.last_insert_id)
.one(db)
.await?
}
};
match found {
Some(model) => Ok(model.into_active_model()),
None => Err(DbErr::Exec("Failed to find inserted item".to_owned())),
}
}
77 changes: 66 additions & 11 deletions src/executor/update.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{
error::*, ActiveModelTrait, ConnectionTrait, EntityTrait, Statement, UpdateMany, UpdateOne,
error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, IntoActiveModel,
Iterable, SelectModel, SelectorRaw, Statement, UpdateMany, UpdateOne,
};
use sea_query::UpdateStatement;
use sea_query::{Alias, Expr, FromValueTuple, Query, UpdateStatement};
use std::future::Future;

/// Defines an update operation
Expand All @@ -25,10 +26,11 @@ where
/// Execute an update operation on an ActiveModel
pub async fn exec<'b, C>(self, db: &'b C) -> Result<A, DbErr>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
C: ConnectionTrait<'b>,
{
// so that self is dropped before entering await
exec_update_and_return_original(self.query, self.model, db).await
exec_update_and_return_updated(self.query, self.model, db).await
}
}

Expand Down Expand Up @@ -78,17 +80,61 @@ where
Updater::new(query).exec(db).await
}

async fn exec_update_and_return_original<'a, A, C>(
query: UpdateStatement,
async fn exec_update_and_return_updated<'a, A, C>(
mut query: UpdateStatement,
model: A,
db: &'a C,
) -> Result<A, DbErr>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait,
C: ConnectionTrait<'a>,
{
Updater::new(query).check_record_exists().exec(db).await?;
billy1624 marked this conversation as resolved.
Show resolved Hide resolved
Ok(model)
match db.support_returning() {
true => {
let mut returning = Query::select();
returning.exprs(<A::Entity as EntityTrait>::Column::iter().map(|c| {
let col = Expr::col(c);
let col_def = c.def();
let col_type = col_def.get_column_type();
match col_type.get_enum_name() {
Some(_) => col.as_enum(Alias::new("text")),
None => col.into(),
}
}));
query.returning(returning);
let db_backend = db.get_database_backend();
let found: Option<<A::Entity as EntityTrait>::Model> =
SelectorRaw::<SelectModel<<A::Entity as EntityTrait>::Model>>::from_statement(
db_backend.build(&query),
)
.one(db)
.await?;
// If we got `None` then we are updating a row that does not exist.
match found {
Some(model) => Ok(model.into_active_model()),
None => Err(DbErr::RecordNotFound(
"None of the database rows are affected".to_owned(),
)),
}
}
tyt2y3 marked this conversation as resolved.
Show resolved Hide resolved
false => {
// If we updating a row that does not exist then an error will be thrown here.
Updater::new(query).check_record_exists().exec(db).await?;
let primary_key_value = match model.get_primary_key_value() {
Some(val) => FromValueTuple::from_value_tuple(val),
None => return Err(DbErr::Exec("Fail to get primary key from model".to_owned())),
};
let found = <A::Entity as EntityTrait>::find_by_id(primary_key_value)
.one(db)
.await?;
// If we cannot select the updated row from db by the cached primary key
match found {
Some(model) => Ok(model.into_active_model()),
None => Err(DbErr::Exec("Failed to find inserted item".to_owned())),
}
}
}
}

async fn exec_update<'a, C>(
Expand Down Expand Up @@ -119,6 +165,15 @@ mod tests {
#[smol_potat::test]
async fn update_record_not_found_1() -> Result<(), DbErr> {
let db = MockDatabase::new(DbBackend::Postgres)
.append_query_results(vec![
vec![cake::Model {
id: 1,
name: "Cheese Cake".to_owned(),
}],
vec![],
vec![],
vec![],
])
.append_exec_results(vec![
MockExecResult {
last_insert_id: 0,
Expand Down Expand Up @@ -217,22 +272,22 @@ mod tests {
vec![
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#,
vec!["Cheese Cake".into(), 1i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
Expand Down
Loading