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