Skip to main content

cratestack_sqlx/query/write/
create_exec.rs

1//! Generic-over-Executor create helper used by both the pool and
2//! transaction paths in [`super::create`]. Validates, applies
3//! auth-defaults, seeds `@version`, evaluates create policies, then
4//! runs `INSERT ... RETURNING`.
5
6use cratestack_core::{CoolContext, CoolError};
7
8use crate::query::support::{
9    apply_create_defaults, evaluate_create_policies, find_column_value, push_bind_value,
10};
11use crate::{CreateModelInput, ModelDescriptor, sqlx};
12
13pub async fn create_record_with_executor<'e, E, M, PK, I>(
14    executor: E,
15    policy_pool: &sqlx::PgPool,
16    descriptor: &'static ModelDescriptor<M, PK>,
17    input: I,
18    ctx: &CoolContext,
19) -> Result<M, CoolError>
20where
21    E: sqlx::Executor<'e, Database = sqlx::Postgres>,
22    I: CreateModelInput<M>,
23    for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
24{
25    input.validate()?;
26    let mut values = apply_create_defaults(input.sql_values(), descriptor.create_defaults, ctx)?;
27    // Seed the optimistic-lock column server-side. `@version` is
28    // excluded from the generated Create input so clients can't pick
29    // the initial value, and the column has no SQL `DEFAULT`. Done
30    // after `apply_create_defaults` so `@default`-driven overrides
31    // still win if a schema ever lands one.
32    if let Some(version_col) = descriptor.version_column
33        && find_column_value(&values, version_col).is_none()
34    {
35        values.push(crate::SqlColumnValue {
36            column: version_col,
37            value: crate::SqlValue::Int(0),
38        });
39    }
40    if values.is_empty() {
41        return Err(CoolError::Validation(
42            "create input must contain at least one column".to_owned(),
43        ));
44    }
45    if !evaluate_create_policies(
46        policy_pool,
47        descriptor.create_allow_policies,
48        descriptor.create_deny_policies,
49        &values,
50        ctx,
51    )
52    .await?
53    {
54        return Err(CoolError::Forbidden(
55            "create policy denied this operation".to_owned(),
56        ));
57    }
58
59    insert_returning_record(executor, descriptor, &values).await
60}
61
62async fn insert_returning_record<'e, E, M, PK>(
63    executor: E,
64    descriptor: &'static ModelDescriptor<M, PK>,
65    values: &[crate::SqlColumnValue],
66) -> Result<M, CoolError>
67where
68    E: sqlx::Executor<'e, Database = sqlx::Postgres>,
69    for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
70{
71    let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("INSERT INTO ");
72    query.push(descriptor.table_name).push(" (");
73    for (index, value) in values.iter().enumerate() {
74        if index > 0 {
75            query.push(", ");
76        }
77        query.push(value.column);
78    }
79    query.push(") VALUES (");
80    for (index, value) in values.iter().enumerate() {
81        if index > 0 {
82            query.push(", ");
83        }
84        push_bind_value(&mut query, &value.value);
85    }
86    query
87        .push(") RETURNING ")
88        .push(descriptor.select_projection());
89
90    query
91        .build_query_as::<M>()
92        .fetch_one(executor)
93        .await
94        .map_err(|error| CoolError::Database(error.to_string()))
95}