From c837e596e83bfa938de7675e08db2f90d28ade5e Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Sat, 4 Jul 2026 13:59:46 +0700 Subject: [PATCH] #180 fix: exclude auto fields from INSERT so database defaults apply --- .../src/entity/insertable.rs | 2 +- .../src/entity/sql/postgres/bulk.rs | 6 +-- .../src/entity/sql/postgres/context.rs | 19 +++++-- .../src/entity/sql/postgres/crud.rs | 54 +++++++++++++++++-- .../src/entity/sql/postgres/helpers.rs | 2 +- .../src/entity/sql/postgres/save.rs | 4 +- .../src/entity/sql/postgres/upsert.rs | 4 +- .../src/entity/transaction.rs | 3 +- crates/entity-derive-impl/src/utils/fields.rs | 4 +- 9 files changed, 78 insertions(+), 20 deletions(-) diff --git a/crates/entity-derive-impl/src/entity/insertable.rs b/crates/entity-derive-impl/src/entity/insertable.rs index 2909ed9..10e12ac 100644 --- a/crates/entity-derive-impl/src/entity/insertable.rs +++ b/crates/entity-derive-impl/src/entity/insertable.rs @@ -76,7 +76,7 @@ pub fn generate(entity: &EntityDef) -> TokenStream { let field_defs = entity .all_fields() .iter() - .filter(|f| f.embed.is_none()) + .filter(|f| f.embed.is_none() && !f.storage.is_auto) .map(|f| { let name = f.name(); let ty = f.ty(); diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/bulk.rs b/crates/entity-derive-impl/src/entity/sql/postgres/bulk.rs index c8b0e08..58ba589 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/bulk.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/bulk.rs @@ -69,7 +69,7 @@ impl Context<'_> { insertable_name, create_dto, table, - columns_str, + insert_columns_str, placeholders_str, entity, returning, @@ -85,7 +85,7 @@ impl Context<'_> { let insert_row = if matches!(returning, ReturningMode::Full) { quote! { let row: #row_name = sqlx::query_as( - concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") + concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") ) #(#bindings)* .fetch_one(&mut *tx).await #constraint_map_err?; @@ -93,7 +93,7 @@ impl Context<'_> { } } else { quote! { - sqlx::query(concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ")")) + sqlx::query(concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ")")) #(#bindings)* .execute(&mut *tx).await #constraint_map_err?; } diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/context.rs b/crates/entity-derive-impl/src/entity/sql/postgres/context.rs index 8252ee1..d5d8ae0 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/context.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/context.rs @@ -64,10 +64,17 @@ pub struct Context<'a> { /// Primary key field type. pub id_type: &'a syn::Type, - /// Comma-separated column names for SELECT/INSERT. + /// Comma-separated column names for SELECT (all columns). pub columns_str: String, - /// Comma-separated placeholders for INSERT ($1, $2, ...). + /// Comma-separated column names for INSERT. + /// + /// Excludes `#[auto]` fields so database defaults + /// (`DEFAULT now()`, sequences, triggers) apply; persisted values + /// come back via RETURNING. + pub insert_columns_str: String, + + /// Comma-separated placeholders matching [`Self::insert_columns_str`]. pub placeholders_str: String, /// Whether soft delete is enabled. @@ -88,6 +95,11 @@ impl<'a> Context<'a> { pub fn new(entity: &'a EntityDef) -> Self { let id_field = entity.id_field(); let fields = entity.column_fields(); + let insert_fields: Vec<&crate::entity::parse::FieldDef> = fields + .iter() + .copied() + .filter(|f| !f.storage.is_auto) + .collect(); let dialect = entity.dialect; Self { @@ -103,7 +115,8 @@ impl<'a> Context<'a> { id_name: id_field.name(), id_type: id_field.ty(), columns_str: join_columns(&fields), - placeholders_str: dialect.placeholders(fields.len()), + insert_columns_str: join_columns(&insert_fields), + placeholders_str: dialect.placeholders(insert_fields.len()), soft_delete: entity.is_soft_delete(), returning: entity.returning.clone(), streams: entity.has_streams(), diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/crud.rs b/crates/entity-derive-impl/src/entity/sql/postgres/crud.rs index 1e35db4..d9895f4 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/crud.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/crud.rs @@ -144,7 +144,7 @@ impl Context<'_> { insertable_name, create_dto, table, - columns_str, + insert_columns_str, placeholders_str, entity, returning, @@ -173,7 +173,7 @@ impl Context<'_> { let entity = #entity_name::from(dto); let insertable = #insertable_name::from(&entity); let row: #row_name = sqlx::query_as( - concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") + concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") ) #(#bindings)* .fetch_one(#executor).await #constraint_map_err?; @@ -193,7 +193,7 @@ impl Context<'_> { #tx_open let entity = #entity_name::from(dto); let insertable = #insertable_name::from(&entity); - sqlx::query(concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ") RETURNING ", stringify!(#id_name))) + sqlx::query(concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ") RETURNING ", stringify!(#id_name))) #(#bindings)* .execute(#executor).await #constraint_map_err?; #outbox_created @@ -210,7 +210,7 @@ impl Context<'_> { #tx_open let entity = #entity_name::from(dto); let insertable = #insertable_name::from(&entity); - sqlx::query(concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ")")) + sqlx::query(concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ")")) #(#bindings)* .execute(#executor).await #constraint_map_err?; #outbox_created @@ -228,7 +228,7 @@ impl Context<'_> { #tx_open let entity = #entity_name::from(dto); let insertable = #insertable_name::from(&entity); - sqlx::query(::sqlx::AssertSqlSafe(format!("INSERT INTO {} ({}) VALUES ({}) RETURNING {}", #table, #columns_str, #placeholders_str, #returning_cols))) + sqlx::query(::sqlx::AssertSqlSafe(format!("INSERT INTO {} ({}) VALUES ({}) RETURNING {}", #table, #insert_columns_str, #placeholders_str, #returning_cols))) #(#bindings)* .execute(#executor).await #constraint_map_err?; #outbox_created @@ -648,3 +648,47 @@ mod version_tests { assert!(!code.contains("expected_version")); } } + +#[cfg(test)] +mod auto_fields_tests { + use quote::quote; + use syn::DeriveInput; + + use super::super::context::Context; + use crate::entity::parse::EntityDef; + + fn timestamped_entity() -> EntityDef { + let input: DeriveInput = syn::parse_quote! { + #[entity(table = "posts")] + pub struct Post { + #[id] + pub id: uuid::Uuid, + #[field(create, update, response)] + pub title: String, + #[field(response)] + #[auto] + pub created_at: chrono::DateTime, + } + }; + EntityDef::from_derive_input(&input).unwrap() + } + + #[test] + fn insert_columns_exclude_auto_fields() { + let entity = timestamped_entity(); + let ctx = Context::new(&entity); + assert_eq!(ctx.insert_columns_str, "id, title"); + assert_eq!(ctx.placeholders_str, "$1, $2"); + assert_eq!(ctx.columns_str, "id, title, created_at"); + } + + #[test] + fn create_inserts_without_auto_columns() { + let entity = timestamped_entity(); + let ctx = Context::new(&entity); + let code = ctx.create_method().to_string(); + assert!(code.contains("\"id, title\"")); + assert!(!code.contains("created_at")); + let _ = quote!(); + } +} diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs index 89423da..2eaa570 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs @@ -45,7 +45,7 @@ pub fn join_columns(fields: &[&FieldDef]) -> String { pub fn insert_bindings(fields: &[FieldDef]) -> Vec { fields .iter() - .filter(|f| f.embed.is_none()) + .filter(|f| f.embed.is_none() && !f.storage.is_auto) .map(|f| { let name = f.name(); quote! { .bind(insertable.#name) } diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/save.rs b/crates/entity-derive-impl/src/entity/sql/postgres/save.rs index ee3883e..9df96f2 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/save.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/save.rs @@ -68,7 +68,7 @@ impl Context<'_> { let row_name = &self.row_name; let insertable_name = &self.insertable_name; let table = &self.table; - let columns_str = &self.columns_str; + let insert_columns_str = &self.insert_columns_str; let placeholders_str = &self.placeholders_str; let bindings = super::helpers::insert_bindings(self.entity.all_fields()); let error_type = self.entity.error_type(); @@ -91,7 +91,7 @@ impl Context<'_> { let mut entity: #entity_name = new.into(); let insertable = #insertable_name::from(&entity); let row: #row_name = sqlx::query_as( - concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") + concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") ) #(#bindings)* .fetch_one(&mut *tx).await?; diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/upsert.rs b/crates/entity-derive-impl/src/entity/sql/postgres/upsert.rs index 433df47..5249641 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/upsert.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/upsert.rs @@ -133,7 +133,7 @@ impl Context<'_> { .into_iter() .filter(|f| { let col = f.name_str(); - !FieldDef::is_id(f) && !conflict.contains(&col) + !FieldDef::is_id(f) && !f.storage.is_auto && !conflict.contains(&col) }) .map(|f| { let col = f.name_str(); @@ -147,7 +147,7 @@ impl Context<'_> { format!( "INSERT INTO {} ({}) VALUES ({}) ON CONFLICT ({}) {} RETURNING *", - self.table, self.columns_str, self.placeholders_str, target, action_sql + self.table, self.insert_columns_str, self.placeholders_str, target, action_sql ) } } diff --git a/crates/entity-derive-impl/src/entity/transaction.rs b/crates/entity-derive-impl/src/entity/transaction.rs index b1a211c..723baf1 100644 --- a/crates/entity-derive-impl/src/entity/transaction.rs +++ b/crates/entity-derive-impl/src/entity/transaction.rs @@ -65,6 +65,7 @@ fn generate_repo_adapter(entity: &EntityDef) -> TokenStream { let create_dto = &ctx.create_dto; let update_dto = &ctx.update_dto; let table = &ctx.table; + let insert_columns_str = &ctx.insert_columns_str; let columns_str = &ctx.columns_str; let placeholders_str = &ctx.placeholders_str; let id_name = ctx.id_name; @@ -95,7 +96,7 @@ fn generate_repo_adapter(entity: &EntityDef) -> TokenStream { let entity = #entity_name::from(dto); let insertable = #insertable_name::from(&entity); let row: #row_name = sqlx::query_as( - concat!("INSERT INTO ", #table, " (", #columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") + concat!("INSERT INTO ", #table, " (", #insert_columns_str, ") VALUES (", #placeholders_str, ") RETURNING *") ) #(#bindings)* .fetch_one(&mut **self.tx).await?; diff --git a/crates/entity-derive-impl/src/utils/fields.rs b/crates/entity-derive-impl/src/utils/fields.rs index c3d998a..526778e 100644 --- a/crates/entity-derive-impl/src/utils/fields.rs +++ b/crates/entity-derive-impl/src/utils/fields.rs @@ -45,7 +45,7 @@ pub fn assigns(fields: &[FieldDef], source: &str) -> Vec { let src = Ident::new(source, Span::call_site()); fields .iter() - .filter(|f| f.embed.is_none()) + .filter(|f| f.embed.is_none() && !f.storage.is_auto) .map(|f: &FieldDef| { let name = f.name(); if let Some((parent, sub)) = &f.embed_origin { @@ -63,7 +63,7 @@ pub fn assigns_clone(fields: &[FieldDef], source: &str) -> Vec { let src = Ident::new(source, Span::call_site()); fields .iter() - .filter(|f| f.embed.is_none()) + .filter(|f| f.embed.is_none() && !f.storage.is_auto) .map(|f: &FieldDef| { let name = f.name(); if let Some((parent, sub)) = &f.embed_origin {