From 0ffbed6274ff399c8554d31d2f41afbe222fdf20 Mon Sep 17 00:00:00 2001 From: kirk Date: Wed, 29 Nov 2023 17:09:31 -0600 Subject: [PATCH 1/3] add skip_insertion attribute for Insertable derive macro --- diesel_derives/src/attrs.rs | 3 ++ diesel_derives/src/field.rs | 17 +++++++++++ diesel_derives/src/insertable.rs | 4 +++ diesel_tests/tests/insert.rs | 41 ++++++++++++++++++++++++++ diesel_tests/tests/schema/pg_schema.rs | 9 ++++++ 5 files changed, 74 insertions(+) diff --git a/diesel_derives/src/attrs.rs b/diesel_derives/src/attrs.rs index c7d82fe250a9..b9287b5d989b 100644 --- a/diesel_derives/src/attrs.rs +++ b/diesel_derives/src/attrs.rs @@ -32,6 +32,7 @@ pub struct AttributeSpanWrapper { pub enum FieldAttr { Embed(Ident), + SkipInsertion(Ident), ColumnName(Ident, SqlIdentifier), SqlType(Ident, TypePath), @@ -123,6 +124,7 @@ impl Parse for FieldAttr { match &*name_str { "embed" => Ok(FieldAttr::Embed(name)), + "skip_insertion" => Ok(FieldAttr::SkipInsertion(name)), "column_name" => Ok(FieldAttr::ColumnName( name, @@ -173,6 +175,7 @@ impl MySpanned for FieldAttr { fn span(&self) -> Span { match self { FieldAttr::Embed(ident) + | FieldAttr::SkipInsertion(ident) | FieldAttr::ColumnName(ident, _) | FieldAttr::SqlType(ident, _) | FieldAttr::TreatNoneAsNull(ident, _) diff --git a/diesel_derives/src/field.rs b/diesel_derives/src/field.rs index 88a0d8cbe17c..492bc9f5cfcb 100644 --- a/diesel_derives/src/field.rs +++ b/diesel_derives/src/field.rs @@ -17,6 +17,7 @@ pub struct Field { pub select_expression: Option>, pub select_expression_type: Option>, pub embed: Option>, + pub skip_insertion: Option>, } impl Field { @@ -30,6 +31,7 @@ impl Field { let mut serialize_as = None; let mut deserialize_as = None; let mut embed = None; + let mut skip_insertion = None; let mut select_expression = None; let mut select_expression_type = None; let mut treat_none_as_default_value = None; @@ -102,6 +104,13 @@ impl Field { ident_span, }) } + FieldAttr::SkipInsertion(_) => { + skip_insertion = Some(AttributeSpanWrapper { + item: true, + attribute_span, + ident_span, + }) + } } } @@ -128,6 +137,7 @@ impl Field { select_expression, select_expression_type, embed, + skip_insertion, }) } @@ -157,6 +167,13 @@ impl Field { pub(crate) fn embed(&self) -> bool { self.embed.as_ref().map(|a| a.item).unwrap_or(false) } + + pub(crate) fn skip_insertion(&self) -> bool { + self.skip_insertion + .as_ref() + .map(|a| a.item) + .unwrap_or(false) + } } pub enum FieldName { diff --git a/diesel_derives/src/insertable.rs b/diesel_derives/src/insertable.rs index f1774bc43cc3..b4f72081f055 100644 --- a/diesel_derives/src/insertable.rs +++ b/diesel_derives/src/insertable.rs @@ -45,6 +45,10 @@ fn derive_into_single_table( let mut ref_field_assign = Vec::with_capacity(model.fields().len()); for field in model.fields() { + // skip this field while generating the insertion + if let true = field.skip_insertion() { + continue; + } // Use field-level attr. with fallback to the struct-level one. let treat_none_as_default_value = match &field.treat_none_as_default_value { Some(attr) => { diff --git a/diesel_tests/tests/insert.rs b/diesel_tests/tests/insert.rs index af0436aa0aa4..2268b7718d3d 100644 --- a/diesel_tests/tests/insert.rs +++ b/diesel_tests/tests/insert.rs @@ -452,6 +452,47 @@ fn insert_returning_count_returns_number_of_rows_inserted() { assert_eq!(1, second_count); } +#[test] +#[cfg(not(any(feature = "mysql", feature = "sqlite")))] +fn insert_with_generated_column() { + use crate::schema::user_with_last_names::table as users; + #[derive(Debug, Queryable, Insertable, Selectable, Default)] + struct UserWithLastName { + id: i32, + first_name: String, + last_name: String, + #[diesel(skip_insertion)] + full_name: String, + } + + let connection = &mut connection(); + diesel::sql_query( + "CREATE TABLE user_with_last_names ( + id SERIAL PRIMARY KEY, + first_name VARCHAR NOT NULL, + last_name VARCHAR NOT NULL, + full_NAME VARCHAR GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED + )", + ) + .execute(connection) + .unwrap(); + let new_users: &[_] = &[UserWithLastName { + first_name: "Sean".to_string(), + last_name: "Black".to_string(), + ..Default::default() + }]; + let count = insert_into(users) + .values(new_users) + .execute(connection) + .unwrap(); + + assert_eq!(1, count); + + let sean_black: UserWithLastName = users.first(connection).unwrap(); + + assert_eq!("Sean Black", sean_black.full_name.as_str()); +} + #[derive(Insertable)] #[diesel(table_name = users)] struct BaldUser { diff --git a/diesel_tests/tests/schema/pg_schema.rs b/diesel_tests/tests/schema/pg_schema.rs index c838b56f2668..73e9b4c02e6d 100644 --- a/diesel_tests/tests/schema/pg_schema.rs +++ b/diesel_tests/tests/schema/pg_schema.rs @@ -175,6 +175,15 @@ table! { } } +table! { + user_with_last_names (id) { + id -> Int4, + first_name -> Varchar, + last_name -> Varchar, + full_name -> Varchar, + } +} + table! { with_keywords (fn_) { #[sql_name = "fn"] From b499541349b141898ee5ab70408f4758af01ac75 Mon Sep 17 00:00:00 2001 From: kirk Date: Fri, 1 Dec 2023 10:26:08 -0600 Subject: [PATCH 2/3] review feedback: if-let -> if and remove extraneous id column --- diesel_derives/src/insertable.rs | 2 +- diesel_tests/tests/insert.rs | 4 +--- diesel_tests/tests/schema/pg_schema.rs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/diesel_derives/src/insertable.rs b/diesel_derives/src/insertable.rs index b4f72081f055..a4b5c8e736b3 100644 --- a/diesel_derives/src/insertable.rs +++ b/diesel_derives/src/insertable.rs @@ -46,7 +46,7 @@ fn derive_into_single_table( for field in model.fields() { // skip this field while generating the insertion - if let true = field.skip_insertion() { + if field.skip_insertion() { continue; } // Use field-level attr. with fallback to the struct-level one. diff --git a/diesel_tests/tests/insert.rs b/diesel_tests/tests/insert.rs index 2268b7718d3d..5ec66394d347 100644 --- a/diesel_tests/tests/insert.rs +++ b/diesel_tests/tests/insert.rs @@ -458,7 +458,6 @@ fn insert_with_generated_column() { use crate::schema::user_with_last_names::table as users; #[derive(Debug, Queryable, Insertable, Selectable, Default)] struct UserWithLastName { - id: i32, first_name: String, last_name: String, #[diesel(skip_insertion)] @@ -468,8 +467,7 @@ fn insert_with_generated_column() { let connection = &mut connection(); diesel::sql_query( "CREATE TABLE user_with_last_names ( - id SERIAL PRIMARY KEY, - first_name VARCHAR NOT NULL, + first_name VARCHAR NOT NULL PRIMARY KEY, last_name VARCHAR NOT NULL, full_NAME VARCHAR GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED )", diff --git a/diesel_tests/tests/schema/pg_schema.rs b/diesel_tests/tests/schema/pg_schema.rs index 73e9b4c02e6d..9999e14ecd10 100644 --- a/diesel_tests/tests/schema/pg_schema.rs +++ b/diesel_tests/tests/schema/pg_schema.rs @@ -176,8 +176,7 @@ table! { } table! { - user_with_last_names (id) { - id -> Int4, + user_with_last_names (first_name) { first_name -> Varchar, last_name -> Varchar, full_name -> Varchar, From d2054c2aa8cfb044397606c7388f21c98db5cc24 Mon Sep 17 00:00:00 2001 From: kirk Date: Mon, 4 Dec 2023 13:09:11 -0600 Subject: [PATCH 3/3] weiznich review feedback: Add documentation of attribute to Insertable field attributes. Add changelog entry. Remove default intialization of struct fields in example test. --- CHANGELOG.md | 1 + diesel_derives/src/lib.rs | 2 ++ diesel_tests/tests/insert.rs | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4a4a269ead..c68a43102c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Increasing the minimal supported Rust version will always be coupled at least wi * Added automatic usage of all sqlite `rowid` aliases when no explicit primary key is defined for `print-schema` * Added a `#[dsl::auto_type]` attribute macro, allowing to infer type of query fragment functions * Added the same type inference on `Selectable` derives, which allows skipping specifying `select_expression_type` most of the time, in turn enabling most queries to be written using just a `Selectable` derive. +* Added an optional `#[diesel(skip_insertion)]` field attribute to the `Insertable` derive macro, allowing fields which map to generated columns to be skipped during insertion. ### Changed diff --git a/diesel_derives/src/lib.rs b/diesel_derives/src/lib.rs index 7f4d95fdf72d..b1dceb5f86fd 100644 --- a/diesel_derives/src/lib.rs +++ b/diesel_derives/src/lib.rs @@ -334,6 +334,8 @@ pub fn derive_identifiable(input: TokenStream) -> TokenStream { /// the actual field type. /// * `#[diesel(treat_none_as_default_value = true/false)]`, overrides the container-level /// `treat_none_as_default_value` attribute for the current field. +/// * `#[diesel(skip_insertion)]`, skips insertion of this field. Useful for working with +/// generated columns. /// /// # Examples /// diff --git a/diesel_tests/tests/insert.rs b/diesel_tests/tests/insert.rs index 5ec66394d347..410d6174f156 100644 --- a/diesel_tests/tests/insert.rs +++ b/diesel_tests/tests/insert.rs @@ -469,7 +469,7 @@ fn insert_with_generated_column() { "CREATE TABLE user_with_last_names ( first_name VARCHAR NOT NULL PRIMARY KEY, last_name VARCHAR NOT NULL, - full_NAME VARCHAR GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED + full_name VARCHAR GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED )", ) .execute(connection) @@ -477,7 +477,7 @@ fn insert_with_generated_column() { let new_users: &[_] = &[UserWithLastName { first_name: "Sean".to_string(), last_name: "Black".to_string(), - ..Default::default() + full_name: "This field not inserted".to_string(), }]; let count = insert_into(users) .values(new_users)