vibesql_executor/
limits.rs

1//! Execution limits and safeguards
2//!
3//! This module defines limits to prevent infinite loops, stack overflow, and runaway queries.
4//! These limits follow industry best practices established by SQLite and other production
5//! databases.
6//!
7//! ## Design Philosophy
8//!
9//! These limits serve multiple purposes:
10//! 1. **Safety**: Prevent crashes from stack overflow or infinite recursion
11//! 2. **Performance**: Kill runaway queries that would hang the system
12//! 3. **Debugging**: Provide clear error messages when limits are exceeded
13//! 4. **Compatibility**: Match SQLite's limit philosophy for consistency
14//!
15//! ## Limit Values
16//!
17//! Default values are chosen conservatively to allow legitimate queries while catching
18//! pathological cases. They can be adjusted based on real-world usage patterns.
19
20/// Maximum depth of expression tree evaluation (subqueries, nested expressions)
21///
22/// SQLite uses 1000 for SQLITE_MAX_EXPR_DEPTH
23/// We use a more conservative 200 to prevent stack overflow
24///
25/// This limit is based on Rust's default stack size (typically 2MB on most platforms).
26/// Testing shows that expression depths beyond ~200 can cause stack overflow during
27/// evaluation even with depth tracking, as each recursive call consumes stack space.
28///
29/// This prevents stack overflow from deeply nested:
30/// - Subqueries (IN, EXISTS, scalar subqueries)
31/// - Arithmetic expressions
32/// - Function calls
33/// - Boolean logic (AND/OR chains)
34///
35/// SQLite uses 1000 for SQLITE_MAX_EXPR_DEPTH by default.
36/// We match this value for better compatibility.
37pub const MAX_EXPRESSION_DEPTH: usize = 1000;
38
39/// Maximum number of compound SELECT terms
40///
41/// SQLite uses 500 for SQLITE_MAX_COMPOUND_SELECT
42/// We match this value
43///
44/// This prevents stack overflow from deeply nested UNION/INTERSECT/EXCEPT chains
45pub const MAX_COMPOUND_SELECT: usize = 500;
46
47/// Maximum number of rows to process in a single query execution
48///
49/// This prevents infinite loops in:
50/// - WHERE clause evaluation
51/// - Join operations
52/// - Aggregate processing
53///
54/// Set high enough for legitimate large queries (10 million rows)
55pub const MAX_ROWS_PROCESSED: usize = 10_000_000;
56
57/// Maximum execution time for a single query (in seconds)
58///
59/// This is a soft timeout - checked periodically, not enforced precisely
60///
61/// Set to 300 seconds (5 minutes) to allow complex multi-table joins to complete.
62/// With predicate pushdown optimization (#1122), memory usage is under control (6.48 GB
63/// vs 73+ GB before). Join reordering optimization is now enabled for 3-8 table joins,
64/// which should significantly reduce execution time for complex analytical queries.
65///
66/// If join reordering proves effective, consider reducing this timeout to a lower value
67/// (e.g., 60-120 seconds) to catch runaway queries more quickly.
68pub const MAX_QUERY_EXECUTION_SECONDS: u64 = 300;
69
70/// Maximum number of iterations in a single loop (e.g., WHERE filtering)
71///
72/// This catches infinite loops in iteration logic
73/// Should be higher than MAX_ROWS_PROCESSED to avoid false positives
74pub const MAX_LOOP_ITERATIONS: usize = 50_000_000;
75
76/// Maximum number of iterations for recursive CTEs
77///
78/// SQLite's default is 1000, but it can be increased via sqlite3_limit().
79/// We use a much higher default (1 million) because:
80/// 1. Our recursive CTE implementation is iterative, not stack-recursive
81/// 2. Many legitimate use cases need more than 1000 iterations (e.g., date ranges)
82/// 3. Memory limits already protect against truly runaway queries
83/// 4. This matches SQLite's practical behavior where the limit is configurable
84///
85/// The limit still catches truly infinite loops while allowing legitimate deep recursion.
86pub const MAX_RECURSIVE_CTE_ITERATIONS: usize = 1_000_000;
87
88/// Maximum number of tables in a single join operation
89///
90/// SQLite uses 64 for SQLITE_LIMIT_COMPOUND_SELECT-like join limits
91/// This prevents exponential cost from very large multi-table joins
92/// and catches queries that would likely be impractical anyway.
93///
94/// Error message: "at most 64 tables in a join"
95pub const MAX_TABLES_IN_JOIN: usize = 64;
96
97/// Maximum memory usage per query execution
98///
99/// This prevents:
100/// - Cartesian product explosions
101/// - Exponential intermediate result growth
102/// - System memory exhaustion
103/// - Swapping/OOM kills
104///
105/// Large enough for legitimate queries, small enough to catch pathological cases
106///
107/// WASM targets have more constrained memory (typically 2-4 GB addressable),
108/// so we use smaller limits there to avoid overflow
109#[cfg(target_family = "wasm")]
110pub const MAX_MEMORY_BYTES: usize = 1 * 1024 * 1024 * 1024; // 1 GB for WASM
111
112#[cfg(not(target_family = "wasm"))]
113pub const MAX_MEMORY_BYTES: usize = 10 * 1024 * 1024 * 1024; // 10 GB for native
114
115/// Warning threshold - log when memory exceeds this
116#[cfg(target_family = "wasm")]
117pub const MEMORY_WARNING_BYTES: usize = 512 * 1024 * 1024; // 512 MB for WASM
118
119#[cfg(not(target_family = "wasm"))]
120pub const MEMORY_WARNING_BYTES: usize = 5 * 1024 * 1024 * 1024; // 5 GB for native
121
122#[cfg(test)]
123#[allow(clippy::assertions_on_constants)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_limits_are_reasonable() {
129        // Expression depth should handle realistic queries
130        assert!(MAX_EXPRESSION_DEPTH >= 100, "Expression depth too low");
131        assert!(MAX_EXPRESSION_DEPTH <= 10000, "Expression depth too high");
132
133        // Row limits should handle large but not infinite datasets
134        assert!(MAX_ROWS_PROCESSED >= 1_000_000, "Row limit too low");
135        assert!(MAX_ROWS_PROCESSED <= 1_000_000_000, "Row limit too high");
136
137        // Query timeout should allow complex queries but catch hangs
138        assert!(MAX_QUERY_EXECUTION_SECONDS >= 10, "Timeout too short");
139        assert!(MAX_QUERY_EXECUTION_SECONDS <= 3600, "Timeout too long");
140    }
141
142    #[test]
143    fn test_limits_relationship() {
144        // Loop iterations should be higher than row processing limit
145        assert!(
146            MAX_LOOP_ITERATIONS > MAX_ROWS_PROCESSED,
147            "Loop iterations should exceed row processing limit to avoid false positives"
148        );
149    }
150
151    #[test]
152    fn test_memory_limits_reasonable() {
153        // Memory warning should be lower than hard limit
154        assert!(
155            MEMORY_WARNING_BYTES < MAX_MEMORY_BYTES,
156            "Warning threshold should be less than hard limit"
157        );
158
159        // Memory limit should be substantial but not unlimited
160        assert!(MAX_MEMORY_BYTES >= 1_000_000_000, "Memory limit too low (< 1 GB)");
161        assert!(MAX_MEMORY_BYTES <= 100_000_000_000, "Memory limit too high (> 100 GB)");
162    }
163}