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    /// Queue a nested write to run alongside this create.
76    ///
77    /// The parent `INSERT` and every queued nested op execute inside a
78    /// single implicit transaction — any failure rolls back the parent
79    /// INSERT too. Typical use is via the codegen-emitted per-relation
80    /// helpers:
81    ///
82    /// ```rust,ignore
83    /// c.user().create()
84    ///     .set("email", "u@x.com")
85    ///     .with(user::posts::create(vec![
86    ///         vec![("title".into(), "p1".into())],
87    ///     ]))
88    ///     .exec().await?;
89    /// ```
90    pub fn with(mut self, nw: NestedWriteOp) -> Self {
91        self.nested.push(nw);
92        self
93    }
94
95    /// Build the SQL query.
96    pub fn build_sql(
97        &self,
98        dialect: &dyn crate::dialect::SqlDialect,
99    ) -> (String, Vec<FilterValue>) {
100        Self::build_insert_sql(&self.columns, &self.values, &self.select, dialect)
101    }
102
103    /// Free-function form of [`Self::build_sql`] — takes the pieces by
104    /// reference so the `exec` path can reuse it after destructuring
105    /// `self` to move the captured state into the transaction closure.
106    fn build_insert_sql(
107        columns: &[String],
108        values: &[FilterValue],
109        select: &Select,
110        dialect: &dyn crate::dialect::SqlDialect,
111    ) -> (String, Vec<FilterValue>) {
112        let mut sql = String::new();
113
114        // INSERT INTO clause
115        sql.push_str("INSERT INTO ");
116        sql.push_str(M::TABLE_NAME);
117
118        // Columns
119        sql.push_str(" (");
120        sql.push_str(&columns.join(", "));
121        sql.push(')');
122
123        // VALUES
124        sql.push_str(" VALUES (");
125        let placeholders: Vec<_> = (1..=values.len()).map(|i| dialect.placeholder(i)).collect();
126        sql.push_str(&placeholders.join(", "));
127        sql.push(')');
128
129        // RETURNING clause
130        sql.push_str(&dialect.returning_clause(&select.to_sql()));
131
132        (sql, values.to_vec())
133    }
134
135    /// Execute the create operation and return the created record.
136    ///
137    /// When no nested writes have been queued via [`Self::with`], this
138    /// runs a single `INSERT ... RETURNING` (or equivalent) against the
139    /// engine. When nested writes are queued, the whole operation is
140    /// wrapped in a transaction — the parent `INSERT` runs first, then
141    /// each nested op in order; if any nested op fails the parent
142    /// insert is rolled back too.
143    ///
144    /// The `ModelWithPk` bound on the transactional branch is what
145    /// gives the nested-write executor the parent's primary-key value
146    /// to splice into child rows' foreign-key columns.
147    pub async fn exec(self) -> QueryResult<M>
148    where
149        M: Send + 'static + ModelWithPk,
150    {
151        let CreateOperation {
152            engine,
153            columns,
154            values,
155            select,
156            nested,
157            _model,
158        } = self;
159
160        // Fast path: no nested writes, run the INSERT directly.
161        if nested.is_empty() {
162            let dialect = engine.dialect();
163            let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
164            return engine.execute_insert::<M>(&sql, params).await;
165        }
166
167        // Slow path: wrap the INSERT + nested writes in a transaction.
168        // `engine.transaction` clones the engine into the closure and
169        // routes every query emitted inside through the same `BEGIN`
170        // block. A non-Ok return from the closure triggers ROLLBACK.
171        engine
172            .transaction(move |tx| async move {
173                let dialect = tx.dialect();
174                let (sql, params) = Self::build_insert_sql(&columns, &values, &select, dialect);
175                let parent: M = tx.execute_insert::<M>(&sql, params).await?;
176                let parent_pk = parent.pk_value();
177                for nw in nested {
178                    nw.execute(&tx, &parent_pk).await?;
179                }
180                Ok(parent)
181            })
182            .await
183    }
184}
185
186/// Create many records at once.
187pub struct CreateManyOperation<E: QueryEngine, M: Model> {
188    engine: E,
189    columns: Vec<String>,
190    rows: Vec<Vec<FilterValue>>,
191    skip_duplicates: bool,
192    _model: PhantomData<M>,
193}
194
195impl<E: QueryEngine, M: Model> CreateManyOperation<E, M> {
196    /// Create a new CreateMany operation.
197    pub fn new(engine: E) -> Self {
198        Self {
199            engine,
200            columns: Vec::new(),
201            rows: Vec::new(),
202            skip_duplicates: false,
203            _model: PhantomData,
204        }
205    }
206
207    /// Set the columns for insertion.
208    pub fn columns(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
209        self.columns = columns.into_iter().map(Into::into).collect();
210        self
211    }
212
213    /// Add a row of values.
214    pub fn row(mut self, values: impl IntoIterator<Item = impl Into<FilterValue>>) -> Self {
215        self.rows.push(values.into_iter().map(Into::into).collect());
216        self
217    }
218
219    /// Add multiple rows.
220    pub fn rows(
221        mut self,
222        rows: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<FilterValue>>>,
223    ) -> Self {
224        for row in rows {
225            self.rows.push(row.into_iter().map(Into::into).collect());
226        }
227        self
228    }
229
230    /// Skip records that violate unique constraints.
231    pub fn skip_duplicates(mut self) -> Self {
232        self.skip_duplicates = true;
233        self
234    }
235
236    /// Build the SQL query.
237    pub fn build_sql(
238        &self,
239        dialect: &dyn crate::dialect::SqlDialect,
240    ) -> (String, Vec<FilterValue>) {
241        let mut sql = String::new();
242        let mut all_params = Vec::new();
243
244        // INSERT INTO clause
245        sql.push_str("INSERT INTO ");
246        sql.push_str(M::TABLE_NAME);
247
248        // Columns
249        sql.push_str(" (");
250        sql.push_str(&self.columns.join(", "));
251        sql.push(')');
252
253        // VALUES
254        sql.push_str(" VALUES ");
255
256        let mut value_groups = Vec::new();
257        let mut param_idx = 1;
258
259        for row in &self.rows {
260            let placeholders: Vec<_> = row
261                .iter()
262                .map(|v| {
263                    all_params.push(v.clone());
264                    let placeholder = dialect.placeholder(param_idx);
265                    param_idx += 1;
266                    placeholder
267                })
268                .collect();
269            value_groups.push(format!("({})", placeholders.join(", ")));
270        }
271
272        sql.push_str(&value_groups.join(", "));
273
274        // ON CONFLICT for skip_duplicates
275        if self.skip_duplicates {
276            sql.push_str(" ON CONFLICT DO NOTHING");
277        }
278
279        (sql, all_params)
280    }
281
282    /// Execute the create operation and return the number of created records.
283    pub async fn exec(self) -> QueryResult<u64> {
284        let dialect = self.engine.dialect();
285        let (sql, params) = self.build_sql(dialect);
286        self.engine.execute_raw(&sql, params).await
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::error::QueryError;
294
295    struct TestModel;
296
297    impl Model for TestModel {
298        const MODEL_NAME: &'static str = "TestModel";
299        const TABLE_NAME: &'static str = "test_models";
300        const PRIMARY_KEY: &'static [&'static str] = &["id"];
301        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
302    }
303
304    impl crate::row::FromRow for TestModel {
305        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
306            Ok(TestModel)
307        }
308    }
309
310    // Gate the transactional `CreateOperation::exec` path in tests:
311    // the new nested-write wiring requires `ModelWithPk` on the return
312    // type. A fixed constant PK is fine because these tests never
313    // exercise the nested path — they only need exec() to compile.
314    impl crate::traits::ModelWithPk for TestModel {
315        fn pk_value(&self) -> FilterValue {
316            FilterValue::Int(0)
317        }
318        fn get_column_value(&self, _column: &str) -> Option<FilterValue> {
319            None
320        }
321    }
322
323    #[derive(Clone)]
324    struct MockEngine {
325        insert_count: u64,
326    }
327
328    impl MockEngine {
329        fn new() -> Self {
330            Self { insert_count: 0 }
331        }
332
333        fn with_count(count: u64) -> Self {
334            Self {
335                insert_count: count,
336            }
337        }
338    }
339
340    impl QueryEngine for MockEngine {
341        fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
342            &crate::dialect::Postgres
343        }
344
345        fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
346            &self,
347            _sql: &str,
348            _params: Vec<FilterValue>,
349        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
350            Box::pin(async { Ok(Vec::new()) })
351        }
352
353        fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
354            &self,
355            _sql: &str,
356            _params: Vec<FilterValue>,
357        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
358            Box::pin(async { Err(QueryError::not_found("test")) })
359        }
360
361        fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
362            &self,
363            _sql: &str,
364            _params: Vec<FilterValue>,
365        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
366            Box::pin(async { Ok(None) })
367        }
368
369        fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
370            &self,
371            _sql: &str,
372            _params: Vec<FilterValue>,
373        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
374            Box::pin(async { Err(QueryError::not_found("test")) })
375        }
376
377        fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
378            &self,
379            _sql: &str,
380            _params: Vec<FilterValue>,
381        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
382            Box::pin(async { Ok(Vec::new()) })
383        }
384
385        fn execute_delete(
386            &self,
387            _sql: &str,
388            _params: Vec<FilterValue>,
389        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
390            Box::pin(async { Ok(0) })
391        }
392
393        fn execute_raw(
394            &self,
395            _sql: &str,
396            _params: Vec<FilterValue>,
397        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
398            let count = self.insert_count;
399            Box::pin(async move { Ok(count) })
400        }
401
402        fn count(
403            &self,
404            _sql: &str,
405            _params: Vec<FilterValue>,
406        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
407            Box::pin(async { Ok(0) })
408        }
409    }
410
411    // ========== CreateOperation Tests ==========
412
413    #[test]
414    fn test_create_new() {
415        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
416        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
417
418        assert!(sql.contains("INSERT INTO test_models"));
419        assert!(sql.contains("RETURNING *"));
420        assert!(params.is_empty());
421    }
422
423    #[test]
424    fn test_create_basic() {
425        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
426            .set("name", "Alice")
427            .set("email", "alice@example.com");
428
429        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
430
431        assert!(sql.contains("INSERT INTO test_models"));
432        assert!(sql.contains("(name, email)"));
433        assert!(sql.contains("VALUES ($1, $2)"));
434        assert!(sql.contains("RETURNING *"));
435        assert_eq!(params.len(), 2);
436    }
437
438    #[test]
439    fn test_create_single_field() {
440        let op =
441            CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
442
443        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
444
445        assert!(sql.contains("(name)"));
446        assert!(sql.contains("VALUES ($1)"));
447        assert_eq!(params.len(), 1);
448    }
449
450    #[test]
451    fn test_create_with_set_many() {
452        let values = vec![
453            ("name", FilterValue::String("Bob".to_string())),
454            ("email", FilterValue::String("bob@test.com".to_string())),
455            ("age", FilterValue::Int(25)),
456        ];
457        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set_many(values);
458
459        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
460
461        assert!(sql.contains("(name, email, age)"));
462        assert!(sql.contains("VALUES ($1, $2, $3)"));
463        assert_eq!(params.len(), 3);
464    }
465
466    #[test]
467    fn test_create_with_select() {
468        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
469            .set("name", "Alice")
470            .select(Select::fields(["id", "name"]));
471
472        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
473
474        assert!(sql.contains("RETURNING id, name"));
475        assert!(!sql.contains("RETURNING *"));
476    }
477
478    #[test]
479    fn test_create_with_null_value() {
480        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
481            .set("name", "Alice")
482            .set("nickname", FilterValue::Null);
483
484        let (_sql, params) = op.build_sql(&crate::dialect::Postgres);
485
486        assert_eq!(params.len(), 2);
487        assert_eq!(params[1], FilterValue::Null);
488    }
489
490    #[test]
491    fn test_create_with_boolean_value() {
492        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
493            .set("active", FilterValue::Bool(true));
494
495        let (_, params) = op.build_sql(&crate::dialect::Postgres);
496
497        assert_eq!(params[0], FilterValue::Bool(true));
498    }
499
500    #[test]
501    fn test_create_with_numeric_values() {
502        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
503            .set("count", FilterValue::Int(42))
504            .set("price", FilterValue::Float(99.99));
505
506        let (_, params) = op.build_sql(&crate::dialect::Postgres);
507
508        assert_eq!(params[0], FilterValue::Int(42));
509        assert_eq!(params[1], FilterValue::Float(99.99));
510    }
511
512    #[test]
513    fn test_create_with_json_value() {
514        let json = serde_json::json!({"key": "value", "nested": {"a": 1}});
515        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
516            .set("metadata", FilterValue::Json(json.clone()));
517
518        let (_, params) = op.build_sql(&crate::dialect::Postgres);
519
520        assert_eq!(params[0], FilterValue::Json(json));
521    }
522
523    #[tokio::test]
524    async fn test_create_exec() {
525        let op =
526            CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
527
528        let result = op.exec().await;
529
530        // MockEngine returns not_found error for execute_insert
531        assert!(result.is_err());
532    }
533
534    // ========== CreateManyOperation Tests ==========
535
536    #[test]
537    fn test_create_many_new() {
538        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
539        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
540
541        assert!(sql.contains("INSERT INTO test_models"));
542        assert!(!sql.contains("RETURNING")); // CreateMany doesn't return
543        assert!(params.is_empty());
544    }
545
546    #[test]
547    fn test_create_many() {
548        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
549            .columns(["name", "email"])
550            .row(["Alice", "alice@example.com"])
551            .row(["Bob", "bob@example.com"]);
552
553        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
554
555        assert!(sql.contains("INSERT INTO test_models"));
556        assert!(sql.contains("(name, email)"));
557        assert!(sql.contains("VALUES ($1, $2), ($3, $4)"));
558        assert_eq!(params.len(), 4);
559    }
560
561    #[test]
562    fn test_create_many_single_row() {
563        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
564            .columns(["name"])
565            .row(["Alice"]);
566
567        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
568
569        assert!(sql.contains("VALUES ($1)"));
570        assert_eq!(params.len(), 1);
571    }
572
573    #[test]
574    fn test_create_many_skip_duplicates() {
575        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
576            .columns(["name", "email"])
577            .row(["Alice", "alice@example.com"])
578            .skip_duplicates();
579
580        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
581
582        assert!(sql.contains("ON CONFLICT DO NOTHING"));
583    }
584
585    #[test]
586    fn test_create_many_without_skip_duplicates() {
587        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
588            .columns(["name"])
589            .row(["Alice"]);
590
591        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
592
593        assert!(!sql.contains("ON CONFLICT"));
594    }
595
596    #[test]
597    fn test_create_many_with_rows() {
598        let rows = vec![
599            vec!["Alice", "alice@test.com"],
600            vec!["Bob", "bob@test.com"],
601            vec!["Charlie", "charlie@test.com"],
602        ];
603        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
604            .columns(["name", "email"])
605            .rows(rows);
606
607        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
608
609        assert!(sql.contains("VALUES ($1, $2), ($3, $4), ($5, $6)"));
610        assert_eq!(params.len(), 6);
611    }
612
613    #[test]
614    fn test_create_many_param_ordering() {
615        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
616            .columns(["a", "b"])
617            .row(["1", "2"])
618            .row(["3", "4"]);
619
620        let (_, params) = op.build_sql(&crate::dialect::Postgres);
621
622        // Params should be ordered: row1.a, row1.b, row2.a, row2.b
623        assert_eq!(params[0], FilterValue::String("1".to_string()));
624        assert_eq!(params[1], FilterValue::String("2".to_string()));
625        assert_eq!(params[2], FilterValue::String("3".to_string()));
626        assert_eq!(params[3], FilterValue::String("4".to_string()));
627    }
628
629    #[tokio::test]
630    async fn test_create_many_exec() {
631        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(3))
632            .columns(["name"])
633            .row(["Alice"])
634            .row(["Bob"])
635            .row(["Charlie"]);
636
637        let result = op.exec().await;
638
639        assert!(result.is_ok());
640        assert_eq!(result.unwrap(), 3);
641    }
642
643    // ========== SQL Structure Tests ==========
644
645    #[test]
646    fn test_create_sql_structure() {
647        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
648            .set("name", "Alice")
649            .select(Select::fields(["id"]));
650
651        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
652
653        let insert_pos = sql.find("INSERT INTO").unwrap();
654        let columns_pos = sql.find("(name)").unwrap();
655        let values_pos = sql.find("VALUES").unwrap();
656        let returning_pos = sql.find("RETURNING").unwrap();
657
658        assert!(insert_pos < columns_pos);
659        assert!(columns_pos < values_pos);
660        assert!(values_pos < returning_pos);
661    }
662
663    #[test]
664    fn test_create_many_sql_structure() {
665        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
666            .columns(["name", "email"])
667            .row(["Alice", "alice@test.com"])
668            .skip_duplicates();
669
670        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
671
672        let insert_pos = sql.find("INSERT INTO").unwrap();
673        let columns_pos = sql.find("(name, email)").unwrap();
674        let values_pos = sql.find("VALUES").unwrap();
675        let conflict_pos = sql.find("ON CONFLICT").unwrap();
676
677        assert!(insert_pos < columns_pos);
678        assert!(columns_pos < values_pos);
679        assert!(values_pos < conflict_pos);
680    }
681
682    #[test]
683    fn test_create_table_name() {
684        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new());
685        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
686
687        assert!(sql.contains("test_models"));
688    }
689
690    // ========== Method Chaining Tests ==========
691
692    #[test]
693    fn test_create_method_chaining() {
694        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
695            .set("name", "Alice")
696            .set("email", "alice@test.com")
697            .select(Select::fields(["id", "name"]));
698
699        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
700
701        assert!(sql.contains("(name, email)"));
702        assert!(sql.contains("VALUES ($1, $2)"));
703        assert!(sql.contains("RETURNING id, name"));
704        assert_eq!(params.len(), 2);
705    }
706
707    #[test]
708    fn test_create_many_method_chaining() {
709        let op = CreateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
710            .columns(["a", "b"])
711            .row(["1", "2"])
712            .row(["3", "4"])
713            .skip_duplicates();
714
715        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
716
717        assert!(sql.contains("ON CONFLICT DO NOTHING"));
718        assert_eq!(params.len(), 4);
719    }
720
721    // ========== Cross-Dialect Tests ==========
722
723    #[test]
724    fn create_mssql_emits_output_inserted() {
725        let op =
726            CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
727        let (sql, _) = op.build_sql(&crate::dialect::Mssql);
728        assert!(
729            sql.contains(" OUTPUT INSERTED.*"),
730            "expected OUTPUT INSERTED.*, got: {sql}"
731        );
732    }
733
734    #[test]
735    fn create_mssql_emits_output_inserted_for_multiple_columns() {
736        // Regression guard: the dialect-level test at
737        // `dialect::tests::returning_mssql_is_output_inserted` verifies the
738        // per-column prefix expansion of `Mssql::returning_clause`, but not
739        // the wiring from the operation builder's `Select` list into that
740        // clause. If a future refactor fails to pass the selected columns
741        // through to the dialect, that path would silently fall back to
742        // `OUTPUT INSERTED.*`. This test pins the end-to-end SQL emitted by
743        // `CreateOperation::build_sql` when a narrow column list is set.
744        let op = CreateOperation::<MockEngine, TestModel>::new(MockEngine::new())
745            .set("name", "Alice")
746            .set("email", "alice@example.com")
747            .select(Select::fields(["id", "email"]));
748
749        let (sql, params) = op.build_sql(&crate::dialect::Mssql);
750        assert!(
751            sql.contains(" OUTPUT INSERTED.id, INSERTED.email"),
752            "expected OUTPUT INSERTED.id, INSERTED.email, got: {sql}"
753        );
754        assert!(
755            !sql.contains("INSERTED.*"),
756            "narrow Select must not fall back to INSERTED.*: {sql}"
757        );
758        assert_eq!(params.len(), 2);
759    }
760
761    #[test]
762    fn create_postgres_emits_returning() {
763        let op =
764            CreateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Alice");
765        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
766        assert!(sql.contains("RETURNING "), "expected RETURNING, got: {sql}");
767    }
768}