Skip to main content

cratestack_sql/values/
sql_value.rs

1use cratestack_core::Value;
2
3use super::decimal_like::DecimalLike;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum SqlValue {
7    Bool(bool),
8    Int(i64),
9    /// A `BigInt` field's value (ADR 0019). Bound as `INT8` on Postgres
10    /// and stored as an SQLite INTEGER, exactly like [`SqlValue::Int`] in
11    /// this release. A variant of its own, rather than reusing `Int`, so
12    /// the next release can narrow `Int` to 32 bits without touching any
13    /// `BigInt` call site, and so a comparison between the two is an
14    /// explicit decision (`cratestack-sqlx` compares them numerically)
15    /// rather than the derived `PartialEq`, which treats them as different.
16    BigInt(i64),
17    Float(f64),
18    String(String),
19    Bytes(Vec<u8>),
20    Uuid(uuid::Uuid),
21    DateTime(chrono::DateTime<chrono::Utc>),
22    Json(Value),
23    /// Holds whichever concrete decimal type the originating schema chose
24    /// (cratestack#505 Direction 2 — `rust_decimal::Decimal`,
25    /// `bigdecimal::BigDecimal`, or any other [`DecimalLike`]
26    /// implementer), boxed rather than a fixed concrete type so two
27    /// schemas that chose different backends can share this one compiled
28    /// `SqlValue` without a Cargo-feature union collision. Unconditional —
29    /// no `#[cfg]` gate, since this variant no longer names a concrete
30    /// backend type at all.
31    Decimal(Box<dyn DecimalLike>),
32    /// A `Vector(n)` field's value (see `docs/design/extensions.md`
33    /// §6). Defined unconditionally — no `pgvector` dependency is
34    /// needed to hold a `Vec<f32>` — but only ever constructed by
35    /// generated code gated on the `pgvector` Cargo feature (#161's
36    /// compile-time check), and only ever bound to a real column by
37    /// `cratestack-sqlx`'s own `pgvector`-gated encode path.
38    Vector(Vec<f32>),
39    /// A `Geography`/`Geometry` field's value as EWKB bytes (see
40    /// `docs/design/extensions.md` §6b and cratestack#842). Defined
41    /// unconditionally — no PostGIS dependency is needed to hold a
42    /// `Vec<u8>` — but only ever constructed by generated code gated on
43    /// the `postgis` Cargo feature, and bound to a real column by
44    /// `cratestack-sqlx`'s own `postgis`-gated encode path.
45    ///
46    /// A distinct variant rather than reusing [`SqlValue::Bytes`] so
47    /// the encode boundary can tell "these bytes are a geometry" from
48    /// "these bytes are a bytea column", which matters for the
49    /// `NULL` arm's type annotation.
50    #[cfg(feature = "postgis")]
51    Spatial(Vec<u8>),
52    NullBool,
53    NullInt,
54    NullBigInt,
55    NullFloat,
56    NullString,
57    NullBytes,
58    NullUuid,
59    NullDateTime,
60    NullJson,
61    NullDecimal,
62    NullVector,
63    #[cfg(feature = "postgis")]
64    NullSpatial,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub enum FilterValue {
69    None,
70    Single(SqlValue),
71    Many(Vec<SqlValue>),
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub struct SqlColumnValue {
76    pub column: &'static str,
77    pub value: SqlValue,
78}
79
80/// Detect the first duplicate value in a list of `SqlValue`s, used for
81/// batch_upsert input deduplication. Linear-scan with `PartialEq` rather
82/// than the hashed variant in `cratestack-core` because `SqlValue::Float`
83/// and `SqlValue::Decimal` don't admit a sound `Hash` impl.
84///
85/// At the documented batch cap (≤ 1000 items) the O(N²) cost is on the
86/// order of a million `PartialEq` comparisons, which dominates nothing
87/// next to a single round-trip to Postgres. Returns `(first_index,
88/// duplicate_index)` on collision, matching `cratestack_core::find_duplicate_position`.
89pub fn find_duplicate_sql_value(values: &[SqlValue]) -> Option<(usize, usize)> {
90    for (index, value) in values.iter().enumerate() {
91        if let Some(earlier) = values[..index].iter().position(|prior| prior == value) {
92            return Some((earlier, index));
93        }
94    }
95    None
96}