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)
34pub const MAX_EXPRESSION_DEPTH: usize = 200;
35
36/// Maximum number of compound SELECT terms
37///
38/// SQLite uses 500 for SQLITE_MAX_COMPOUND_SELECT
39/// We match this value
40///
41/// This prevents stack overflow from deeply nested UNION/INTERSECT/EXCEPT chains
42pub const MAX_COMPOUND_SELECT: usize = 500;
43
44/// Maximum number of rows to process in a single query execution
45///
46/// This prevents infinite loops in:
47/// - WHERE clause evaluation
48/// - Join operations
49/// - Aggregate processing
50///
51/// Set high enough for legitimate large queries (10 million rows)
52pub const MAX_ROWS_PROCESSED: usize = 10_000_000;
53
54/// Maximum execution time for a single query (in seconds)
55///
56/// This is a soft timeout - checked periodically, not enforced precisely
57///
58/// Set to 300 seconds (5 minutes) to allow complex multi-table joins to complete.
59/// With predicate pushdown optimization (#1122), memory usage is under control (6.48 GB
60/// vs 73+ GB before). Join reordering optimization is now enabled for 3-8 table joins,
61/// which should significantly reduce execution time for complex analytical queries.
62///
63/// If join reordering proves effective, consider reducing this timeout to a lower value
64/// (e.g., 60-120 seconds) to catch runaway queries more quickly.
65pub const MAX_QUERY_EXECUTION_SECONDS: u64 = 300;
66
67/// Maximum number of iterations in a single loop (e.g., WHERE filtering)
68///
69/// This catches infinite loops in iteration logic
70/// Should be higher than MAX_ROWS_PROCESSED to avoid false positives
71pub const MAX_LOOP_ITERATIONS: usize = 50_000_000;
72
73/// Maximum memory usage per query execution
74///
75/// This prevents:
76/// - Cartesian product explosions
77/// - Exponential intermediate result growth
78/// - System memory exhaustion
79/// - Swapping/OOM kills
80///
81/// Large enough for legitimate queries, small enough to catch pathological cases
82///
83/// WASM targets have more constrained memory (typically 2-4 GB addressable),
84/// so we use smaller limits there to avoid overflow
85#[cfg(target_family = "wasm")]
86pub const MAX_MEMORY_BYTES: usize = 1 * 1024 * 1024 * 1024; // 1 GB for WASM
87
88#[cfg(not(target_family = "wasm"))]
89pub const MAX_MEMORY_BYTES: usize = 10 * 1024 * 1024 * 1024; // 10 GB for native
90
91/// Warning threshold - log when memory exceeds this
92#[cfg(target_family = "wasm")]
93pub const MEMORY_WARNING_BYTES: usize = 512 * 1024 * 1024; // 512 MB for WASM
94
95#[cfg(not(target_family = "wasm"))]
96pub const MEMORY_WARNING_BYTES: usize = 5 * 1024 * 1024 * 1024; // 5 GB for native
97
98#[cfg(test)]
99#[allow(clippy::assertions_on_constants)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_limits_are_reasonable() {
105 // Expression depth should handle realistic queries
106 assert!(MAX_EXPRESSION_DEPTH >= 100, "Expression depth too low");
107 assert!(MAX_EXPRESSION_DEPTH <= 10000, "Expression depth too high");
108
109 // Row limits should handle large but not infinite datasets
110 assert!(MAX_ROWS_PROCESSED >= 1_000_000, "Row limit too low");
111 assert!(MAX_ROWS_PROCESSED <= 1_000_000_000, "Row limit too high");
112
113 // Query timeout should allow complex queries but catch hangs
114 assert!(MAX_QUERY_EXECUTION_SECONDS >= 10, "Timeout too short");
115 assert!(MAX_QUERY_EXECUTION_SECONDS <= 3600, "Timeout too long");
116 }
117
118 #[test]
119 fn test_limits_relationship() {
120 // Loop iterations should be higher than row processing limit
121 assert!(
122 MAX_LOOP_ITERATIONS > MAX_ROWS_PROCESSED,
123 "Loop iterations should exceed row processing limit to avoid false positives"
124 );
125 }
126
127 #[test]
128 fn test_memory_limits_reasonable() {
129 // Memory warning should be lower than hard limit
130 assert!(
131 MEMORY_WARNING_BYTES < MAX_MEMORY_BYTES,
132 "Warning threshold should be less than hard limit"
133 );
134
135 // Memory limit should be substantial but not unlimited
136 assert!(MAX_MEMORY_BYTES >= 1_000_000_000, "Memory limit too low (< 1 GB)");
137 assert!(MAX_MEMORY_BYTES <= 100_000_000_000, "Memory limit too high (> 100 GB)");
138 }
139}