Skip to main content

prax_query/operations/
create.rs

1//! Create operation for inserting new records.
2
3use 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
12/// A create operation for inserting a new record.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// let user = client
18///     .user()
19///     .create(user::Create {
20///         email: "new@example.com".into(),
21///         name: Some("New User".into()),
22///     })
23///     .exec()
24///     .await?;
25/// ```
26pub struct CreateOperation<E: QueryEngine, M: Model> {
27    engine: E,
28    columns: Vec<String>,
29    values: Vec<FilterValue>,
30    select: Select,
31    /// Queued nested-write ops run after the parent INSERT inside an
32    /// implicit transaction. Populated by [`CreateOperation::with`].
33    /// Empty on the fast path (single INSERT, no transaction wrap).
34    nested: Vec<NestedWriteOp>,
35    _model: PhantomData<M>,
36}
37
38impl<E: QueryEngine, M: Model + crate::row::FromRow> CreateOperation<E, M> {
39    /// Create a new Create operation.
40    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    /// Set a column value.
52    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    /// Set multiple column values from an iterator.
59    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    /// Select specific fields to return.
71    pub fn select(mut self, select: impl Into<Select>) -> Self {
72        self.select = select.into();
73        self
74    }
75
76    /// Apply a typed `SelectInput`.
77    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    /// Apply a typed `CreateInput`.
83    ///
84    /// The input's `into_ir` produces a flat `Vec<(column, value)>`
85    /// (per `prax_query::inputs::CreatePayload`), which is appended to
86    /// the operation's columns + values just like the existing
87    /// `set_many`. Phase 5a does not surface nested writes through
88    /// this path — relation operators inside `data:` are rejected by
89    /// codegen with a "phase 5b" diagnostic before reaching the
90    /// runtime.
91    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    /// Queue a nested write to run alongside this create.
104    ///
105    /// The parent `INSERT` and every queued nested op execute inside a
106    /// single implicit transaction — any failure rolls back the parent
107    /// INSERT too. Typical use is via the codegen-emitted per-relation
108    /// helpers:
109    ///
110    /// ```rust,ignore
111    /// c.user().create()
112    ///     .set("email", "u@x.com")
113    ///     .with(user::posts::create(vec![
114    ///         vec![("title".into(), "p1".into())],
115    ///     ]))
116    ///     .exec().await?;
117    /// ```
118    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    /// Build the SQL query.
127    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    /// Free-function form of [`Self::build_sql`] — takes the pieces by
135    /// reference so the `exec` path can reuse it after destructuring
136    /// `self` to move the captured state into the transaction closure.
137    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        // INSERT INTO clause
146        sql.push_str("INSERT INTO ");
147        sql.push_str(M::TABLE_NAME);
148
149        // Columns
150        sql.push_str(" (");
151        sql.push_str(&columns.join(", "));
152        sql.push(')');
153
154        // VALUES
155        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        // RETURNING clause
161        sql.push_str(&dialect.returning_clause(&select.to_sql()));
162
163        (sql, values.to_vec())
164    }
165
166    /// Execute the create operation and return the created record.
167    ///
168    /// When no nested writes have been queued via [`Self::with`], this
169    /// runs a single `INSERT ... RETURNING` (or equivalent) against the
170    /// engine. When nested writes are queued, the whole operation is
171    /// wrapped in a transaction — the parent `INSERT` runs first, then
172    /// each nested op in order; if any nested op fails the parent
173    /// insert is rolled back too.
174    ///
175    /// The `ModelWithPk` bound on the transactional branch is what
176    /// gives the nested-write executor the parent's primary-key value
177    /// to splice into child rows' foreign-key columns.
178    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        // Fast path: no nested writes, run the INSERT directly.
192        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        // Slow path: wrap the INSERT + nested writes in a transaction.
199        // `engine.transaction` clones the engine into the closure and
200        // routes every query emitted inside through the same `BEGIN`
201        // block. A non-Ok return from the closure triggers ROLLBACK.
202        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                // Batch consecutive Connect ops with the same
210                // (target_table, foreign_key, target_pk) into a single
211                // UPDATE ... WHERE pk IN (...). Creates and other
212                // variants pass through unchanged. Final state is
213                // identical; this just collapses adjacent runs to
214                // reduce round trips.
215                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
290/// Create many records at once.
291pub 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    /// Create a new CreateMany operation.
301    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    /// Set the columns for insertion.
312    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    /// Add a row of values.
318    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    /// Add multiple rows.
324    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    /// Skip records that violate unique constraints.
335    ///
336    /// Emitted per dialect: `ON CONFLICT DO NOTHING` (Postgres/SQLite)
337    /// or an `INSERT IGNORE` prefix (MySQL). Dialects with no
338    /// single-statement equivalent (MSSQL) emit a plain INSERT and log
339    /// a warning.
340    pub fn skip_duplicates(mut self) -> Self {
341        self.skip_duplicates = true;
342        self
343    }
344
345    /// Toggle `skip_duplicates` via a runtime flag.
346    ///
347    /// The bare [`Self::skip_duplicates`] is a builder-style "enable
348    /// it" call. The macros emit `with_skip_duplicates(<bool-expr>)`
349    /// so the DSL's `skip_duplicates: false` shortcut produces a
350    /// statement-level no-op without conditional macro emission.
351    pub fn with_skip_duplicates(mut self, flag: bool) -> Self {
352        self.skip_duplicates = flag;
353        self
354    }
355
356    /// Apply a batch of typed `CreateInput`s.
357    ///
358    /// Each input lowers to its own `CreatePayload`
359    /// (`Vec<(column, value)>`). The full set of columns across every
360    /// input becomes the operation's column list (first occurrence
361    /// wins for ordering); rows missing a column get `FilterValue::Null`
362    /// in that slot. This matches Prisma's `createMany` semantics,
363    /// where omitted optional fields are inserted as NULL.
364    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        // Lower every input first so we can compute the union column
370        // set before deciding the row layout.
371        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        // Seed columns from existing state (preserves any prior
379        // `.columns(...)` call) and append new columns in first-seen
380        // order. The set mirrors `columns` for O(1) membership checks
381        // instead of scanning the accumulating vec per column.
382        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        // Build each row in the canonical column order, padding missing
393        // entries with NULL. Indexing each row's values by column first
394        // keeps the canonical walk O(columns) per row instead of a
395        // linear scan per (row, column) pair.
396        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                // First occurrence wins, matching the previous
401                // `Iterator::find` semantics for duplicated columns.
402                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    /// Build the SQL query.
422    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        // Resolve skip_duplicates through the dialect before writing the
430        // INSERT keyword: MySQL has no trailing DO NOTHING clause, so its
431        // conflict handling is expressed as an `INSERT IGNORE` prefix
432        // (the canonical form — prax-query's Upsert builder emits the
433        // same for MySQL). `SqlDialect` is sealed, so classifying on the
434        // emitted clause shape is exhaustive over the known dialect set.
435        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        // INSERT INTO clause
443        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        // Columns
451        sql.push_str(" (");
452        sql.push_str(&self.columns.join(", "));
453        sql.push(')');
454
455        // VALUES
456        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        // Trailing skip_duplicates clause for dialects that express it
477        // as a suffix. MySQL was handled by the INSERT IGNORE prefix
478        // above.
479        if self.skip_duplicates && !insert_ignore {
480            if do_nothing.is_empty() {
481                // MSSQL/CQL: no single-statement equivalent — the trait
482                // returns an empty clause and the plain INSERT goes out
483                // unchanged (the nested-write path makes the same
484                // empty-clause fallback).
485                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                // Postgres/SQLite: the dialect wraps the conflict target in
492                // parens, but createMany skips rows conflicting on ANY
493                // unique constraint, so there is no target to name — the
494                // parenthesized form would be the invalid
495                // `ON CONFLICT () DO NOTHING`. The target-less form is the
496                // only valid spelling, same as the sibling Upsert builder
497                // emits for its do-nothing variant.
498                sql.push_str(" ON CONFLICT DO NOTHING");
499            }
500        }
501
502        (sql, all_params)
503    }
504
505    /// Execute the create operation and return the number of created records.
506    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    // Gate the transactional `CreateOperation::exec` path in tests:
534    // the new nested-write wiring requires `ModelWithPk` on the return
535    // type. A fixed constant PK is fine because these tests never
536    // exercise the nested path — they only need exec() to compile.
537    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    // ========== CreateOperation Tests ==========
635
636    #[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        // MockEngine returns not_found error for execute_insert
754        assert!(result.is_err());
755    }
756
757    // ========== CreateManyOperation Tests ==========
758
759    #[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")); // CreateMany doesn't return
766        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        // Params should be ordered: row1.a, row1.b, row2.a, row2.b
846        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    // ========== SQL Structure Tests ==========
867
868    #[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    // ========== Method Chaining Tests ==========
914
915    #[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    // ========== Cross-Dialect Tests ==========
945
946    #[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        // Regression guard: the dialect-level test at
960        // `dialect::tests::returning_mssql_is_output_inserted` verifies the
961        // per-column prefix expansion of `Mssql::returning_clause`, but not
962        // the wiring from the operation builder's `Select` list into that
963        // clause. If a future refactor fails to pass the selected columns
964        // through to the dialect, that path would silently fall back to
965        // `OUTPUT INSERTED.*`. This test pins the end-to-end SQL emitted by
966        // `CreateOperation::build_sql` when a narrow column list is set.
967        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        // MySQL has no ON CONFLICT DO NOTHING; skip_duplicates must come
995        // out as an INSERT IGNORE prefix (the canonical MySQL form).
996        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    // ========== Phase 5a: typed-input wiring ==========
1019
1020    /// Mock `CreateInput` used by the `with_create_input(s)` tests.
1021    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        // The chain should produce identical SQL to the existing
1042        // `.set(...).set(...)` chain — that's the contract.
1043        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        // Second input omits `email` — codegen does this for inputs
1055        // where the optional `email` field was left as `None`.
1056        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        // Row 1 introduces `name` only; row 2 introduces `email` (a
1071        // column row 1 doesn't have) and lists `email` before `name`.
1072        // Canonical order is first-seen across all rows — (name, email) —
1073        // and every row Null-fills the columns it doesn't mention.
1074        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, // row 1 has no email
1091                FilterValue::String("Bob".into()),
1092                FilterValue::String("b@x.com".into()),
1093            ]
1094        );
1095    }
1096}