1use std::collections::{HashMap, HashSet};
4use std::marker::PhantomData;
5
6use crate::error::QueryResult;
7use crate::filter::FilterValue;
8use crate::nested::NestedWriteOp;
9use crate::traits::{Model, ModelWithPk, QueryEngine};
10use crate::types::Select;
11
12pub struct CreateOperation<E: QueryEngine, M: Model> {
27 engine: E,
28 columns: Vec<String>,
29 values: Vec<FilterValue>,
30 select: Select,
31 nested: Vec<NestedWriteOp>,
35 _model: PhantomData<M>,
36}
37
38impl<E: QueryEngine, M: Model + crate::row::FromRow> CreateOperation<E, M> {
39 pub fn new(engine: E) -> Self {
41 Self {
42 engine,
43 columns: Vec::new(),
44 values: Vec::new(),
45 select: Select::All,
46 nested: Vec::new(),
47 _model: PhantomData,
48 }
49 }
50
51 pub fn set(mut self, column: impl Into<String>, value: impl Into<FilterValue>) -> Self {
53 self.columns.push(column.into());
54 self.values.push(value.into());
55 self
56 }
57
58 pub fn set_many(
60 mut self,
61 values: impl IntoIterator<Item = (impl Into<String>, impl Into<FilterValue>)>,
62 ) -> Self {
63 for (col, val) in values {
64 self.columns.push(col.into());
65 self.values.push(val.into());
66 }
67 self
68 }
69
70 pub fn select(mut self, select: impl Into<Select>) -> Self {
72 self.select = select.into();
73 self
74 }
75
76 pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
78 self.select = s.into_ir();
79 self
80 }
81
82 pub fn with_create_input<I>(mut self, input: I) -> Self
92 where
93 I: crate::inputs::CreateInput<Model = M, Data = crate::inputs::CreatePayload>,
94 {
95 let data: crate::inputs::CreatePayload = input.into_ir();
96 for (col, val) in data {
97 self.columns.push(col);
98 self.values.push(val);
99 }
100 self
101 }
102
103 pub fn with(mut self, nw: NestedWriteOp) -> Self
119 where
120 E: crate::capabilities::SupportsNestedWrites,
121 {
122 self.nested.push(nw);
123 self
124 }
125
126 pub fn build_sql(
128 &self,
129 dialect: &dyn crate::dialect::SqlDialect,
130 ) -> (String, Vec<FilterValue>) {
131 Self::build_insert_sql(&self.columns, &self.values, &self.select, dialect)
132 }
133
134 fn build_insert_sql(
138 columns: &[String],
139 values: &[FilterValue],
140 select: &Select,
141 dialect: &dyn crate::dialect::SqlDialect,
142 ) -> (String, Vec<FilterValue>) {
143 let mut sql = String::new();
144
145 sql.push_str("INSERT INTO ");
147 sql.push_str(M::TABLE_NAME);
148
149 sql.push_str(" (");
151 sql.push_str(&columns.join(", "));
152 sql.push(')');
153
154 sql.push_str(" VALUES (");
156 let placeholders: Vec<_> = (1..=values.len()).map(|i| dialect.placeholder(i)).collect();
157 sql.push_str(&placeholders.join(", "));
158 sql.push(')');
159
160 sql.push_str(&dialect.returning_clause(&select.to_sql()));
162
163 (sql, values.to_vec())
164 }
165
166 pub async fn exec(self) -> QueryResult<M>
179 where
180 M: Send + 'static + ModelWithPk,
181 {
182 let CreateOperation {
183 engine,
184 columns,
185 values,
186 select,
187 nested,
188 _model,
189 } = self;
190
191 if nested.is_empty() {
193 let dialect = engine.dialect();
194 let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
195 return engine.execute_insert::<M>(&sql, params).await;
196 }
197
198 engine
203 .transaction(move |tx| async move {
204 let dialect = tx.dialect();
205 let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
206 let parent: M = tx.execute_insert::<M>(&sql, params).await?;
207 let parent_pk = parent.pk_value();
208
209 let mut idx = 0;
216 while idx < nested.len() {
217 if let NestedWriteOp::Connect {
218 target_table: run_table,
219 foreign_key: run_fk,
220 target_pk: run_target_pk,
221 ..
222 } = &nested[idx]
223 {
224 let run_table = *run_table;
225 let run_fk = *run_fk;
226 let run_target_pk = *run_target_pk;
227 let mut end = idx + 1;
228 while end < nested.len() {
229 match &nested[end] {
230 NestedWriteOp::Connect {
231 target_table,
232 foreign_key,
233 target_pk,
234 ..
235 } if *target_table == run_table
236 && *foreign_key == run_fk
237 && *target_pk == run_target_pk =>
238 {
239 end += 1;
240 }
241 _ => break,
242 }
243 }
244
245 if end - idx == 1 {
246 let op = nested[idx].clone();
247 op.execute(&tx, &parent_pk).await?;
248 } else {
249 let expected = (end - idx) as u64;
250 let mut pks: Vec<FilterValue> = Vec::with_capacity(end - idx + 1);
251 pks.push(parent_pk.clone());
252 for op in &nested[idx..end] {
253 if let NestedWriteOp::Connect { pk, .. } = op {
254 pks.push(pk.clone());
255 }
256 }
257 let placeholders: Vec<String> =
258 (2..=pks.len()).map(|i| dialect.placeholder(i)).collect();
259 let sql = format!(
260 "UPDATE {} SET {} = {} WHERE {} IN ({})",
261 dialect.quote_ident(run_table),
262 dialect.quote_ident(run_fk),
263 dialect.placeholder(1),
264 dialect.quote_ident(run_target_pk),
265 placeholders.join(", "),
266 );
267 let affected = tx.execute_raw(&sql, pks).await?;
268 if affected != expected {
269 return Err(crate::error::QueryError::not_found(run_table)
270 .with_context("Nested Connect batch")
271 .with_help(format!(
272 "Expected {} matching rows but UPDATE affected {}",
273 expected, affected
274 )));
275 }
276 }
277 idx = end;
278 } else {
279 let op = nested[idx].clone();
280 op.execute(&tx, &parent_pk).await?;
281 idx += 1;
282 }
283 }
284 Ok(parent)
285 })
286 .await
287 }
288}
289
290pub struct CreateManyOperation<E: QueryEngine, M: Model> {
292 engine: E,
293 columns: Vec<String>,
294 rows: Vec<Vec<FilterValue>>,
295 skip_duplicates: bool,
296 _model: PhantomData<M>,
297}
298
299impl<E: QueryEngine, M: Model> CreateManyOperation<E, M> {
300 pub fn new(engine: E) -> Self {
302 Self {
303 engine,
304 columns: Vec::new(),
305 rows: Vec::new(),
306 skip_duplicates: false,
307 _model: PhantomData,
308 }
309 }
310
311 pub fn columns(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
313 self.columns = columns.into_iter().map(Into::into).collect();
314 self
315 }
316
317 pub fn row(mut self, values: impl IntoIterator<Item = impl Into<FilterValue>>) -> Self {
319 self.rows.push(values.into_iter().map(Into::into).collect());
320 self
321 }
322
323 pub fn rows(
325 mut self,
326 rows: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<FilterValue>>>,
327 ) -> Self {
328 for row in rows {
329 self.rows.push(row.into_iter().map(Into::into).collect());
330 }
331 self
332 }
333
334 pub fn skip_duplicates(mut self) -> Self {
341 self.skip_duplicates = true;
342 self
343 }
344
345 pub fn with_skip_duplicates(mut self, flag: bool) -> Self {
352 self.skip_duplicates = flag;
353 self
354 }
355
356 pub fn with_create_inputs<I, T>(mut self, inputs: I) -> Self
365 where
366 I: IntoIterator<Item = T>,
367 T: crate::inputs::CreateInput<Model = M, Data = crate::inputs::CreatePayload>,
368 {
369 let lowered: Vec<crate::inputs::CreatePayload> =
372 inputs.into_iter().map(|i| i.into_ir()).collect();
373
374 if lowered.is_empty() {
375 return self;
376 }
377
378 let mut columns: Vec<String> = self.columns.clone();
383 let mut seen: HashSet<&str> = self.columns.iter().map(String::as_str).collect();
384 for row in &lowered {
385 for (col, _) in row {
386 if seen.insert(col.as_str()) {
387 columns.push(col.clone());
388 }
389 }
390 }
391
392 let mut rows: Vec<Vec<FilterValue>> = Vec::with_capacity(lowered.len());
397 for row in lowered {
398 let mut by_column: HashMap<&str, &FilterValue> = HashMap::with_capacity(row.len());
399 for (col, value) in &row {
400 by_column.entry(col.as_str()).or_insert(value);
403 }
404 let mut out: Vec<FilterValue> = Vec::with_capacity(columns.len());
405 for col in &columns {
406 out.push(
407 by_column
408 .get(col.as_str())
409 .map(|value| (*value).clone())
410 .unwrap_or(FilterValue::Null),
411 );
412 }
413 rows.push(out);
414 }
415
416 self.columns = columns;
417 self.rows.extend(rows);
418 self
419 }
420
421 pub fn build_sql(
423 &self,
424 dialect: &dyn crate::dialect::SqlDialect,
425 ) -> (String, Vec<FilterValue>) {
426 let mut sql = String::new();
427 let mut all_params = Vec::new();
428
429 let do_nothing = if self.skip_duplicates {
436 dialect.upsert_do_nothing_clause(&[])
437 } else {
438 String::new()
439 };
440 let insert_ignore = do_nothing.starts_with(" ON DUPLICATE KEY");
441
442 if insert_ignore {
444 sql.push_str("INSERT IGNORE INTO ");
445 } else {
446 sql.push_str("INSERT INTO ");
447 }
448 sql.push_str(M::TABLE_NAME);
449
450 sql.push_str(" (");
452 sql.push_str(&self.columns.join(", "));
453 sql.push(')');
454
455 sql.push_str(" VALUES ");
457
458 let mut value_groups = Vec::new();
459 let mut param_idx = 1;
460
461 for row in &self.rows {
462 let placeholders: Vec<_> = row
463 .iter()
464 .map(|v| {
465 all_params.push(v.clone());
466 let placeholder = dialect.placeholder(param_idx);
467 param_idx += 1;
468 placeholder
469 })
470 .collect();
471 value_groups.push(format!("({})", placeholders.join(", ")));
472 }
473
474 sql.push_str(&value_groups.join(", "));
475
476 if self.skip_duplicates && !insert_ignore {
480 if do_nothing.is_empty() {
481 tracing::warn!(
486 table = M::TABLE_NAME,
487 "skip_duplicates has no single-statement equivalent on this \
488 dialect; emitting a plain INSERT"
489 );
490 } else {
491 sql.push_str(" ON CONFLICT DO NOTHING");
499 }
500 }
501
502 (sql, all_params)
503 }
504
505 pub async fn exec(self) -> QueryResult<u64> {
507 let dialect = self.engine.dialect();
508 let (sql, params) = self.build_sql(dialect);
509 self.engine.execute_raw(&sql, params).await
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use crate::error::QueryError;
517
518 struct TestModel;
519
520 impl Model for TestModel {
521 const MODEL_NAME: &'static str = "TestModel";
522 const TABLE_NAME: &'static str = "test_models";
523 const PRIMARY_KEY: &'static [&'static str] = &["id"];
524 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
525 }
526
527 impl crate::row::FromRow for TestModel {
528 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
529 Ok(TestModel)
530 }
531 }
532
533 impl crate::traits::ModelWithPk for TestModel {
538 fn pk_value(&self) -> FilterValue {
539 FilterValue::Int(0)
540 }
541 fn get_column_value(&self, _column: &str) -> Option<FilterValue> {
542 None
543 }
544 }
545
546 #[derive(Clone)]
547 struct MockEngine {
548 insert_count: u64,
549 }
550
551 impl MockEngine {
552 fn new() -> Self {
553 Self { insert_count: 0 }
554 }
555
556 fn with_count(count: u64) -> Self {
557 Self {
558 insert_count: count,
559 }
560 }
561 }
562
563 impl QueryEngine for MockEngine {
564 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
565 &crate::dialect::Postgres
566 }
567
568 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
569 &self,
570 _sql: &str,
571 _params: Vec<FilterValue>,
572 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
573 Box::pin(async { Ok(Vec::new()) })
574 }
575
576 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
577 &self,
578 _sql: &str,
579 _params: Vec<FilterValue>,
580 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
581 Box::pin(async { Err(QueryError::not_found("test")) })
582 }
583
584 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
585 &self,
586 _sql: &str,
587 _params: Vec<FilterValue>,
588 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
589 Box::pin(async { Ok(None) })
590 }
591
592 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
593 &self,
594 _sql: &str,
595 _params: Vec<FilterValue>,
596 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
597 Box::pin(async { Err(QueryError::not_found("test")) })
598 }
599
600 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
601 &self,
602 _sql: &str,
603 _params: Vec<FilterValue>,
604 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
605 Box::pin(async { Ok(Vec::new()) })
606 }
607
608 fn execute_delete(
609 &self,
610 _sql: &str,
611 _params: Vec<FilterValue>,
612 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
613 Box::pin(async { Ok(0) })
614 }
615
616 fn execute_raw(
617 &self,
618 _sql: &str,
619 _params: Vec<FilterValue>,
620 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
621 let count = self.insert_count;
622 Box::pin(async move { Ok(count) })
623 }
624
625 fn count(
626 &self,
627 _sql: &str,
628 _params: Vec<FilterValue>,
629 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
630 Box::pin(async { Ok(0) })
631 }
632 }
633
634 #[test]
637 fn test_create_new() {
638 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
639 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
640
641 assert!(sql.contains("INSERT INTO test_models"));
642 assert!(sql.contains("RETURNING *"));
643 assert!(params.is_empty());
644 }
645
646 #[test]
647 fn test_create_basic() {
648 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
649 .set("name", "Alice")
650 .set("email", "alice@example.com");
651
652 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
653
654 assert!(sql.contains("INSERT INTO test_models"));
655 assert!(sql.contains("(name, email)"));
656 assert!(sql.contains("VALUES ($1, $2)"));
657 assert!(sql.contains("RETURNING *"));
658 assert_eq!(params.len(), 2);
659 }
660
661 #[test]
662 fn test_create_single_field() {
663 let op =
664 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
665
666 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
667
668 assert!(sql.contains("(name)"));
669 assert!(sql.contains("VALUES ($1)"));
670 assert_eq!(params.len(), 1);
671 }
672
673 #[test]
674 fn test_create_with_set_many() {
675 let values = vec![
676 ("name", FilterValue::String("Bob".to_string())),
677 ("email", FilterValue::String("bob@test.com".to_string())),
678 ("age", FilterValue::Int(25)),
679 ];
680 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set_many(values);
681
682 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
683
684 assert!(sql.contains("(name, email, age)"));
685 assert!(sql.contains("VALUES ($1, $2, $3)"));
686 assert_eq!(params.len(), 3);
687 }
688
689 #[test]
690 fn test_create_with_select() {
691 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
692 .set("name", "Alice")
693 .select(Select::fields(["id", "name"]));
694
695 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
696
697 assert!(sql.contains("RETURNING id, name"));
698 assert!(!sql.contains("RETURNING *"));
699 }
700
701 #[test]
702 fn test_create_with_null_value() {
703 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
704 .set("name", "Alice")
705 .set("nickname", FilterValue::Null);
706
707 let (_sql, params) = op.build_sql(&crate::dialect::Postgres);
708
709 assert_eq!(params.len(), 2);
710 assert_eq!(params[1], FilterValue::Null);
711 }
712
713 #[test]
714 fn test_create_with_boolean_value() {
715 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
716 .set("active", FilterValue::Bool(true));
717
718 let (_, params) = op.build_sql(&crate::dialect::Postgres);
719
720 assert_eq!(params[0], FilterValue::Bool(true));
721 }
722
723 #[test]
724 fn test_create_with_numeric_values() {
725 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
726 .set("count", FilterValue::Int(42))
727 .set("price", FilterValue::Float(99.99));
728
729 let (_, params) = op.build_sql(&crate::dialect::Postgres);
730
731 assert_eq!(params[0], FilterValue::Int(42));
732 assert_eq!(params[1], FilterValue::Float(99.99));
733 }
734
735 #[test]
736 fn test_create_with_json_value() {
737 let json = serde_json::json!({"key": "value", "nested": {"a": 1}});
738 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
739 .set("metadata", FilterValue::Json(json.clone()));
740
741 let (_, params) = op.build_sql(&crate::dialect::Postgres);
742
743 assert_eq!(params[0], FilterValue::Json(json));
744 }
745
746 #[tokio::test]
747 async fn test_create_exec() {
748 let op =
749 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
750
751 let result = op.exec().await;
752
753 assert!(result.is_err());
755 }
756
757 #[test]
760 fn test_create_many_new() {
761 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
762 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
763
764 assert!(sql.contains("INSERT INTO test_models"));
765 assert!(!sql.contains("RETURNING")); assert!(params.is_empty());
767 }
768
769 #[test]
770 fn test_create_many() {
771 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
772 .columns(["name", "email"])
773 .row(["Alice", "alice@example.com"])
774 .row(["Bob", "bob@example.com"]);
775
776 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
777
778 assert!(sql.contains("INSERT INTO test_models"));
779 assert!(sql.contains("(name, email)"));
780 assert!(sql.contains("VALUES ($1, $2), ($3, $4)"));
781 assert_eq!(params.len(), 4);
782 }
783
784 #[test]
785 fn test_create_many_single_row() {
786 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
787 .columns(["name"])
788 .row(["Alice"]);
789
790 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
791
792 assert!(sql.contains("VALUES ($1)"));
793 assert_eq!(params.len(), 1);
794 }
795
796 #[test]
797 fn test_create_many_skip_duplicates() {
798 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
799 .columns(["name", "email"])
800 .row(["Alice", "alice@example.com"])
801 .skip_duplicates();
802
803 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
804
805 assert!(sql.contains("ON CONFLICT DO NOTHING"));
806 }
807
808 #[test]
809 fn test_create_many_without_skip_duplicates() {
810 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
811 .columns(["name"])
812 .row(["Alice"]);
813
814 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
815
816 assert!(!sql.contains("ON CONFLICT"));
817 }
818
819 #[test]
820 fn test_create_many_with_rows() {
821 let rows = vec![
822 vec!["Alice", "alice@test.com"],
823 vec!["Bob", "bob@test.com"],
824 vec!["Charlie", "charlie@test.com"],
825 ];
826 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
827 .columns(["name", "email"])
828 .rows(rows);
829
830 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
831
832 assert!(sql.contains("VALUES ($1, $2), ($3, $4), ($5, $6)"));
833 assert_eq!(params.len(), 6);
834 }
835
836 #[test]
837 fn test_create_many_param_ordering() {
838 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
839 .columns(["a", "b"])
840 .row(["1", "2"])
841 .row(["3", "4"]);
842
843 let (_, params) = op.build_sql(&crate::dialect::Postgres);
844
845 assert_eq!(params[0], FilterValue::String("1".to_string()));
847 assert_eq!(params[1], FilterValue::String("2".to_string()));
848 assert_eq!(params[2], FilterValue::String("3".to_string()));
849 assert_eq!(params[3], FilterValue::String("4".to_string()));
850 }
851
852 #[tokio::test]
853 async fn test_create_many_exec() {
854 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(3))
855 .columns(["name"])
856 .row(["Alice"])
857 .row(["Bob"])
858 .row(["Charlie"]);
859
860 let result = op.exec().await;
861
862 assert!(result.is_ok());
863 assert_eq!(result.unwrap(), 3);
864 }
865
866 #[test]
869 fn test_create_sql_structure() {
870 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
871 .set("name", "Alice")
872 .select(Select::fields(["id"]));
873
874 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
875
876 let insert_pos = sql.find("INSERT INTO").unwrap();
877 let columns_pos = sql.find("(name)").unwrap();
878 let values_pos = sql.find("VALUES").unwrap();
879 let returning_pos = sql.find("RETURNING").unwrap();
880
881 assert!(insert_pos < columns_pos);
882 assert!(columns_pos < values_pos);
883 assert!(values_pos < returning_pos);
884 }
885
886 #[test]
887 fn test_create_many_sql_structure() {
888 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
889 .columns(["name", "email"])
890 .row(["Alice", "alice@test.com"])
891 .skip_duplicates();
892
893 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
894
895 let insert_pos = sql.find("INSERT INTO").unwrap();
896 let columns_pos = sql.find("(name, email)").unwrap();
897 let values_pos = sql.find("VALUES").unwrap();
898 let conflict_pos = sql.find("ON CONFLICT").unwrap();
899
900 assert!(insert_pos < columns_pos);
901 assert!(columns_pos < values_pos);
902 assert!(values_pos < conflict_pos);
903 }
904
905 #[test]
906 fn test_create_table_name() {
907 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
908 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
909
910 assert!(sql.contains("test_models"));
911 }
912
913 #[test]
916 fn test_create_method_chaining() {
917 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
918 .set("name", "Alice")
919 .set("email", "alice@test.com")
920 .select(Select::fields(["id", "name"]));
921
922 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
923
924 assert!(sql.contains("(name, email)"));
925 assert!(sql.contains("VALUES ($1, $2)"));
926 assert!(sql.contains("RETURNING id, name"));
927 assert_eq!(params.len(), 2);
928 }
929
930 #[test]
931 fn test_create_many_method_chaining() {
932 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
933 .columns(["a", "b"])
934 .row(["1", "2"])
935 .row(["3", "4"])
936 .skip_duplicates();
937
938 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
939
940 assert!(sql.contains("ON CONFLICT DO NOTHING"));
941 assert_eq!(params.len(), 4);
942 }
943
944 #[test]
947 fn create_mssql_emits_output_inserted() {
948 let op =
949 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
950 let (sql, _) = op.build_sql(&crate::dialect::Mssql);
951 assert!(
952 sql.contains(" OUTPUT INSERTED.*"),
953 "expected OUTPUT INSERTED.*, got: {sql}"
954 );
955 }
956
957 #[test]
958 fn create_mssql_emits_output_inserted_for_multiple_columns() {
959 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
968 .set("name", "Alice")
969 .set("email", "alice@example.com")
970 .select(Select::fields(["id", "email"]));
971
972 let (sql, params) = op.build_sql(&crate::dialect::Mssql);
973 assert!(
974 sql.contains(" OUTPUT INSERTED.id, INSERTED.email"),
975 "expected OUTPUT INSERTED.id, INSERTED.email, got: {sql}"
976 );
977 assert!(
978 !sql.contains("INSERTED.*"),
979 "narrow Select must not fall back to INSERTED.*: {sql}"
980 );
981 assert_eq!(params.len(), 2);
982 }
983
984 #[test]
985 fn create_postgres_emits_returning() {
986 let op =
987 CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
988 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
989 assert!(sql.contains("RETURNING "), "expected RETURNING, got: {sql}");
990 }
991
992 #[test]
993 fn create_many_mysql_skip_duplicates_emits_insert_ignore() {
994 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
997 .columns(["name", "email"])
998 .row(["Alice", "alice@example.com"])
999 .skip_duplicates();
1000
1001 let (sql, params) = op.build_sql(&crate::dialect::Mysql);
1002
1003 assert!(
1004 sql.starts_with("INSERT IGNORE INTO test_models"),
1005 "expected INSERT IGNORE prefix, got: {sql}"
1006 );
1007 assert!(
1008 !sql.contains("ON CONFLICT"),
1009 "MySQL must not get Postgres conflict syntax: {sql}"
1010 );
1011 assert!(
1012 !sql.contains("ON DUPLICATE KEY"),
1013 "skip_duplicates uses INSERT IGNORE, not a self-assign: {sql}"
1014 );
1015 assert_eq!(params.len(), 2);
1016 }
1017
1018 struct MockCreateInput(Vec<(String, FilterValue)>);
1022
1023 impl crate::inputs::CreateInput for MockCreateInput {
1024 type Model = TestModel;
1025 type Data = crate::inputs::CreatePayload;
1026 fn into_ir(self) -> Self::Data {
1027 self.0
1028 }
1029 }
1030
1031 #[test]
1032 fn with_create_input_appends_columns_and_values() {
1033 let input = MockCreateInput(vec![
1034 ("name".into(), FilterValue::String("Alice".into())),
1035 ("email".into(), FilterValue::String("a@x.com".into())),
1036 ]);
1037 let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
1038 .with_create_input(input);
1039
1040 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1041 assert!(sql.contains("(name, email)"), "got: {sql}");
1044 assert!(sql.contains("VALUES ($1, $2)"), "got: {sql}");
1045 assert_eq!(params.len(), 2);
1046 }
1047
1048 #[test]
1049 fn with_create_inputs_pads_missing_columns_with_null() {
1050 let row1 = MockCreateInput(vec![
1051 ("name".into(), FilterValue::String("Alice".into())),
1052 ("email".into(), FilterValue::String("a@x.com".into())),
1053 ]);
1054 let row2 = MockCreateInput(vec![("name".into(), FilterValue::String("Bob".into()))]);
1057
1058 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
1059 .with_create_inputs(vec![row1, row2]);
1060
1061 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1062 assert!(sql.contains("(name, email)"), "got: {sql}");
1063 assert!(sql.contains("VALUES ($1, $2), ($3, $4)"), "got: {sql}");
1064 assert_eq!(params.len(), 4);
1065 assert_eq!(params[3], FilterValue::Null);
1066 }
1067
1068 #[test]
1069 fn with_create_inputs_heterogeneous_rows_null_fill_both_directions() {
1070 let row1 = MockCreateInput(vec![("name".into(), FilterValue::String("Alice".into()))]);
1075 let row2 = MockCreateInput(vec![
1076 ("email".into(), FilterValue::String("b@x.com".into())),
1077 ("name".into(), FilterValue::String("Bob".into())),
1078 ]);
1079
1080 let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
1081 .with_create_inputs(vec![row1, row2]);
1082
1083 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1084 assert!(sql.contains("(name, email)"), "got: {sql}");
1085 assert!(sql.contains("VALUES ($1, $2), ($3, $4)"), "got: {sql}");
1086 assert_eq!(
1087 params,
1088 vec![
1089 FilterValue::String("Alice".into()),
1090 FilterValue::Null, FilterValue::String("Bob".into()),
1092 FilterValue::String("b@x.com".into()),
1093 ]
1094 );
1095 }
1096}