1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::FilterValue;
7use crate::nested::NestedWriteOp;
8use crate::traits::{Model, ModelWithPk, QueryEngine};
9use crate::types::Select;
10
11pub struct CreateOperation<E: QueryEngine, M: Model> {
26 engine: E,
27 columns: Vec<String>,
28 values: Vec<FilterValue>,
29 select: Select,
30 nested: Vec<NestedWriteOp>,
34 _model: PhantomData<M>,
35}
36
37impl<E: QueryEngine, M: Model + crate::row::FromRow> CreateOperation<E, M> {
38 pub fn new(engine: E) -> Self {
40 Self {
41 engine,
42 columns: Vec::new(),
43 values: Vec::new(),
44 select: Select::All,
45 nested: Vec::new(),
46 _model: PhantomData,
47 }
48 }
49
50 pub fn set(mut self, column: impl Into<String>, value: impl Into<FilterValue>) -> Self {
52 self.columns.push(column.into());
53 self.values.push(value.into());
54 self
55 }
56
57 pub fn set_many(
59 mut self,
60 values: impl IntoIterator<Item = (impl Into<String>, impl Into<FilterValue>)>,
61 ) -> Self {
62 for (col, val) in values {
63 self.columns.push(col.into());
64 self.values.push(val.into());
65 }
66 self
67 }
68
69 pub fn select(mut self, select: impl Into<Select>) -> Self {
71 self.select = select.into();
72 self
73 }
74
75 pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
77 self.select = s.into_ir();
78 self
79 }
80
81 pub fn with_create_input<I>(mut self, input: I) -> Self
91 where
92 I: crate::inputs::CreateInput<Model = M, Data = crate::inputs::CreatePayload>,
93 {
94 let data: crate::inputs::CreatePayload = input.into_ir();
95 for (col, val) in data {
96 self.columns.push(col);
97 self.values.push(val);
98 }
99 self
100 }
101
102 pub fn with(mut self, nw: NestedWriteOp) -> Self
118 where
119 E: crate::capabilities::SupportsNestedWrites,
120 {
121 self.nested.push(nw);
122 self
123 }
124
125 pub fn build_sql(
127 &self,
128 dialect: &dyn crate::dialect::SqlDialect,
129 ) -> (String, Vec<FilterValue>) {
130 Self::build_insert_sql(&self.columns, &self.values, &self.select, dialect)
131 }
132
133 fn build_insert_sql(
137 columns: &[String],
138 values: &[FilterValue],
139 select: &Select,
140 dialect: &dyn crate::dialect::SqlDialect,
141 ) -> (String, Vec<FilterValue>) {
142 let mut sql = String::new();
143
144 sql.push_str("INSERT INTO ");
146 sql.push_str(M::TABLE_NAME);
147
148 sql.push_str(" (");
150 sql.push_str(&columns.join(", "));
151 sql.push(')');
152
153 sql.push_str(" VALUES (");
155 let placeholders: Vec<_> = (1..=values.len()).map(|i| dialect.placeholder(i)).collect();
156 sql.push_str(&placeholders.join(", "));
157 sql.push(')');
158
159 sql.push_str(&dialect.returning_clause(&select.to_sql()));
161
162 (sql, values.to_vec())
163 }
164
165 pub async fn exec(self) -> QueryResult<M>
178 where
179 M: Send + 'static + ModelWithPk,
180 {
181 let CreateOperation {
182 engine,
183 columns,
184 values,
185 select,
186 nested,
187 _model,
188 } = self;
189
190 if nested.is_empty() {
192 let dialect = engine.dialect();
193 let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
194 return engine.execute_insert::<M>(&sql, params).await;
195 }
196
197 engine
202 .transaction(move |tx| async move {
203 let dialect = tx.dialect();
204 let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
205 let parent: M = tx.execute_insert::<M>(&sql, params).await?;
206 let parent_pk = parent.pk_value();
207
208 let mut idx = 0;
215 while idx < nested.len() {
216 if let NestedWriteOp::Connect {
217 target_table: run_table,
218 foreign_key: run_fk,
219 target_pk: run_target_pk,
220 ..
221 } = &nested[idx]
222 {
223 let run_table = *run_table;
224 let run_fk = *run_fk;
225 let run_target_pk = *run_target_pk;
226 let mut end = idx + 1;
227 while end < nested.len() {
228 match &nested[end] {
229 NestedWriteOp::Connect {
230 target_table,
231 foreign_key,
232 target_pk,
233 ..
234 } if *target_table == run_table
235 && *foreign_key == run_fk
236 && *target_pk == run_target_pk =>
237 {
238 end += 1;
239 }
240 _ => break,
241 }
242 }
243
244 if end - idx == 1 {
245 let op = nested[idx].clone();
246 op.execute(&tx, &parent_pk).await?;
247 } else {
248 let expected = (end - idx) as u64;
249 let mut pks: Vec<FilterValue> = Vec::with_capacity(end - idx + 1);
250 pks.push(parent_pk.clone());
251 for op in &nested[idx..end] {
252 if let NestedWriteOp::Connect { pk, .. } = op {
253 pks.push(pk.clone());
254 }
255 }
256 let placeholders: Vec<String> =
257 (2..=pks.len()).map(|i| dialect.placeholder(i)).collect();
258 let sql = format!(
259 "UPDATE {} SET {} = {} WHERE {} IN ({})",
260 dialect.quote_ident(run_table),
261 dialect.quote_ident(run_fk),
262 dialect.placeholder(1),
263 dialect.quote_ident(run_target_pk),
264 placeholders.join(", "),
265 );
266 let affected = tx.execute_raw(&sql, pks).await?;
267 if affected != expected {
268 return Err(crate::error::QueryError::not_found(run_table)
269 .with_context("Nested Connect batch")
270 .with_help(format!(
271 "Expected {} matching rows but UPDATE affected {}",
272 expected, affected
273 )));
274 }
275 }
276 idx = end;
277 } else {
278 let op = nested[idx].clone();
279 op.execute(&tx, &parent_pk).await?;
280 idx += 1;
281 }
282 }
283 Ok(parent)
284 })
285 .await
286 }
287}
288
289pub struct CreateManyOperation<E: QueryEngine, M: Model> {
291 engine: E,
292 columns: Vec<String>,
293 rows: Vec<Vec<FilterValue>>,
294 skip_duplicates: bool,
295 _model: PhantomData<M>,
296}
297
298impl<E: QueryEngine, M: Model> CreateManyOperation<E, M> {
299 pub fn new(engine: E) -> Self {
301 Self {
302 engine,
303 columns: Vec::new(),
304 rows: Vec::new(),
305 skip_duplicates: false,
306 _model: PhantomData,
307 }
308 }
309
310 pub fn columns(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
312 self.columns = columns.into_iter().map(Into::into).collect();
313 self
314 }
315
316 pub fn row(mut self, values: impl IntoIterator<Item = impl Into<FilterValue>>) -> Self {
318 self.rows.push(values.into_iter().map(Into::into).collect());
319 self
320 }
321
322 pub fn rows(
324 mut self,
325 rows: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<FilterValue>>>,
326 ) -> Self {
327 for row in rows {
328 self.rows.push(row.into_iter().map(Into::into).collect());
329 }
330 self
331 }
332
333 pub fn skip_duplicates(mut self) -> Self {
335 self.skip_duplicates = true;
336 self
337 }
338
339 pub fn with_skip_duplicates(mut self, flag: bool) -> Self {
346 self.skip_duplicates = flag;
347 self
348 }
349
350 pub fn with_create_inputs<I, T>(mut self, inputs: I) -> Self
359 where
360 I: IntoIterator<Item = T>,
361 T: crate::inputs::CreateInput<Model = M, Data = crate::inputs::CreatePayload>,
362 {
363 let lowered: Vec<crate::inputs::CreatePayload> =
366 inputs.into_iter().map(|i| i.into_ir()).collect();
367
368 if lowered.is_empty() {
369 return self;
370 }
371
372 let mut columns: Vec<String> = self.columns.clone();
376 for row in &lowered {
377 for (col, _) in row {
378 if !columns.iter().any(|c| c == col) {
379 columns.push(col.clone());
380 }
381 }
382 }
383
384 let mut rows: Vec<Vec<FilterValue>> = Vec::with_capacity(lowered.len());
387 for row in lowered {
388 let mut out: Vec<FilterValue> = Vec::with_capacity(columns.len());
389 for col in &columns {
390 let v = row
391 .iter()
392 .find(|(c, _)| c == col)
393 .map(|(_, v)| v.clone())
394 .unwrap_or(FilterValue::Null);
395 out.push(v);
396 }
397 rows.push(out);
398 }
399
400 self.columns = columns;
401 self.rows.extend(rows);
402 self
403 }
404
405 pub fn build_sql(
407 &self,
408 dialect: &dyn crate::dialect::SqlDialect,
409 ) -> (String, Vec<FilterValue>) {
410 let mut sql = String::new();
411 let mut all_params = Vec::new();
412
413 sql.push_str("INSERT INTO ");
415 sql.push_str(M::TABLE_NAME);
416
417 sql.push_str(" (");
419 sql.push_str(&self.columns.join(", "));
420 sql.push(')');
421
422 sql.push_str(" VALUES ");
424
425 let mut value_groups = Vec::new();
426 let mut param_idx = 1;
427
428 for row in &self.rows {
429 let placeholders: Vec<_> = row
430 .iter()
431 .map(|v| {
432 all_params.push(v.clone());
433 let placeholder = dialect.placeholder(param_idx);
434 param_idx += 1;
435 placeholder
436 })
437 .collect();
438 value_groups.push(format!("({})", placeholders.join(", ")));
439 }
440
441 sql.push_str(&value_groups.join(", "));
442
443 if self.skip_duplicates {
445 sql.push_str(" ON CONFLICT DO NOTHING");
446 }
447
448 (sql, all_params)
449 }
450
451 pub async fn exec(self) -> QueryResult<u64> {
453 let dialect = self.engine.dialect();
454 let (sql, params) = self.build_sql(dialect);
455 self.engine.execute_raw(&sql, params).await
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::error::QueryError;
463
464 struct TestModel;
465
466 impl Model for TestModel {
467 const MODEL_NAME: &'static str = "TestModel";
468 const TABLE_NAME: &'static str = "test_models";
469 const PRIMARY_KEY: &'static [&'static str] = &["id"];
470 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
471 }
472
473 impl crate::row::FromRow for TestModel {
474 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
475 Ok(TestModel)
476 }
477 }
478
479 impl crate::traits::ModelWithPk for TestModel {
484 fn pk_value(&self) -> FilterValue {
485 FilterValue::Int(0)
486 }
487 fn get_column_value(&self, _column: &str) -> Option<FilterValue> {
488 None
489 }
490 }
491
492 #[derive(Clone)]
493 struct MockEngine {
494 insert_count: u64,
495 }
496
497 impl MockEngine {
498 fn new() -> Self {
499 Self { insert_count: 0 }
500 }
501
502 fn with_count(count: u64) -> Self {
503 Self {
504 insert_count: count,
505 }
506 }
507 }
508
509 impl QueryEngine for MockEngine {
510 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
511 &crate::dialect::Postgres
512 }
513
514 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
515 &self,
516 _sql: &str,
517 _params: Vec<FilterValue>,
518 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
519 Box::pin(async { Ok(Vec::new()) })
520 }
521
522 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
523 &self,
524 _sql: &str,
525 _params: Vec<FilterValue>,
526 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
527 Box::pin(async { Err(QueryError::not_found("test")) })
528 }
529
530 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
531 &self,
532 _sql: &str,
533 _params: Vec<FilterValue>,
534 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
535 Box::pin(async { Ok(None) })
536 }
537
538 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
539 &self,
540 _sql: &str,
541 _params: Vec<FilterValue>,
542 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
543 Box::pin(async { Err(QueryError::not_found("test")) })
544 }
545
546 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
547 &self,
548 _sql: &str,
549 _params: Vec<FilterValue>,
550 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
551 Box::pin(async { Ok(Vec::new()) })
552 }
553
554 fn execute_delete(
555 &self,
556 _sql: &str,
557 _params: Vec<FilterValue>,
558 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
559 Box::pin(async { Ok(0) })
560 }
561
562 fn execute_raw(
563 &self,
564 _sql: &str,
565 _params: Vec<FilterValue>,
566 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
567 let count = self.insert_count;
568 Box::pin(async move { Ok(count) })
569 }
570
571 fn count(
572 &self,
573 _sql: &str,
574 _params: Vec<FilterValue>,
575 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
576 Box::pin(async { Ok(0) })
577 }
578 }
579
580 #[test]
583 fn test_create_new() {
584 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
585 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
586
587 assert!(sql.contains("INSERT INTO test_models"));
588 assert!(sql.contains("RETURNING *"));
589 assert!(params.is_empty());
590 }
591
592 #[test]
593 fn test_create_basic() {
594 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
595 .set("name", "Alice")
596 .set("email", "alice@example.com");
597
598 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
599
600 assert!(sql.contains("INSERT INTO test_models"));
601 assert!(sql.contains("(name, email)"));
602 assert!(sql.contains("VALUES ($1, $2)"));
603 assert!(sql.contains("RETURNING *"));
604 assert_eq!(params.len(), 2);
605 }
606
607 #[test]
608 fn test_create_single_field() {
609 let op =
610 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
611
612 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
613
614 assert!(sql.contains("(name)"));
615 assert!(sql.contains("VALUES ($1)"));
616 assert_eq!(params.len(), 1);
617 }
618
619 #[test]
620 fn test_create_with_set_many() {
621 let values = vec![
622 ("name", FilterValue::String("Bob".to_string())),
623 ("email", FilterValue::String("bob@test.com".to_string())),
624 ("age", FilterValue::Int(25)),
625 ];
626 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set_many(values);
627
628 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
629
630 assert!(sql.contains("(name, email, age)"));
631 assert!(sql.contains("VALUES ($1, $2, $3)"));
632 assert_eq!(params.len(), 3);
633 }
634
635 #[test]
636 fn test_create_with_select() {
637 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
638 .set("name", "Alice")
639 .select(Select::fields(["id", "name"]));
640
641 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
642
643 assert!(sql.contains("RETURNING id, name"));
644 assert!(!sql.contains("RETURNING *"));
645 }
646
647 #[test]
648 fn test_create_with_null_value() {
649 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
650 .set("name", "Alice")
651 .set("nickname", FilterValue::Null);
652
653 let (_sql, params) = op.build_sql(&crate::dialect::Postgres);
654
655 assert_eq!(params.len(), 2);
656 assert_eq!(params[1], FilterValue::Null);
657 }
658
659 #[test]
660 fn test_create_with_boolean_value() {
661 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
662 .set("active", FilterValue::Bool(true));
663
664 let (_, params) = op.build_sql(&crate::dialect::Postgres);
665
666 assert_eq!(params[0], FilterValue::Bool(true));
667 }
668
669 #[test]
670 fn test_create_with_numeric_values() {
671 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
672 .set("count", FilterValue::Int(42))
673 .set("price", FilterValue::Float(99.99));
674
675 let (_, params) = op.build_sql(&crate::dialect::Postgres);
676
677 assert_eq!(params[0], FilterValue::Int(42));
678 assert_eq!(params[1], FilterValue::Float(99.99));
679 }
680
681 #[test]
682 fn test_create_with_json_value() {
683 let json = serde_json::json!({"key": "value", "nested": {"a": 1}});
684 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
685 .set("metadata", FilterValue::Json(json.clone()));
686
687 let (_, params) = op.build_sql(&crate::dialect::Postgres);
688
689 assert_eq!(params[0], FilterValue::Json(json));
690 }
691
692 #[tokio::test]
693 async fn test_create_exec() {
694 let op =
695 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
696
697 let result = op.exec().await;
698
699 assert!(result.is_err());
701 }
702
703 #[test]
706 fn test_create_many_new() {
707 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
708 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
709
710 assert!(sql.contains("INSERT INTO test_models"));
711 assert!(!sql.contains("RETURNING")); assert!(params.is_empty());
713 }
714
715 #[test]
716 fn test_create_many() {
717 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
718 .columns(["name", "email"])
719 .row(["Alice", "alice@example.com"])
720 .row(["Bob", "bob@example.com"]);
721
722 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
723
724 assert!(sql.contains("INSERT INTO test_models"));
725 assert!(sql.contains("(name, email)"));
726 assert!(sql.contains("VALUES ($1, $2), ($3, $4)"));
727 assert_eq!(params.len(), 4);
728 }
729
730 #[test]
731 fn test_create_many_single_row() {
732 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
733 .columns(["name"])
734 .row(["Alice"]);
735
736 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
737
738 assert!(sql.contains("VALUES ($1)"));
739 assert_eq!(params.len(), 1);
740 }
741
742 #[test]
743 fn test_create_many_skip_duplicates() {
744 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
745 .columns(["name", "email"])
746 .row(["Alice", "alice@example.com"])
747 .skip_duplicates();
748
749 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
750
751 assert!(sql.contains("ON CONFLICT DO NOTHING"));
752 }
753
754 #[test]
755 fn test_create_many_without_skip_duplicates() {
756 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
757 .columns(["name"])
758 .row(["Alice"]);
759
760 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
761
762 assert!(!sql.contains("ON CONFLICT"));
763 }
764
765 #[test]
766 fn test_create_many_with_rows() {
767 let rows = vec![
768 vec!["Alice", "alice@test.com"],
769 vec!["Bob", "bob@test.com"],
770 vec!["Charlie", "charlie@test.com"],
771 ];
772 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
773 .columns(["name", "email"])
774 .rows(rows);
775
776 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
777
778 assert!(sql.contains("VALUES ($1, $2), ($3, $4), ($5, $6)"));
779 assert_eq!(params.len(), 6);
780 }
781
782 #[test]
783 fn test_create_many_param_ordering() {
784 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
785 .columns(["a", "b"])
786 .row(["1", "2"])
787 .row(["3", "4"]);
788
789 let (_, params) = op.build_sql(&crate::dialect::Postgres);
790
791 assert_eq!(params[0], FilterValue::String("1".to_string()));
793 assert_eq!(params[1], FilterValue::String("2".to_string()));
794 assert_eq!(params[2], FilterValue::String("3".to_string()));
795 assert_eq!(params[3], FilterValue::String("4".to_string()));
796 }
797
798 #[tokio::test]
799 async fn test_create_many_exec() {
800 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(3))
801 .columns(["name"])
802 .row(["Alice"])
803 .row(["Bob"])
804 .row(["Charlie"]);
805
806 let result = op.exec().await;
807
808 assert!(result.is_ok());
809 assert_eq!(result.unwrap(), 3);
810 }
811
812 #[test]
815 fn test_create_sql_structure() {
816 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
817 .set("name", "Alice")
818 .select(Select::fields(["id"]));
819
820 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
821
822 let insert_pos = sql.find("INSERT INTO").unwrap();
823 let columns_pos = sql.find("(name)").unwrap();
824 let values_pos = sql.find("VALUES").unwrap();
825 let returning_pos = sql.find("RETURNING").unwrap();
826
827 assert!(insert_pos < columns_pos);
828 assert!(columns_pos < values_pos);
829 assert!(values_pos < returning_pos);
830 }
831
832 #[test]
833 fn test_create_many_sql_structure() {
834 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
835 .columns(["name", "email"])
836 .row(["Alice", "alice@test.com"])
837 .skip_duplicates();
838
839 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
840
841 let insert_pos = sql.find("INSERT INTO").unwrap();
842 let columns_pos = sql.find("(name, email)").unwrap();
843 let values_pos = sql.find("VALUES").unwrap();
844 let conflict_pos = sql.find("ON CONFLICT").unwrap();
845
846 assert!(insert_pos < columns_pos);
847 assert!(columns_pos < values_pos);
848 assert!(values_pos < conflict_pos);
849 }
850
851 #[test]
852 fn test_create_table_name() {
853 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
854 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
855
856 assert!(sql.contains("test_models"));
857 }
858
859 #[test]
862 fn test_create_method_chaining() {
863 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
864 .set("name", "Alice")
865 .set("email", "alice@test.com")
866 .select(Select::fields(["id", "name"]));
867
868 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
869
870 assert!(sql.contains("(name, email)"));
871 assert!(sql.contains("VALUES ($1, $2)"));
872 assert!(sql.contains("RETURNING id, name"));
873 assert_eq!(params.len(), 2);
874 }
875
876 #[test]
877 fn test_create_many_method_chaining() {
878 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
879 .columns(["a", "b"])
880 .row(["1", "2"])
881 .row(["3", "4"])
882 .skip_duplicates();
883
884 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
885
886 assert!(sql.contains("ON CONFLICT DO NOTHING"));
887 assert_eq!(params.len(), 4);
888 }
889
890 #[test]
893 fn create_mssql_emits_output_inserted() {
894 let op =
895 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
896 let (sql, _) = op.build_sql(&crate::dialect::Mssql);
897 assert!(
898 sql.contains(" OUTPUT INSERTED.*"),
899 "expected OUTPUT INSERTED.*, got: {sql}"
900 );
901 }
902
903 #[test]
904 fn create_mssql_emits_output_inserted_for_multiple_columns() {
905 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
914 .set("name", "Alice")
915 .set("email", "alice@example.com")
916 .select(Select::fields(["id", "email"]));
917
918 let (sql, params) = op.build_sql(&crate::dialect::Mssql);
919 assert!(
920 sql.contains(" OUTPUT INSERTED.id, INSERTED.email"),
921 "expected OUTPUT INSERTED.id, INSERTED.email, got: {sql}"
922 );
923 assert!(
924 !sql.contains("INSERTED.*"),
925 "narrow Select must not fall back to INSERTED.*: {sql}"
926 );
927 assert_eq!(params.len(), 2);
928 }
929
930 #[test]
931 fn create_postgres_emits_returning() {
932 let op =
933 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
934 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
935 assert!(sql.contains("RETURNING "), "expected RETURNING, got: {sql}");
936 }
937
938 struct MockCreateInput(Vec<(String, FilterValue)>);
942
943 impl crate::inputs::CreateInput for MockCreateInput {
944 type Model = TestModel;
945 type Data = crate::inputs::CreatePayload;
946 fn into_ir(self) -> Self::Data {
947 self.0
948 }
949 }
950
951 #[test]
952 fn with_create_input_appends_columns_and_values() {
953 let input = MockCreateInput(vec![
954 ("name".into(), FilterValue::String("Alice".into())),
955 ("email".into(), FilterValue::String("a@x.com".into())),
956 ]);
957 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
958 .with_create_input(input);
959
960 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
961 assert!(sql.contains("(name, email)"), "got: {sql}");
964 assert!(sql.contains("VALUES ($1, $2)"), "got: {sql}");
965 assert_eq!(params.len(), 2);
966 }
967
968 #[test]
969 fn with_create_inputs_pads_missing_columns_with_null() {
970 let row1 = MockCreateInput(vec![
971 ("name".into(), FilterValue::String("Alice".into())),
972 ("email".into(), FilterValue::String("a@x.com".into())),
973 ]);
974 let row2 = MockCreateInput(vec![("name".into(), FilterValue::String("Bob".into()))]);
977
978 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
979 .with_create_inputs(vec![row1, row2]);
980
981 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
982 assert!(sql.contains("(name, email)"), "got: {sql}");
983 assert!(sql.contains("VALUES ($1, $2), ($3, $4)"), "got: {sql}");
984 assert_eq!(params.len(), 4);
985 assert_eq!(params[3], FilterValue::Null);
986 }
987}