Skip to main content

prax_query/operations/
create.rs

1//! Create operation for inserting new records.
2
3use 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
11/// A create operation for inserting a new record.
12///
13/// # Example
14///
15/// ```rust,ignore
16/// let user = client
17///     .user()
18///     .create(user::Create {
19///         email: "new@example.com".into(),
20///         name: Some("New User".into()),
21///     })
22///     .exec()
23///     .await?;
24/// ```
25pub struct CreateOperation<E: QueryEngine, M: Model> {
26    engine: E,
27    columns: Vec<String>,
28    values: Vec<FilterValue>,
29    select: Select,
30    /// Queued nested-write ops run after the parent INSERT inside an
31    /// implicit transaction. Populated by [`CreateOperation::with`].
32    /// Empty on the fast path (single INSERT, no transaction wrap).
33    nested: Vec<NestedWriteOp>,
34    _model: PhantomData<M>,
35}
36
37impl<E: QueryEngine, M: Model + crate::row::FromRow> CreateOperation<E, M> {
38    /// Create a new Create operation.
39    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    /// Set a column value.
51    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    /// Set multiple column values from an iterator.
58    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    /// Select specific fields to return.
70    pub fn select(mut self, select: impl Into<Select>) -> Self {
71        self.select = select.into();
72        self
73    }
74
75    /// Apply a typed `SelectInput`.
76    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    /// Apply a typed `CreateInput`.
82    ///
83    /// The input's `into_ir` produces a flat `Vec<(column, value)>`
84    /// (per `prax_query::inputs::CreatePayload`), which is appended to
85    /// the operation's columns + values just like the existing
86    /// `set_many`. Phase 5a does not surface nested writes through
87    /// this path — relation operators inside `data:` are rejected by
88    /// codegen with a "phase 5b" diagnostic before reaching the
89    /// runtime.
90    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    /// Queue a nested write to run alongside this create.
103    ///
104    /// The parent `INSERT` and every queued nested op execute inside a
105    /// single implicit transaction — any failure rolls back the parent
106    /// INSERT too. Typical use is via the codegen-emitted per-relation
107    /// helpers:
108    ///
109    /// ```rust,ignore
110    /// c.user().create()
111    ///     .set("email", "u@x.com")
112    ///     .with(user::posts::create(vec![
113    ///         vec![("title".into(), "p1".into())],
114    ///     ]))
115    ///     .exec().await?;
116    /// ```
117    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    /// Build the SQL query.
126    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    /// Free-function form of [`Self::build_sql`] — takes the pieces by
134    /// reference so the `exec` path can reuse it after destructuring
135    /// `self` to move the captured state into the transaction closure.
136    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        // INSERT INTO clause
145        sql.push_str("INSERT INTO ");
146        sql.push_str(M::TABLE_NAME);
147
148        // Columns
149        sql.push_str(" (");
150        sql.push_str(&columns.join(", "));
151        sql.push(')');
152
153        // VALUES
154        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        // RETURNING clause
160        sql.push_str(&dialect.returning_clause(&select.to_sql()));
161
162        (sql, values.to_vec())
163    }
164
165    /// Execute the create operation and return the created record.
166    ///
167    /// When no nested writes have been queued via [`Self::with`], this
168    /// runs a single `INSERT ... RETURNING` (or equivalent) against the
169    /// engine. When nested writes are queued, the whole operation is
170    /// wrapped in a transaction — the parent `INSERT` runs first, then
171    /// each nested op in order; if any nested op fails the parent
172    /// insert is rolled back too.
173    ///
174    /// The `ModelWithPk` bound on the transactional branch is what
175    /// gives the nested-write executor the parent's primary-key value
176    /// to splice into child rows' foreign-key columns.
177    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        // Fast path: no nested writes, run the INSERT directly.
191        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        // Slow path: wrap the INSERT + nested writes in a transaction.
198        // `engine.transaction` clones the engine into the closure and
199        // routes every query emitted inside through the same `BEGIN`
200        // block. A non-Ok return from the closure triggers ROLLBACK.
201        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                // Batch consecutive Connect ops with the same
209                // (target_table, foreign_key, target_pk) into a single
210                // UPDATE ... WHERE pk IN (...). Creates and other
211                // variants pass through unchanged. Final state is
212                // identical; this just collapses adjacent runs to
213                // reduce round trips.
214                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
289/// Create many records at once.
290pub 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    /// Create a new CreateMany operation.
300    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    /// Set the columns for insertion.
311    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    /// Add a row of values.
317    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    /// Add multiple rows.
323    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    /// Skip records that violate unique constraints.
334    pub fn skip_duplicates(mut self) -> Self {
335        self.skip_duplicates = true;
336        self
337    }
338
339    /// Toggle `skip_duplicates` via a runtime flag.
340    ///
341    /// The bare [`Self::skip_duplicates`] is a builder-style "enable
342    /// it" call. The macros emit `with_skip_duplicates(<bool-expr>)`
343    /// so the DSL's `skip_duplicates: false` shortcut produces a
344    /// statement-level no-op without conditional macro emission.
345    pub fn with_skip_duplicates(mut self, flag: bool) -> Self {
346        self.skip_duplicates = flag;
347        self
348    }
349
350    /// Apply a batch of typed `CreateInput`s.
351    ///
352    /// Each input lowers to its own `CreatePayload`
353    /// (`Vec<(column, value)>`). The full set of columns across every
354    /// input becomes the operation's column list (first occurrence
355    /// wins for ordering); rows missing a column get `FilterValue::Null`
356    /// in that slot. This matches Prisma's `createMany` semantics,
357    /// where omitted optional fields are inserted as NULL.
358    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        // Lower every input first so we can compute the union column
364        // set before deciding the row layout.
365        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        // Seed columns from existing state (preserves any prior
373        // `.columns(...)` call) and append new columns in first-seen
374        // order.
375        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        // Build each row in the canonical column order, padding missing
385        // entries with NULL.
386        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    /// Build the SQL query.
406    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        // INSERT INTO clause
414        sql.push_str("INSERT INTO ");
415        sql.push_str(M::TABLE_NAME);
416
417        // Columns
418        sql.push_str(" (");
419        sql.push_str(&self.columns.join(", "));
420        sql.push(')');
421
422        // VALUES
423        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        // ON CONFLICT for skip_duplicates
444        if self.skip_duplicates {
445            sql.push_str(" ON CONFLICT DO NOTHING");
446        }
447
448        (sql, all_params)
449    }
450
451    /// Execute the create operation and return the number of created records.
452    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    // Gate the transactional `CreateOperation::exec` path in tests:
480    // the new nested-write wiring requires `ModelWithPk` on the return
481    // type. A fixed constant PK is fine because these tests never
482    // exercise the nested path — they only need exec() to compile.
483    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    // ========== CreateOperation Tests ==========
581
582    #[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        // MockEngine returns not_found error for execute_insert
700        assert!(result.is_err());
701    }
702
703    // ========== CreateManyOperation Tests ==========
704
705    #[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")); // CreateMany doesn't return
712        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        // Params should be ordered: row1.a, row1.b, row2.a, row2.b
792        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    // ========== SQL Structure Tests ==========
813
814    #[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    // ========== Method Chaining Tests ==========
860
861    #[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    // ========== Cross-Dialect Tests ==========
891
892    #[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        // Regression guard: the dialect-level test at
906        // `dialect::tests::returning_mssql_is_output_inserted` verifies the
907        // per-column prefix expansion of `Mssql::returning_clause`, but not
908        // the wiring from the operation builder's `Select` list into that
909        // clause. If a future refactor fails to pass the selected columns
910        // through to the dialect, that path would silently fall back to
911        // `OUTPUT INSERTED.*`. This test pins the end-to-end SQL emitted by
912        // `CreateOperation::build_sql` when a narrow column list is set.
913        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    // ========== Phase 5a: typed-input wiring ==========
939
940    /// Mock `CreateInput` used by the `with_create_input(s)` tests.
941    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        // The chain should produce identical SQL to the existing
962        // `.set(...).set(...)` chain — that's the contract.
963        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        // Second input omits `email` — codegen does this for inputs
975        // where the optional `email` field was left as `None`.
976        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}