Skip to content

Commit 7dffaf1

Browse files
tyt2y3billy1624
andauthored
Insert many allow active models to have different column set (#2433)
* Insert many allow active models to have different column set * comment and fmt * comment * clippy * Fixup * Refactor * Docs and restore old implementation --------- Co-authored-by: Billy Chan <[email protected]>
1 parent 5d0efaa commit 7dffaf1

File tree

4 files changed

+163
-21
lines changed

4 files changed

+163
-21
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ tracing = { version = "0.1", default-features = false, features = ["attributes",
3434
rust_decimal = { version = "1", default-features = false, optional = true }
3535
bigdecimal = { version = "0.4", default-features = false, optional = true }
3636
sea-orm-macros = { version = "~1.1.2", path = "sea-orm-macros", default-features = false, features = ["strum"] }
37-
sea-query = { version = "0.32.0", default-features = false, features = ["thread-safe", "hashable-value", "backend-mysql", "backend-postgres", "backend-sqlite"] }
37+
sea-query = { version = "0.32.1", default-features = false, features = ["thread-safe", "hashable-value", "backend-mysql", "backend-postgres", "backend-sqlite"] }
3838
sea-query-binder = { version = "0.7.0", default-features = false, optional = true }
3939
strum = { version = "0.26", default-features = false }
4040
serde = { version = "1.0", default-features = false }

src/entity/base_entity.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,51 @@ pub trait EntityTrait: EntityName {
467467
/// # Ok(())
468468
/// # }
469469
/// ```
470+
///
471+
/// Before 1.1.3, if the active models have different column set, this method would panic.
472+
/// Now, it'd attempt to fill in the missing columns with null
473+
/// (which may or may not be correct, depending on whether the column is nullable):
474+
///
475+
/// ```
476+
/// use sea_orm::{
477+
/// entity::*,
478+
/// query::*,
479+
/// tests_cfg::{cake, cake_filling},
480+
/// DbBackend,
481+
/// };
482+
///
483+
/// assert_eq!(
484+
/// cake::Entity::insert_many([
485+
/// cake::ActiveModel {
486+
/// id: NotSet,
487+
/// name: Set("Apple Pie".to_owned()),
488+
/// },
489+
/// cake::ActiveModel {
490+
/// id: NotSet,
491+
/// name: Set("Orange Scone".to_owned()),
492+
/// }
493+
/// ])
494+
/// .build(DbBackend::Postgres)
495+
/// .to_string(),
496+
/// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie'), ('Orange Scone')"#,
497+
/// );
498+
///
499+
/// assert_eq!(
500+
/// cake_filling::Entity::insert_many([
501+
/// cake_filling::ActiveModel {
502+
/// cake_id: ActiveValue::set(2),
503+
/// filling_id: ActiveValue::NotSet,
504+
/// },
505+
/// cake_filling::ActiveModel {
506+
/// cake_id: ActiveValue::NotSet,
507+
/// filling_id: ActiveValue::set(3),
508+
/// }
509+
/// ])
510+
/// .build(DbBackend::Postgres)
511+
/// .to_string(),
512+
/// r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#,
513+
/// );
514+
/// ```
470515
fn insert_many<A, I>(models: I) -> Insert<A>
471516
where
472517
A: ActiveModelTrait<Entity = Self>,

src/query/insert.rs

Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
PrimaryKeyTrait, QueryTrait,
44
};
55
use core::marker::PhantomData;
6-
use sea_query::{Expr, InsertStatement, OnConflict, ValueTuple};
6+
use sea_query::{Expr, InsertStatement, Keyword, OnConflict, SimpleExpr, Value, ValueTuple};
77

88
/// Performs INSERT operations on a ActiveModel
99
#[derive(Debug)]
@@ -112,7 +112,7 @@ where
112112
///
113113
/// # Panics
114114
///
115-
/// Panics if the column value has discrepancy across rows
115+
/// Panics if the rows have different column sets from what've previously been cached in the query statement
116116
#[allow(clippy::should_implement_trait)]
117117
pub fn add<M>(mut self, m: M) -> Self
118118
where
@@ -149,15 +149,91 @@ where
149149
self
150150
}
151151

152+
/// Add many Models to Self. This is the legacy implementation priori to `1.1.3`.
153+
///
154+
/// # Panics
155+
///
156+
/// Panics if the rows have different column sets
157+
#[deprecated(
158+
since = "1.1.3",
159+
note = "Please use [`Insert::add_many`] which does not panic"
160+
)]
161+
pub fn add_multi<M, I>(mut self, models: I) -> Self
162+
where
163+
M: IntoActiveModel<A>,
164+
I: IntoIterator<Item = M>,
165+
{
166+
for model in models.into_iter() {
167+
self = self.add(model);
168+
}
169+
self
170+
}
171+
152172
/// Add many Models to Self
153173
pub fn add_many<M, I>(mut self, models: I) -> Self
154174
where
155175
M: IntoActiveModel<A>,
156176
I: IntoIterator<Item = M>,
157177
{
178+
let mut columns: Vec<_> = <A::Entity as EntityTrait>::Column::iter()
179+
.map(|_| None)
180+
.collect();
181+
let mut null_value: Vec<Option<Value>> =
182+
std::iter::repeat(None).take(columns.len()).collect();
183+
let mut all_values: Vec<Vec<SimpleExpr>> = Vec::new();
184+
158185
for model in models.into_iter() {
159-
self = self.add(model);
186+
let mut am: A = model.into_active_model();
187+
self.primary_key =
188+
if !<<A::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::auto_increment() {
189+
am.get_primary_key_value()
190+
} else {
191+
None
192+
};
193+
let mut values = Vec::with_capacity(columns.len());
194+
for (idx, col) in <A::Entity as EntityTrait>::Column::iter().enumerate() {
195+
let av = am.take(col);
196+
match av {
197+
ActiveValue::Set(value) | ActiveValue::Unchanged(value) => {
198+
columns[idx] = Some(col); // mark the column as used
199+
null_value[idx] = Some(value.as_null()); // store the null value with the correct type
200+
values.push(col.save_as(Expr::val(value))); // same as add() above
201+
}
202+
ActiveValue::NotSet => {
203+
values.push(SimpleExpr::Keyword(Keyword::Null)); // indicate a missing value
204+
}
205+
}
206+
}
207+
all_values.push(values);
208+
}
209+
210+
if !all_values.is_empty() {
211+
// filter only used column
212+
self.query.columns(columns.iter().cloned().flatten());
213+
214+
// flag used column
215+
self.columns = columns.iter().map(Option::is_some).collect();
216+
}
217+
218+
for values in all_values {
219+
// since we've aligned the column set, this never panics
220+
self.query
221+
.values_panic(values.into_iter().enumerate().filter_map(|(i, v)| {
222+
if columns[i].is_some() {
223+
// only if the column is used
224+
if !matches!(v, SimpleExpr::Keyword(Keyword::Null)) {
225+
// use the value expression
226+
Some(v)
227+
} else {
228+
// use null as standin, which must be Some
229+
null_value[i].clone().map(SimpleExpr::Value)
230+
}
231+
} else {
232+
None
233+
}
234+
}));
160235
}
236+
161237
self
162238
}
163239

@@ -209,16 +285,15 @@ where
209285
self
210286
}
211287

212-
/// Allow insert statement return safely if inserting nothing.
213-
/// The database will not be affected.
288+
/// Allow insert statement to return without error if nothing's been inserted
214289
pub fn do_nothing(self) -> TryInsert<A>
215290
where
216291
A: ActiveModelTrait,
217292
{
218293
TryInsert::from_insert(self)
219294
}
220295

221-
/// alias to do_nothing
296+
/// Alias to `do_nothing`
222297
pub fn on_empty_do_nothing(self) -> TryInsert<A>
223298
where
224299
A: ActiveModelTrait,
@@ -393,8 +468,11 @@ where
393468
mod tests {
394469
use sea_query::OnConflict;
395470

396-
use crate::tests_cfg::cake::{self};
397-
use crate::{ActiveValue, DbBackend, DbErr, EntityTrait, Insert, IntoActiveModel, QueryTrait};
471+
use crate::tests_cfg::{cake, cake_filling};
472+
use crate::{
473+
ActiveValue, DbBackend, DbErr, EntityTrait, Insert, IntoActiveModel, NotSet, QueryTrait,
474+
Set,
475+
};
398476

399477
#[test]
400478
fn insert_1() {
@@ -439,7 +517,7 @@ mod tests {
439517
}
440518

441519
#[test]
442-
fn insert_4() {
520+
fn insert_many_1() {
443521
assert_eq!(
444522
Insert::<cake::ActiveModel>::new()
445523
.add_many([
@@ -459,22 +537,41 @@ mod tests {
459537
}
460538

461539
#[test]
462-
#[should_panic(expected = "columns mismatch")]
463-
fn insert_5() {
464-
let apple = cake::ActiveModel {
465-
name: ActiveValue::set("Apple".to_owned()),
466-
..Default::default()
540+
fn insert_many_2() {
541+
assert_eq!(
542+
Insert::<cake::ActiveModel>::new()
543+
.add_many([
544+
cake::ActiveModel {
545+
id: NotSet,
546+
name: Set("Apple Pie".to_owned()),
547+
},
548+
cake::ActiveModel {
549+
id: NotSet,
550+
name: Set("Orange Scone".to_owned()),
551+
}
552+
])
553+
.build(DbBackend::Postgres)
554+
.to_string(),
555+
r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie'), ('Orange Scone')"#,
556+
);
557+
}
558+
559+
#[test]
560+
fn insert_many_3() {
561+
let apple = cake_filling::ActiveModel {
562+
cake_id: ActiveValue::set(2),
563+
filling_id: ActiveValue::NotSet,
467564
};
468-
let orange = cake::ActiveModel {
469-
id: ActiveValue::set(2),
470-
name: ActiveValue::set("Orange".to_owned()),
565+
let orange = cake_filling::ActiveModel {
566+
cake_id: ActiveValue::NotSet,
567+
filling_id: ActiveValue::set(3),
471568
};
472569
assert_eq!(
473-
Insert::<cake::ActiveModel>::new()
570+
Insert::<cake_filling::ActiveModel>::new()
474571
.add_many([apple, orange])
475572
.build(DbBackend::Postgres)
476573
.to_string(),
477-
r#"INSERT INTO "cake" ("id", "name") VALUES (NULL, 'Apple'), (2, 'Orange')"#,
574+
r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#,
478575
);
479576
}
480577

src/tests_cfg/cake_filling_price.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl EntityName for Entity {
1818
pub struct Model {
1919
pub cake_id: i32,
2020
pub filling_id: i32,
21-
#[cfg(feature = "with-decimal")]
21+
#[cfg(feature = "with-rust_decimal")]
2222
pub price: Decimal,
2323
#[sea_orm(ignore)]
2424
pub ignored_attr: i32,

0 commit comments

Comments
 (0)