From 1de0c9cfd61624700608e94616fa55c44454332c Mon Sep 17 00:00:00 2001 From: Alexander Zaitsev Date: Tue, 31 Dec 2024 13:30:53 +0200 Subject: [PATCH] Added insert multiple rows support (#8) * Added insert multiple rows support * Updated inserts to using Vec * Renamed Insert values property * Updated insert_many method documentation * Fixed different value types issue * Reset main.rs * Created eloquent_sql_row macro * Added insert columns validator * Formatting --------- Co-authored-by: Tjardo --- .../cannot_insert_with_different_columns.rs | 59 +++++++++++++++++++ eloquent_core/src/checks/mod.rs | 1 + eloquent_core/src/compilers/inserts.rs | 29 +++++---- eloquent_core/src/error.rs | 5 ++ eloquent_core/src/lib.rs | 11 +++- eloquent_core/src/queries/inserts.rs | 55 +++++++++++++++-- eloquent_core/src/validator.rs | 1 + 7 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 eloquent_core/src/checks/cannot_insert_with_different_columns.rs diff --git a/eloquent_core/src/checks/cannot_insert_with_different_columns.rs b/eloquent_core/src/checks/cannot_insert_with_different_columns.rs new file mode 100644 index 0000000..bceca51 --- /dev/null +++ b/eloquent_core/src/checks/cannot_insert_with_different_columns.rs @@ -0,0 +1,59 @@ +use crate::{error::EloquentError, PerformChecks, QueryBuilder}; + +pub struct CannotInsertWithDifferentColumns; + +impl PerformChecks for CannotInsertWithDifferentColumns { + fn check(builder: &QueryBuilder) -> Result<(), EloquentError> { + if builder.inserts.is_empty() { + return Ok(()); + } + + let column_count = builder + .inserts + .first() + .map(|insert| insert.values.len()) + .unwrap_or(0); + + let inconsistent_row = builder + .inserts + .iter() + .find(|insert| insert.values.len() != column_count); + + if inconsistent_row.is_some() { + return Err(EloquentError::InconsistentInsertColumns); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{eloquent_sql_row, error::EloquentError, QueryBuilder, ToSql}; + + #[test] + fn test_cannot_insert_with_different_columns() { + let result = QueryBuilder::new() + .table("users") + .insert_many(vec![ + eloquent_sql_row! { + "name" => "Alice", + "email" => "alice@example.com", + "is_active" => true, + }, + eloquent_sql_row! { + "name" => "Bob", + "email" => "bob@example.com", + "age" => 22, + "is_active" => false, + }, + ]) + .to_sql(); + + match result { + Err(EloquentError::InconsistentInsertColumns) => (), + Err(_error) => panic!(), + Ok(_value) => panic!(), + } + } +} diff --git a/eloquent_core/src/checks/mod.rs b/eloquent_core/src/checks/mod.rs index 28b0fe9..ddaaa97 100644 --- a/eloquent_core/src/checks/mod.rs +++ b/eloquent_core/src/checks/mod.rs @@ -9,3 +9,4 @@ pub mod having_clause_without_aggregate_function; pub mod missing_table; pub mod multiple_crud_actions; pub mod order_by_without_selected_or_aggregate_function; +pub mod cannot_insert_with_different_columns; diff --git a/eloquent_core/src/compilers/inserts.rs b/eloquent_core/src/compilers/inserts.rs index 0e60c0f..fb67f61 100644 --- a/eloquent_core/src/compilers/inserts.rs +++ b/eloquent_core/src/compilers/inserts.rs @@ -11,28 +11,27 @@ pub(crate) fn format<'a>( sql.push_str(table); sql.push_str(" ("); - sql.push_str( - &inserts - .iter() - .map(|insert| insert.column.clone()) - .collect::>() - .join(", "), - ); + let columns: Vec<_> = inserts.iter().map(|insert| insert.column.as_str()).collect(); + sql.push_str(&columns.join(", ")); + + sql.push_str(") VALUES "); - sql.push_str(") VALUES ("); + let row_count = inserts.first().map_or(0, |insert| insert.values.len()); - sql.push_str( - &inserts + let mut value_placeholders = vec![]; + for i in 0..row_count { + let row_values: Vec<_> = inserts .iter() .map(|insert| { - params.push(&insert.value); + params.push(&insert.values[i]); "?".to_string() }) - .collect::>() - .join(", "), - ); + .collect(); + + value_placeholders.push(format!("({})", row_values.join(", "))); + } - sql.push(')'); + sql.push_str(&value_placeholders.join(", ")); sql.to_string() } diff --git a/eloquent_core/src/error.rs b/eloquent_core/src/error.rs index 36d3f54..085c519 100644 --- a/eloquent_core/src/error.rs +++ b/eloquent_core/src/error.rs @@ -12,6 +12,7 @@ pub enum EloquentError { CannotApplyClauseOnUpdate(String), CannotApplyClauseOnDelete(String), CannotUseOffsetLimitWithPagination(String), + InconsistentInsertColumns, } impl std::error::Error for EloquentError {} @@ -57,6 +58,10 @@ impl std::fmt::Display for EloquentError { EloquentError::CannotUseOffsetLimitWithPagination(clause) => { write!(f, "Cannot use '{}' with PAGINATION", clause) } + EloquentError::InconsistentInsertColumns => write!( + f, + "INSERT statement has inconsistent column counts across rows" + ), } } } diff --git a/eloquent_core/src/lib.rs b/eloquent_core/src/lib.rs index 3dc1444..74e1b5a 100644 --- a/eloquent_core/src/lib.rs +++ b/eloquent_core/src/lib.rs @@ -101,7 +101,7 @@ struct Select { struct Insert { column: String, - value: Box, + values: Vec>, } struct Update { @@ -417,3 +417,12 @@ impl Condition { } } } + +#[macro_export] +macro_rules! eloquent_sql_row { + ($($key:expr => $value:expr),* $(,)?) => { + vec![ + $(($key, Box::new($value) as Box)),* + ] + }; +} diff --git a/eloquent_core/src/queries/inserts.rs b/eloquent_core/src/queries/inserts.rs index 8e5f5a0..5ad479e 100644 --- a/eloquent_core/src/queries/inserts.rs +++ b/eloquent_core/src/queries/inserts.rs @@ -17,11 +17,58 @@ impl QueryBuilder { /// ); /// ``` pub fn insert(mut self, column: &str, value: impl ToSql + 'static) -> Self { - self.inserts.push(Insert { - column: column.to_string(), - value: Box::new(value), - }); + self.add_insert(column, Box::new(value)); self } + + /// Insert single or multiple rows into the table. + /// + /// ``` + /// use eloquent_core::{QueryBuilder, ToSql, eloquent_sql_row}; + /// + /// let rows = vec![ + /// eloquent_sql_row! { + /// "name" => "Alice", + /// "email" => "alice@example.com", + /// "age" => 21, + /// "is_active" => true, + /// }, + /// eloquent_sql_row! { + /// "name" => "Bob", + /// "email" => "bob@example.com", + /// "age" => 22, + /// "is_active" => false, + /// }, + /// ]; + /// let query = QueryBuilder::new() + /// .table("users") + /// .insert_many(rows); + /// + /// assert_eq!( + /// query.sql().unwrap(), + /// "INSERT INTO users (name, email, age, is_active) VALUES ('Alice', 'alice@example.com', 21, true), ('Bob', 'bob@example.com', 22, false)" + /// ); + /// ``` + pub fn insert_many(mut self, rows: Vec)>>) -> Self { + rows.into_iter().for_each(|row| self.add_row(row)); + + self + } + + fn add_insert(&mut self, column: &str, value: Box) { + if let Some(insert) = self.inserts.iter_mut().find(|i| i.column == column) { + insert.values.push(value); + } else { + self.inserts.push(Insert { + column: column.to_string(), + values: vec![value], + }); + } + } + + fn add_row(&mut self, row: Vec<(&str, Box)>) { + row.into_iter() + .for_each(|(column, value)| self.add_insert(column, value)); + } } diff --git a/eloquent_core/src/validator.rs b/eloquent_core/src/validator.rs index 2b2fc41..235ea5f 100644 --- a/eloquent_core/src/validator.rs +++ b/eloquent_core/src/validator.rs @@ -15,6 +15,7 @@ impl QueryBuilder { cannot_apply_clause_on_update::CannotApplyClauseOnUpdate::check(self)?; cannot_apply_clause_on_delete::CannotApplyClauseOnDelete::check(self)?; cannot_use_offset_limit_with_pagination::CannotUseOffsetLimitWithPagination::check(self)?; + cannot_insert_with_different_columns::CannotInsertWithDifferentColumns::check(self)?; Ok(()) }