Skip to main content

cratestack_sql/
values.rs

1use cratestack_core::Value;
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum SqlValue {
5    Bool(bool),
6    Int(i64),
7    Float(f64),
8    String(String),
9    Bytes(Vec<u8>),
10    Uuid(uuid::Uuid),
11    DateTime(chrono::DateTime<chrono::Utc>),
12    Json(Value),
13    Decimal(cratestack_core::Decimal),
14    NullBool,
15    NullInt,
16    NullFloat,
17    NullString,
18    NullBytes,
19    NullUuid,
20    NullDateTime,
21    NullJson,
22    NullDecimal,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum FilterValue {
27    None,
28    Single(SqlValue),
29    Many(Vec<SqlValue>),
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct SqlColumnValue {
34    pub column: &'static str,
35    pub value: SqlValue,
36}
37
38pub trait CreateModelInput<M> {
39    fn sql_values(&self) -> Vec<SqlColumnValue>;
40    /// Run schema-derived validators (`@length`, `@email`, `@regex`, ...) on
41    /// the input. Default impl is a no-op for inputs without validators.
42    fn validate(&self) -> Result<(), cratestack_core::CoolError> {
43        Ok(())
44    }
45}
46
47pub trait UpdateModelInput<M> {
48    fn sql_values(&self) -> Vec<SqlColumnValue>;
49    fn validate(&self) -> Result<(), cratestack_core::CoolError> {
50        Ok(())
51    }
52}
53
54/// Input shape for the upsert primitive — `INSERT … ON CONFLICT (<pk>) DO
55/// UPDATE …`. `sql_values()` must include the primary-key column (so the
56/// backend can target the conflict), and `primary_key_value()` exposes the
57/// PK separately so the runtime can issue a `SELECT … FOR UPDATE` before
58/// the upsert to drive `Created` vs. `Updated` event / audit semantics.
59///
60/// Only models with a client-supplied primary key (i.e. `@id` *without*
61/// `@default(...)`) emit this trait impl; models with server-generated PKs
62/// don't get an `.upsert()` builder at all. That's intentional — at v1 the
63/// upsert primitive is PK-conflict only, and a server-generated PK can't be
64/// upserted without the caller supplying one anyway.
65pub trait UpsertModelInput<M>: Send {
66    /// Full set of column→value bindings, *including* the primary key.
67    fn sql_values(&self) -> Vec<SqlColumnValue>;
68
69    /// The primary-key value, used to issue the `SELECT … FOR UPDATE` probe
70    /// inside the upsert transaction. Must match the PK column carried in
71    /// `sql_values()`.
72    fn primary_key_value(&self) -> SqlValue;
73
74    fn validate(&self) -> Result<(), cratestack_core::CoolError> {
75        Ok(())
76    }
77}
78
79pub trait IntoSqlValue {
80    fn into_sql_value(self) -> SqlValue;
81}
82
83impl IntoSqlValue for bool {
84    fn into_sql_value(self) -> SqlValue {
85        SqlValue::Bool(self)
86    }
87}
88
89impl IntoSqlValue for i64 {
90    fn into_sql_value(self) -> SqlValue {
91        SqlValue::Int(self)
92    }
93}
94
95impl IntoSqlValue for f64 {
96    fn into_sql_value(self) -> SqlValue {
97        SqlValue::Float(self)
98    }
99}
100
101impl IntoSqlValue for String {
102    fn into_sql_value(self) -> SqlValue {
103        SqlValue::String(self)
104    }
105}
106
107impl IntoSqlValue for &str {
108    fn into_sql_value(self) -> SqlValue {
109        SqlValue::String(self.to_owned())
110    }
111}
112
113impl IntoSqlValue for uuid::Uuid {
114    fn into_sql_value(self) -> SqlValue {
115        SqlValue::Uuid(self)
116    }
117}
118
119impl IntoSqlValue for chrono::DateTime<chrono::Utc> {
120    fn into_sql_value(self) -> SqlValue {
121        SqlValue::DateTime(self)
122    }
123}
124
125impl IntoSqlValue for Value {
126    fn into_sql_value(self) -> SqlValue {
127        SqlValue::Json(self)
128    }
129}
130
131impl IntoSqlValue for cratestack_core::Decimal {
132    fn into_sql_value(self) -> SqlValue {
133        SqlValue::Decimal(self)
134    }
135}
136
137/// Accessor for a model's primary key. Implemented by the macro on every
138/// generated model struct so the batch operations can pair returned rows
139/// back to the position of their input PK in the request, producing a
140/// `BatchItemResult` with the right `index` and a `NotFound` entry for any
141/// requested PK that didn't come back.
142pub trait ModelPrimaryKey<PK> {
143    fn primary_key(&self) -> PK;
144}
145
146/// Detect the first duplicate value in a list of `SqlValue`s, used for
147/// batch_upsert input deduplication. Linear-scan with `PartialEq` rather
148/// than the hashed variant in `cratestack-core` because `SqlValue::Float`
149/// and `SqlValue::Decimal` don't admit a sound `Hash` impl.
150///
151/// At the documented batch cap (≤ 1000 items) the O(N²) cost is on the
152/// order of a million `PartialEq` comparisons, which dominates nothing
153/// next to a single round-trip to Postgres. Returns `(first_index,
154/// duplicate_index)` on collision, matching `cratestack_core::find_duplicate_position`.
155pub fn find_duplicate_sql_value(values: &[SqlValue]) -> Option<(usize, usize)> {
156    for (index, value) in values.iter().enumerate() {
157        if let Some(earlier) = values[..index].iter().position(|prior| prior == value) {
158            return Some((earlier, index));
159        }
160    }
161    None
162}