1#![forbid(unsafe_code)]
22
23#[cfg(not(any(feature = "postgres", feature = "sqlite", feature = "mysql")))]
24compile_error!(
25 "gatekeep-sqlx requires at least one SQLx backend feature: postgres, sqlite, or mysql"
26);
27
28use std::marker::PhantomData;
29
30use gatekeep::{
31 Condition, Context, FactId, LowerError, Lowered, QueryLowering, ResidualPolicy,
32 ResidualPolicyBranch, ResidualPolicyNode,
33};
34
35mod audit;
36mod fragment;
37
38#[cfg(feature = "dovecote-mysql")]
39pub use audit::MySqlBridgeError;
40#[cfg(feature = "mysql")]
41pub use audit::MySqlDecisionAuditRepository;
42#[cfg(feature = "postgres")]
43pub use audit::PgDecisionAuditRepository;
44#[cfg(feature = "dovecote-postgres")]
45pub use audit::PostgresBridgeError;
46#[cfg(feature = "dovecote-sqlite")]
47pub use audit::SqliteBridgeError;
48#[cfg(feature = "sqlite")]
49pub use audit::SqliteDecisionAuditRepository;
50#[cfg(any(
51 feature = "dovecote-postgres",
52 feature = "dovecote-sqlite",
53 feature = "dovecote-mysql"
54))]
55pub use audit::{
56 BRIDGE_PAYLOAD_CODEC, BRIDGE_PAYLOAD_PROVENANCE_DUAL_WRITE,
57 BRIDGE_PAYLOAD_PROVENANCE_LEGACY_JSON_VALUE, BRIDGE_PAYLOAD_PROVENANCE_LEGACY_TEXT,
58 BridgeConfigError, BridgeEventError, BridgeImportOptions, BridgeImportReport,
59 BridgePublication, BridgeWriteOutcome, DEFAULT_DOVECOTE_STREAM, DovecoteAuditBridge,
60 GATEKEEP_AUDIT_EVENT_TYPE, LegacyOutboxClaim, encode_audit_entry_v1,
61 encode_reconstructed_audit_v1,
62};
63pub use audit::{DecisionAuditRecord, SqlxAuditError, SqlxAuditStore, SqlxDecisionAuditRepository};
64#[cfg(feature = "mysql")]
65pub use fragment::MySqlBackend;
66#[cfg(feature = "sqlite")]
67pub use fragment::SqliteBackend;
68pub use fragment::{
69 GatekeepSqlxBackend, SqlxDriver, SqlxDriverError, SqlxFragment, SqlxValue,
70 infer_enabled_driver_from_url, validate_database_url_for_backend,
71};
72#[cfg(feature = "postgres")]
73pub use fragment::{PgFragment, PgValue, PostgresBackend};
74
75pub trait SqlxFactPredicates<B>
77where
78 B: GatekeepSqlxBackend,
79{
80 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<B>>;
83}
84
85#[cfg(feature = "postgres")]
87pub trait PgFactPredicates {
88 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<PgFragment>;
91}
92
93#[cfg(feature = "postgres")]
94impl<T> SqlxFactPredicates<PostgresBackend> for T
95where
96 T: PgFactPredicates,
97{
98 fn predicate(&self, fact: &FactId, cx: &Context) -> Option<SqlxFragment<PostgresBackend>> {
99 PgFactPredicates::predicate(self, fact, cx)
100 }
101}
102
103pub trait SqlOutcome {
105 fn to_sql_ordinal(&self) -> i64;
107}
108
109impl SqlOutcome for () {
110 fn to_sql_ordinal(&self) -> i64 {
111 0
112 }
113}
114
115pub trait OutcomeProjection<B, O>
117where
118 B: GatekeepSqlxBackend,
119{
120 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError>;
127}
128
129#[derive(Clone, Copy, Debug, Default)]
131pub struct OrdinalProjection;
132
133impl<B, O> OutcomeProjection<B, O> for OrdinalProjection
134where
135 B: GatekeepSqlxBackend,
136 O: SqlOutcome,
137{
138 fn constant(&self, outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
139 Ok(SqlxFragment::bind(outcome.to_sql_ordinal()))
140 }
141}
142
143#[derive(Clone, Copy, Debug, Default)]
145pub struct NoGradeProjection;
146
147impl<B, O> OutcomeProjection<B, O> for NoGradeProjection
148where
149 B: GatekeepSqlxBackend,
150{
151 fn constant(&self, _outcome: &O) -> Result<SqlxFragment<B>, LowerError> {
152 Err(LowerError::NonTotalGrade)
153 }
154}
155
156#[derive(Clone, Debug)]
158pub struct SqlxLowerer<B, P, M = OrdinalProjection> {
159 predicates: P,
160 projection: M,
161 backend: PhantomData<fn() -> B>,
162}
163
164#[cfg(feature = "postgres")]
166pub type PgLowerer<P, M = OrdinalProjection> = SqlxLowerer<PostgresBackend, P, M>;
167
168#[derive(Clone, Debug, PartialEq, Eq)]
169struct SqlxLowered<B> {
170 filter: SqlxFragment<B>,
171 grade: SqlxFragment<B>,
172}
173
174impl<B, P> SqlxLowerer<B, P, OrdinalProjection>
175where
176 B: GatekeepSqlxBackend,
177{
178 #[must_use]
180 pub const fn new(predicates: P) -> Self {
181 Self::with_projection(predicates, OrdinalProjection)
182 }
183}
184
185impl<B, P, M> SqlxLowerer<B, P, M>
186where
187 B: GatekeepSqlxBackend,
188{
189 #[must_use]
191 pub const fn with_projection(predicates: P, projection: M) -> Self {
192 Self {
193 predicates,
194 projection,
195 backend: PhantomData,
196 }
197 }
198
199 pub fn lower_filter<O>(
206 &self,
207 residual: &ResidualPolicy<O>,
208 cx: &Context,
209 ) -> Result<SqlxFragment<B>, LowerError>
210 where
211 P: SqlxFactPredicates<B>,
212 {
213 residual.try_fold_pruned(
214 &mut |branch| match branch {
215 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
216 !fallback.carries_obligation()
217 }
218 },
219 &mut |node| self.lower_filter_node(node, cx),
220 )
221 }
222
223 fn lower_filter_node<O>(
224 &self,
225 node: ResidualPolicyNode<'_, O, SqlxFragment<B>>,
226 cx: &Context,
227 ) -> Result<SqlxFragment<B>, LowerError>
228 where
229 P: SqlxFactPredicates<B>,
230 {
231 match node {
232 ResidualPolicyNode::Permit(_) | ResidualPolicyNode::PermitWithTrace { .. } => {
233 Ok(SqlxFragment::trusted("TRUE"))
234 }
235 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
236 Ok(SqlxFragment::trusted("FALSE"))
237 }
238 ResidualPolicyNode::Grant { condition, .. } => self.lower_condition(condition, cx),
239 ResidualPolicyNode::All { arms, .. } => Ok(fragment_set(arms, " AND ", "FALSE")),
240 ResidualPolicyNode::Any { arms, .. } => Ok(fragment_set(arms, " OR ", "FALSE")),
241 ResidualPolicyNode::OrElse {
242 fallback_policy,
243 primary,
244 fallback,
245 ..
246 } => {
247 if fallback_policy.carries_obligation() {
248 Ok(primary)
249 } else {
250 Ok(match fallback {
251 Some(fallback) => SqlxFragment::binary(" OR ", vec![primary, fallback]),
252 None => primary,
253 })
254 }
255 }
256 }
257 }
258
259 fn lower_condition(
260 &self,
261 condition: &Condition,
262 cx: &Context,
263 ) -> Result<SqlxFragment<B>, LowerError>
264 where
265 P: SqlxFactPredicates<B>,
266 {
267 match condition {
268 Condition::Always => Ok(SqlxFragment::trusted("TRUE")),
269 Condition::Never => Ok(SqlxFragment::trusted("FALSE")),
270 Condition::Has(fact) => self
271 .predicates
272 .predicate(fact, cx)
273 .map(is_true)
274 .ok_or_else(|| LowerError::Unlowerable(fact.clone())),
275 Condition::Not(inner) => Ok(SqlxFragment::unary(
276 "NOT ",
277 self.lower_condition(inner, cx)?,
278 )),
279 Condition::All(conditions) => {
280 lower_condition_set(conditions, " AND ", "FALSE", |item| {
281 self.lower_condition(item, cx)
282 })
283 }
284 Condition::Any(conditions) => {
285 lower_condition_set(conditions, " OR ", "FALSE", |item| {
286 self.lower_condition(item, cx)
287 })
288 }
289 }
290 }
291
292 fn lower_policy<O>(
293 &self,
294 residual: &ResidualPolicy<O>,
295 cx: &Context,
296 ) -> Result<SqlxLowered<B>, LowerError>
297 where
298 P: SqlxFactPredicates<B>,
299 M: OutcomeProjection<B, O>,
300 {
301 residual.try_fold_pruned(
302 &mut |branch| match branch {
303 ResidualPolicyBranch::OrElseFallback { fallback, .. } => {
304 !fallback.carries_obligation()
305 }
306 },
307 &mut |node| self.lower_node(node, cx),
308 )
309 }
310
311 fn lower_node<O>(
312 &self,
313 node: ResidualPolicyNode<'_, O, SqlxLowered<B>>,
314 cx: &Context,
315 ) -> Result<SqlxLowered<B>, LowerError>
316 where
317 P: SqlxFactPredicates<B>,
318 M: OutcomeProjection<B, O>,
319 {
320 match node {
321 ResidualPolicyNode::Permit(outcome)
322 | ResidualPolicyNode::PermitWithTrace { outcome, .. } => Ok(SqlxLowered {
323 filter: SqlxFragment::trusted("TRUE"),
324 grade: self.projection.constant(outcome)?,
325 }),
326 ResidualPolicyNode::Deny | ResidualPolicyNode::DenyWithTrace { .. } => {
327 Ok(SqlxLowered {
328 filter: SqlxFragment::trusted("FALSE"),
329 grade: SqlxFragment::trusted("NULL"),
330 })
331 }
332 ResidualPolicyNode::Grant {
333 outcome, condition, ..
334 } => {
335 let filter = self.lower_condition(condition, cx)?;
336 let outcome = self.projection.constant(outcome)?;
337 Ok(SqlxLowered {
338 filter: filter.clone(),
339 grade: case_when(filter, outcome, SqlxFragment::trusted("NULL")),
340 })
341 }
342 ResidualPolicyNode::All { arms, .. } => {
343 let (filters, grades) = unzip_lowered(arms);
344 Ok(SqlxLowered {
345 filter: fragment_set(filters, " AND ", "FALSE"),
346 grade: grade_set::<B>(grades, B::MIN_FUNCTION),
347 })
348 }
349 ResidualPolicyNode::Any { arms, .. } => {
350 let (filters, grades) = unzip_lowered(arms);
351 Ok(SqlxLowered {
352 filter: fragment_set(filters, " OR ", "FALSE"),
353 grade: grade_set::<B>(grades, B::MAX_FUNCTION),
354 })
355 }
356 ResidualPolicyNode::OrElse {
357 fallback_policy,
358 primary,
359 fallback,
360 ..
361 } => {
362 if fallback_policy.carries_obligation() {
363 return Ok(primary);
364 }
365
366 Ok(match fallback {
367 Some(fallback) => SqlxLowered {
368 filter: SqlxFragment::binary(
369 " OR ",
370 vec![primary.filter.clone(), fallback.filter],
371 ),
372 grade: case_when(primary.filter, primary.grade, fallback.grade),
373 },
374 None => primary,
375 })
376 }
377 }
378 }
379}
380
381impl<O, B, P, M> QueryLowering<O> for SqlxLowerer<B, P, M>
382where
383 B: GatekeepSqlxBackend,
384 P: SqlxFactPredicates<B>,
385 M: OutcomeProjection<B, O>,
386{
387 type Filter = SqlxFragment<B>;
388 type Projection = SqlxFragment<B>;
389
390 fn lower(
391 &self,
392 residual: &ResidualPolicy<O>,
393 cx: &Context,
394 ) -> Result<Lowered<Self::Filter, Self::Projection>, LowerError> {
395 let lowered = self.lower_policy(residual, cx)?;
396 Ok(Lowered {
397 filter: lowered.filter,
398 grade: lowered.grade,
399 })
400 }
401}
402
403fn lower_condition_set<B>(
404 conditions: &[Condition],
405 separator: &str,
406 empty: &str,
407 lower: impl FnMut(&Condition) -> Result<SqlxFragment<B>, LowerError>,
408) -> Result<SqlxFragment<B>, LowerError> {
409 if conditions.is_empty() {
410 return Ok(SqlxFragment::trusted(empty));
411 }
412
413 let fragments = conditions
414 .iter()
415 .map(lower)
416 .collect::<Result<Vec<_>, _>>()?;
417 Ok(SqlxFragment::binary(separator, fragments))
418}
419
420fn fragment_set<B>(
421 fragments: Vec<SqlxFragment<B>>,
422 separator: &str,
423 empty: &str,
424) -> SqlxFragment<B> {
425 if fragments.is_empty() {
426 SqlxFragment::trusted(empty)
427 } else {
428 SqlxFragment::binary(separator, fragments)
429 }
430}
431
432fn grade_set<B>(grades: Vec<SqlxFragment<B>>, function: &str) -> SqlxFragment<B>
433where
434 B: GatekeepSqlxBackend,
435{
436 match grades.len() {
437 0 => SqlxFragment::trusted("NULL"),
438 1 => grades
439 .into_iter()
440 .next()
441 .unwrap_or_else(|| SqlxFragment::trusted("NULL")),
442 _ if B::GRADE_FUNCTION_PROPAGATES_NULL => {
443 let mut iter = grades.into_iter();
444 let mut combined = iter.next().unwrap_or_else(|| SqlxFragment::trusted("NULL"));
445 for grade in iter {
446 combined = null_safe_grade_pair(function, combined, grade);
447 }
448 combined
449 }
450 _ => SqlxFragment::function(function, grades),
451 }
452}
453
454fn null_safe_grade_pair<B>(
455 function: &str,
456 left: SqlxFragment<B>,
457 right: SqlxFragment<B>,
458) -> SqlxFragment<B> {
459 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
460 fragment.push_fragment(left.clone().wrapped());
461 fragment.push_sql(" IS NULL THEN ");
462 fragment.push_fragment(right.clone());
463 fragment.push_sql(" WHEN ");
464 fragment.push_fragment(right.clone().wrapped());
465 fragment.push_sql(" IS NULL THEN ");
466 fragment.push_fragment(left.clone());
467 fragment.push_sql(" ELSE ");
468 fragment.push_fragment(SqlxFragment::function(function, vec![left, right]));
469 fragment.push_sql(" END");
470 fragment
471}
472
473fn unzip_lowered<B>(lowered: Vec<SqlxLowered<B>>) -> (Vec<SqlxFragment<B>>, Vec<SqlxFragment<B>>) {
474 lowered
475 .into_iter()
476 .map(|lowered| (lowered.filter, lowered.grade))
477 .unzip()
478}
479
480fn case_when<B>(
481 condition: SqlxFragment<B>,
482 then_expr: SqlxFragment<B>,
483 else_expr: SqlxFragment<B>,
484) -> SqlxFragment<B> {
485 let mut fragment = SqlxFragment::trusted("CASE WHEN ");
486 fragment.push_fragment(condition);
487 fragment.push_sql(" THEN ");
488 fragment.push_fragment(then_expr);
489 fragment.push_sql(" ELSE ");
490 fragment.push_fragment(else_expr);
491 fragment.push_sql(" END");
492 fragment
493}
494
495fn is_true<B>(predicate: SqlxFragment<B>) -> SqlxFragment<B> {
496 let mut fragment = SqlxFragment::trusted("(");
497 fragment.push_fragment(predicate);
498 fragment.push_sql(") IS TRUE");
499 fragment
500}