Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ Cargo.lock
.vscode
.idea/*
*/.idea/*
.zed/
*/.zed/*
.env.local
.DS_Store
.DS_Store
34 changes: 34 additions & 0 deletions sea-orm-macros/src/derives/active_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ impl DeriveActiveModel {
fn impl_active_model(&self) -> TokenStream {
let mut ts = self.impl_active_model_convert();
ts.extend(self.impl_active_model_trait());
ts.extend(self.impl_insert_active_model_trait());
ts
}

Expand Down Expand Up @@ -124,6 +125,39 @@ impl DeriveActiveModel {
)
}

fn impl_insert_active_model_trait(&self) -> TokenStream {
let fields = &self.fields;
let names = &self.names;

quote! {
#[automatically_derived]
impl sea_orm::InsertActiveModelTrait for ActiveModel {
type Entity = Entity;

fn is_to_safe_insert(&self) -> bool {
// Check each field, with short circuit
#(
{
let field_is_set = !matches!(self.#fields, sea_orm::ActiveValue::NotSet);
let col = <Self::Entity as sea_orm::EntityTrait>::Column::#names.def();
let field_is_nullable = col.is_null();
//FIXME: current default implementation doesn't seem to reflect the schema and seems to be a separate layer (I'm not sure).
// Also, this doesn't work with id columns
let field_has_default = col.get_column_default().is_some();

// TODO: Invert conditions
if !field_is_set && !field_is_nullable && !field_has_default {
return false;
}
}
)*
// Finnaly true
true
}
}
}
}

fn impl_active_model_trait(&self) -> TokenStream {
let fields = &self.fields;
let methods = self.impl_active_model_trait_methods();
Expand Down
194 changes: 194 additions & 0 deletions src/entity/active_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ use crate::{
use sea_query::ValueTuple;
use std::fmt::Debug;

pub trait InsertActiveModelTrait: Clone + Debug {
type Entity: EntityTrait;
/// Checks if all required fields are set
/// Set, nullable or fields with defaults pass
fn is_to_safe_insert(&self) -> bool;
}

/// `ActiveModel` is a type for constructing `INSERT` and `UPDATE` statements for a particular table.
///
/// Like [Model][ModelTrait], it represents a database record and each field represents a column.
Expand Down Expand Up @@ -2241,6 +2248,193 @@ mod tests {
Ok(())
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_all_set() {
// All fields explicitly Set -> always safe
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub description: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

let am = my_entity::ActiveModel {
id: Set(1),
name: Set("hello".to_owned()),
description: Set(None),
};
assert!(am.is_to_safe_insert());
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_required_field_not_set() {
// A non-nullable, non-defaulted field is NotSet -> not safe
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

// `name` is required (non-nullable, no default) and not set
let am = my_entity::ActiveModel {
id: Set(1),
name: NotSet,
};
assert!(!am.is_to_safe_insert());
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_nullable_field_not_set() {
// A nullable (Option<T>) field that is NotSet is still safe, the DB will store NULL
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub description: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

// `description` is nullable -> NotSet is fine
let am = my_entity::ActiveModel {
id: Set(1),
name: Set("hello".to_owned()),
description: NotSet,
};
assert!(am.is_to_safe_insert());
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_auto_increment_pk_not_set() {
// The primary key has auto_increment=true -> NotSet is fine
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

// `id` is auto-increment -> NotSet is acceptable
let am = my_entity::ActiveModel {
id: NotSet,
name: Set("hello".to_owned()),
};
assert!(am.is_to_safe_insert());
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_field_with_default_not_set() {
// A non-nullable field that has a schema-level default -> NotSet is safe
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(default_value = 0)]
pub score: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

// `score` has a default value -> NotSet is fine
let am = my_entity::ActiveModel {
id: Set(1),
name: Set("hello".to_owned()),
score: NotSet,
};
assert!(am.is_to_safe_insert());
}

#[test]
#[cfg(feature = "macros")]
fn test_is_to_safe_insert_multiple_missing_required_fields() {
// Several required fields are NotSet -> not safe
mod my_entity {
use crate as sea_orm;
use crate::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_entity")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub first_name: String,
pub last_name: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

let am = my_entity::ActiveModel {
id: Set(1),
first_name: NotSet,
last_name: NotSet,
};
assert!(!am.is_to_safe_insert());
}

#[smol_potat::test]
#[cfg(feature = "with-json")]
async fn test_active_model_set_from_json_3() -> Result<(), DbErr> {
Expand Down
Loading