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/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..a4b5c8e736b3 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 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_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 af0436aa0aa4..410d6174f156 100644 --- a/diesel_tests/tests/insert.rs +++ b/diesel_tests/tests/insert.rs @@ -452,6 +452,45 @@ 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 { + 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 ( + first_name VARCHAR NOT NULL PRIMARY KEY, + 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(), + full_name: "This field not inserted".to_string(), + }]; + 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..9999e14ecd10 100644 --- a/diesel_tests/tests/schema/pg_schema.rs +++ b/diesel_tests/tests/schema/pg_schema.rs @@ -175,6 +175,14 @@ table! { } } +table! { + user_with_last_names (first_name) { + first_name -> Varchar, + last_name -> Varchar, + full_name -> Varchar, + } +} + table! { with_keywords (fn_) { #[sql_name = "fn"]