Skip to main content

palimpsest_sql/
limits.rs

1// Copyright 2026 Thousand Birds Inc.
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Resource bounds applied to inbound SQL.
5//!
6//! v1 enforces two limits, both at parse/lower time:
7//!
8//! * `max_input_bytes` — how big the SQL string can be. Stops a runaway
9//!   client from forcing the parser to chew through megabytes of text.
10//! * `max_mir_nodes` — how big the lowered MIR can be. Stops cleverly
11//!   short queries (deep set-op chains, big CTE webs) from expanding
12//!   into a graph the planner has to walk N² over.
13//!
14//! Both are advisory: callers explicitly invoke
15//! [`enforce_input_size`] / [`enforce_graph_size`] (or use the
16//! `*_with_limits` helpers in [`lower`](crate::lower)). The default
17//! limits are deliberately generous enough for the conformance suite to
18//! pass unmodified.
19
20use crate::SqlError;
21
22/// Resource bounds applied to inbound SQL, surfaced to the gRPC layer
23/// so it can refuse oversized queries before parsing.
24#[derive(Debug, Clone, Copy)]
25pub struct QueryLimits {
26    /// Maximum byte length of the SQL input.
27    pub max_input_bytes: usize,
28    /// Maximum node count in the lowered MIR.
29    pub max_mir_nodes: usize,
30}
31
32impl QueryLimits {
33    /// Default budget: 64 KiB of SQL, 256 MIR nodes. Set generously
34    /// enough that real-world dashboards do not bump into them.
35    pub const DEFAULT: Self = Self {
36        max_input_bytes: 64 * 1024,
37        max_mir_nodes: 256,
38    };
39}
40
41impl Default for QueryLimits {
42    fn default() -> Self {
43        Self::DEFAULT
44    }
45}
46
47/// Returns [`SqlError::QueryTooLarge`] when `sql.len()` exceeds
48/// `limits.max_input_bytes`.
49///
50/// # Errors
51/// As above.
52pub const fn enforce_input_size(sql: &str, limits: QueryLimits) -> Result<(), SqlError> {
53    let len = sql.len();
54    if len > limits.max_input_bytes {
55        Err(SqlError::QueryTooLarge {
56            len,
57            limit: limits.max_input_bytes,
58        })
59    } else {
60        Ok(())
61    }
62}
63
64/// Returns [`SqlError::QueryTooComplex`] when `nodes` exceeds
65/// `limits.max_mir_nodes`.
66///
67/// # Errors
68/// As above.
69pub const fn enforce_graph_size(nodes: usize, limits: QueryLimits) -> Result<(), SqlError> {
70    if nodes > limits.max_mir_nodes {
71        Err(SqlError::QueryTooComplex {
72            nodes,
73            limit: limits.max_mir_nodes,
74        })
75    } else {
76        Ok(())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{enforce_graph_size, enforce_input_size, QueryLimits};
83    use crate::SqlError;
84
85    #[test]
86    fn input_at_limit_passes() {
87        let limits = QueryLimits {
88            max_input_bytes: 4,
89            max_mir_nodes: 8,
90        };
91        assert!(enforce_input_size("abcd", limits).is_ok());
92    }
93
94    #[test]
95    fn input_above_limit_rejects() {
96        let limits = QueryLimits {
97            max_input_bytes: 3,
98            max_mir_nodes: 8,
99        };
100        match enforce_input_size("abcd", limits) {
101            Err(SqlError::QueryTooLarge { len: 4, limit: 3 }) => {}
102            other => panic!("expected QueryTooLarge, got {other:?}"),
103        }
104    }
105
106    #[test]
107    fn graph_above_limit_rejects() {
108        let limits = QueryLimits {
109            max_input_bytes: 1024,
110            max_mir_nodes: 5,
111        };
112        match enforce_graph_size(10, limits) {
113            Err(SqlError::QueryTooComplex {
114                nodes: 10,
115                limit: 5,
116            }) => {}
117            other => panic!("expected QueryTooComplex, got {other:?}"),
118        }
119    }
120}