Skip to main content

alibi_sqlx/
additional_fields.rs

1//! Raw configured values staged on actual model columns.
2
3use crate::json_metadata::JsonMetadata;
4use crate::pool::Exec;
5use crate::sql::Sql;
6use crate::value::{ColumnKind, SqlValue, SqlxValue};
7use alibi_core::store::adapter::RawFieldValue;
8use alibi_core::{AuthError, AuthResult, utils::json::JsValue};
9
10/// Called by generated model bindings before any backend affinity conversion.
11///
12/// # Errors
13///
14/// Returns an error if an object or array cannot be serialized as JSON.
15pub fn raw_value(value: &JsValue) -> AuthResult<SqlValue> {
16    Ok(match RawFieldValue::from_js(value)? {
17        RawFieldValue::Null => SqlValue::Text(None),
18        RawFieldValue::Bool(value) => SqlValue::Bool(Some(value)),
19        RawFieldValue::Integer(value) => SqlValue::BigInt(Some(value)),
20        RawFieldValue::Number(value) => SqlValue::Double(Some(value)),
21        RawFieldValue::Text(value) => SqlValue::Text(Some(value)),
22        RawFieldValue::Json(value) => SqlValue::Json(Some(Box::new(value))),
23    })
24}
25
26pub(crate) async fn prepare_string_value(
27    exec: Exec<'_>,
28    value: SqlValue,
29) -> AuthResult<Option<String>> {
30    if let SqlValue::Text(value) | SqlValue::BpChar(value) = value {
31        return Ok(value);
32    }
33    if matches!(&value, SqlValue::Json(_)) {
34        return Err(AuthError::internal(
35            "object cannot bind to a scalar session field",
36        ));
37    }
38    let mut sql = Sql::with(exec.engine(), "SELECT CAST(");
39    sql.bind(value);
40    sql.push(" AS TEXT) AS value");
41    exec.fetch_scalar::<Option<String>>(sql)
42        .await?
43        .ok_or_else(|| AuthError::internal("session TEXT affinity returned no row"))
44}
45
46#[expect(
47    clippy::as_conversions,
48    clippy::cast_possible_truncation,
49    clippy::cast_precision_loss,
50    reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
51)]
52pub(crate) async fn prepare_value(
53    exec: Exec<'_>,
54    kind: ColumnKind,
55    value: SqlValue,
56) -> AuthResult<SqlValue> {
57    let backend = exec.engine();
58    match kind {
59        ColumnKind::Text | ColumnKind::BpChar => {
60            Ok(SqlValue::Text(prepare_string_value(exec, value).await?))
61        }
62        ColumnKind::Json => {
63            let json = match value {
64                SqlValue::Bool(Some(value)) => serde_json::json!(value),
65                SqlValue::BigInt(Some(value)) => serde_json::json!(value),
66                SqlValue::Double(Some(value)) => serde_json::json!(value),
67                SqlValue::Text(Some(value)) => serde_json::json!(value),
68                SqlValue::Json(Some(value)) => *value,
69                _ => serde_json::Value::Null,
70            };
71            Ok(JsonMetadata::for_backend(json, backend)?.into_sql_value())
72        }
73        ColumnKind::Double | ColumnKind::Number => Ok(match value {
74            SqlValue::BigInt(Some(value)) => SqlValue::Double(Some(value as f64)),
75            SqlValue::Text(None) => SqlValue::Double(None),
76            other => other,
77        }),
78        ColumnKind::Float => Ok(match value {
79            SqlValue::BigInt(Some(value)) => SqlValue::Float(Some(value as f32)),
80            SqlValue::Double(Some(value)) => SqlValue::Float(Some(value as f32)),
81            SqlValue::Text(None) => SqlValue::Float(None),
82            other => other,
83        }),
84        ColumnKind::Boolean => Ok(match value {
85            SqlValue::Text(None) => SqlValue::Bool(None),
86            other => other,
87        }),
88        ColumnKind::Int | ColumnKind::BigInt | ColumnKind::Other | ColumnKind::NaiveTimestamp => {
89            Ok(value)
90        }
91    }
92}