1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! Value-shaped helpers: `SqlValue` → bind-slot push, `auth_field`
//! lookup with type narrowing, slice-of-columns scan, and the two
//! equality checks shared by the create-policy evaluator.
use cratestack_core::{CratestackContext, Value};
use crate::{Json, PolicyLiteral, SqlColumnValue, SqlValue, sqlx};
use super::decimal_bind::{bind_decimal, bind_null_decimal};
pub(crate) fn push_bind_value(query: &mut sqlx::QueryBuilder<sqlx::Postgres>, value: &SqlValue) {
// Every arm is a statement (trailing `;`), not a `match`-expression
// value: `bind_decimal`/`bind_null_decimal` can't return `&mut
// QueryBuilder` without a lifetime parameter this free function has no
// clean way to name (two independently-elided input lifetimes, no
// `&self` to anchor on) — discarding each arm's value sidesteps that
// entirely, at the cost of losing `push_bind`'s method-chaining value,
// which nothing here used anyway.
match value {
SqlValue::Bool(value) => {
query.push_bind(*value);
}
SqlValue::Int(value) => {
query.push_bind(*value);
}
SqlValue::Float(value) => {
query.push_bind(*value);
}
SqlValue::String(value) => {
query.push_bind(value.clone());
}
SqlValue::Bytes(value) => {
query.push_bind(value.clone());
}
SqlValue::Uuid(value) => {
query.push_bind(*value);
}
SqlValue::DateTime(value) => {
query.push_bind(*value);
}
SqlValue::Json(value) => {
query.push_bind(Json(value.clone()));
}
// `SqlValue::Decimal` holds a `Box<dyn DecimalLike>` (cratestack#505
// Direction 2), not a fixed concrete type, so this boundary has to
// downcast to whichever concrete backend(s) this crate's own
// `decimal-*` features enabled before it can call `push_bind` — sqlx
// binds a concrete, `Encode`-implementing type, not a trait object.
// See `bind_decimal` below.
SqlValue::Decimal(value) => bind_decimal(query, value.as_ref()),
SqlValue::NullBool => {
query.push_bind(Option::<bool>::None);
}
SqlValue::NullInt => {
query.push_bind(Option::<i64>::None);
}
SqlValue::NullFloat => {
query.push_bind(Option::<f64>::None);
}
SqlValue::NullString => {
query.push_bind(Option::<String>::None);
}
SqlValue::NullBytes => {
query.push_bind(Option::<Vec<u8>>::None);
}
SqlValue::NullUuid => {
query.push_bind(Option::<uuid::Uuid>::None);
}
SqlValue::NullDateTime => {
query.push_bind(Option::<chrono::DateTime<chrono::Utc>>::None);
}
SqlValue::NullJson => {
query.push_bind(Option::<Json<Value>>::None);
}
SqlValue::NullDecimal => bind_null_decimal(query),
#[cfg(feature = "pgvector")]
SqlValue::Vector(value) => {
query.push_bind(pgvector::Vector::from(value.clone()));
}
#[cfg(feature = "pgvector")]
SqlValue::NullVector => {
query.push_bind(Option::<pgvector::Vector>::None);
}
// `Vector(n)`/`pgvector::Vector` requires the `pgvector` Cargo
// feature on this crate. Reaching here without it means an
// `SqlValue::Vector`/`NullVector` was constructed without
// going through cratestack-macros' generated code, which
// itself can't exist unless the matching feature is enabled
// end-to-end (#161's compile-time gate) — an upstream
// invariant violation, not a case to handle gracefully.
#[cfg(not(feature = "pgvector"))]
SqlValue::Vector(_) | SqlValue::NullVector => unreachable!(
"SqlValue::Vector/NullVector requires the `pgvector` Cargo feature on \
cratestack-sqlx"
),
// EWKB bytes bound as `bytea`. PostGIS registers an *implicit*
// cast from `bytea` to both `geography` and `geometry`, so a
// bytea-typed parameter binds straight into a spatial column
// with no `::geography` in the generated SQL — verified against
// postgis/postgis:16-3.4, where
// `PREPARE ins(bytea) AS INSERT INTO t(geog_col) VALUES ($1)`
// prepares cleanly. Decoding is the asymmetric half and goes
// through `crate::spatial::Ewkb`, because on the way *out* the
// column's type OID is geography's, not bytea's.
#[cfg(feature = "postgis")]
SqlValue::Spatial(value) => {
query.push_bind(value.clone());
}
#[cfg(feature = "postgis")]
SqlValue::NullSpatial => {
query.push_bind(Option::<Vec<u8>>::None);
} // No `#[cfg(not(feature = "postgis"))]` counterpart to the
// `Vector` arm above: `SqlValue::Spatial`/`NullSpatial` are
// themselves gated on `postgis` in `cratestack-sql`
// (cratestack#842), so without the feature the variants don't
// exist and there is nothing left to match.
}
}
pub(crate) fn auth_value_to_sql(ctx: &CratestackContext, auth_field: &str) -> Option<SqlValue> {
match ctx.auth_field(auth_field)? {
Value::Bool(value) => Some(SqlValue::Bool(*value)),
Value::Int(value) => Some(SqlValue::Int(*value)),
Value::String(value) => Some(SqlValue::String(value.clone())),
_ => None,
}
}
pub(crate) fn find_column_value<'a>(
values: &'a [SqlColumnValue],
column: &str,
) -> Option<&'a SqlValue> {
values
.iter()
.find(|value| value.column == column)
.map(|value| &value.value)
}
pub(crate) fn sql_value_matches_literal(value: &SqlValue, literal: PolicyLiteral) -> bool {
match (value, literal) {
(SqlValue::Bool(left), PolicyLiteral::Bool(right)) => *left == right,
(SqlValue::Int(left), PolicyLiteral::Int(right)) => *left == right,
(SqlValue::String(left), PolicyLiteral::String(right)) => left == right,
_ => false,
}
}
pub(crate) fn value_matches_auth_literal(value: &Value, literal: PolicyLiteral) -> bool {
match (value, literal) {
(Value::Bool(left), PolicyLiteral::Bool(right)) => *left == right,
(Value::Int(left), PolicyLiteral::Int(right)) => *left == right,
(Value::String(left), PolicyLiteral::String(right)) => left == right,
_ => false,
}
}