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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! Parser hardening property suite for subqueries (issue #106).
//!
//! Reuses the `tests/support/parser_hardening` harness from #87 to
//! cover the five subquery shapes called out in the issue:
//! - `WHERE x IN (SELECT …)`
//! - `WHERE EXISTS (SELECT …)`
//! - scalar subqueries `= (SELECT …)`
//! - `FROM (SELECT …) AS sub`
//! - correlated outer/inner references
//!
//! Phase A (#106): tests-only. The only shape the parser already
//! accepts on main is `FROM (SELECT …) AS sub` (`parser/join.rs`).
//! The other four are pinned as `assert_no_panic_on::<SubqueryParser>`
//! today and the snapshot suite (`subquery_snapshots.rs`) records the
//! exact error message a future `Subquery`-AST landing must update.
//! No source mods, no deps bump.
mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_rql::parser::{self, ParseError, ParseErrorKind, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::subquery_adversarial_inputs, subquery_grammar,
HardenedParser,
};
/// `HardenedParser` shim. Subqueries reach the parser via the same
/// top-level `parser::parse` entry point as the rest of the SQL
/// surface, so this is mechanically identical to the SQL / vector
/// shims — what makes it distinct is the property + snapshot suites
/// below, which only feed subquery-shaped inputs.
pub struct SubqueryParser;
impl HardenedParser for SubqueryParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
// ---- panic-safety on adversarial corpus -------------------------
#[test]
fn subquery_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in subquery_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<SubqueryParser>(&input);
}));
if result.is_err() {
panic!("subquery adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
// ---- depth-guard regression (issue #91 SELECT-recursion) --------
/// Deeply nested scalar subqueries must surface
/// `ParseErrorKind::DepthLimit` rather than overflow the Rust stack.
///
/// Counterpart to `dos_limit_chained_not_in_where_does_not_overflow_stack`
/// from `parser/tests.rs` (which covered NOT-recursion). A 10k-NOT
/// payload trips `parse_not_expr`; this test will trip the
/// `parse_atom`/`parse_select_query` reentry that subqueries take —
/// once the AST `Subquery` variant lands and a parenthesised SELECT
/// becomes a valid expression atom.
///
/// Phase A (#106) reality: the scalar-subquery RHS errors with
/// `Syntax` long before the depth guard counts the nested SELECTs,
/// because the inner `SELECT` keyword is not yet a valid atom. The
/// assertion below therefore accepts `Syntax` *or* `DepthLimit` and
/// pins the post-fix expectation in a FIXME comment so the test
/// becomes load-bearing the moment subquery support lands.
///
/// FIXME: bug — fix when AST `Subquery` variant lands (ast.rs L216).
/// Once `(SELECT …)` becomes a valid expression atom, tighten the
/// assertion to `matches!(err.kind, DepthLimit { … })` only — the
/// `Syntax` fallback is a Phase A artefact.
///
/// Runs on a generous-stack thread so an unfixed build (where the
/// depth guard is missing) reports the regression as a panic on the
/// *inner* thread instead of aborting the whole runner.
#[test]
fn dos_limit_deeply_nested_select_subquery_does_not_overflow_stack() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
// N=200: well above ParserLimits::default().max_depth=128
// so the guard *must* fire structurally before any leaf
// expression reaches the Rust stack ceiling. The 10k-NOT
// case in #91 used 10k specifically because each NOT
// pushes one stack frame; SELECT subqueries push more
// frames per level (parse_select → parse_filter →
// parse_expr_prec → parse_atom → parse_select), so 200
// is the equivalent stress point for this slice.
let input = subquery_grammar::nested_scalar_subquery(200);
let err = parser::parse(&input).expect_err("must error, not panic");
assert!(
matches!(
err.kind,
ParseErrorKind::DepthLimit {
limit_name: "max_depth",
..
} | ParseErrorKind::Syntax
),
"expected DepthLimit or Syntax (Phase A), got: {:?}",
err.kind
);
})
.expect("spawn deep-SELECT thread");
handle.join().expect("deep-SELECT thread must not panic");
}
/// Companion test to `dos_limit_deeply_nested_select_subquery_does_not_overflow_stack`
/// using the FROM-prefixed subquery shape — the **only** subquery
/// form parsed today (`parser/join.rs` L23).
///
/// Phase A reality: `parse_select_query_inner` (`parser/table.rs`
/// L130) requires an identifier after the inner `FROM`, so
/// `FROM (SELECT * FROM (SELECT * FROM t) AS a) AS b` hits a Syntax
/// error at the inner `(` long before `parse_select_query`'s depth
/// guard can count the nested subqueries. Once the
/// `parse_select_query_inner` body is widened to accept a
/// FROM-subquery (Fase 1.7 follow-up), the depth guard becomes
/// load-bearing and the assertion below tightens to `DepthLimit`
/// only.
///
/// FIXME: bug — fix when nested FROM-subqueries become supported in
/// `parse_select_query_inner`. Today the assertion accepts
/// `Syntax | DepthLimit`; once the wider acceptance lands the
/// `Syntax` arm should be dropped so this test becomes a hard pin.
#[test]
fn dos_limit_deeply_nested_from_subquery_returns_depth_limit() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let depth = 200usize;
let mut s = String::new();
for _ in 0..depth {
s.push_str("FROM (SELECT * ");
}
s.push_str("FROM t");
for i in 0..depth {
s.push_str(&format!(") AS a{}", i));
}
let err = parser::parse(&s).expect_err("must error, not panic");
assert!(
matches!(
err.kind,
ParseErrorKind::DepthLimit {
limit_name: "max_depth",
..
} | ParseErrorKind::Syntax
),
"expected DepthLimit or Syntax (Phase A), got: {:?}",
err.kind
);
})
.expect("spawn deep-FROM-subquery thread");
handle
.join()
.expect("deep-FROM-subquery thread must not panic");
}
// ---- property tests ---------------------------------------------
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
/// `WHERE x IN (SELECT …)` never panics. Today the parser bails
/// because `parse_in` only accepts a comma-list of expressions
/// (FIXME pinned in `subquery_grammar.rs::where_in_subquery_stmt`).
/// Once the AST `Subquery` variant lands this assertion can be
/// strengthened to `parse(...).is_ok()`.
#[test]
fn proptest_where_in_subquery_no_panic(
s in subquery_grammar::where_in_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
/// `WHERE EXISTS (SELECT …)` never panics. Same FIXME gating as
/// the IN-subquery proptest.
#[test]
fn proptest_where_exists_subquery_no_panic(
s in subquery_grammar::where_exists_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
/// Scalar subqueries `<col> <op> (SELECT …)` never panic. Same
/// FIXME gating as the IN-subquery proptest.
#[test]
fn proptest_scalar_subquery_no_panic(
s in subquery_grammar::scalar_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
/// `FROM (SELECT …) AS sub` parses cleanly. This is the only
/// subquery shape on main today (`parser/join.rs` L23) and the
/// strategy enforces `is_ok()` to catch regressions.
#[test]
fn proptest_from_aliased_subquery_roundtrips(
s in subquery_grammar::from_aliased_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
prop_assert!(
SubqueryParser::parse(&s).is_ok(),
"FROM (SELECT …) AS sub did not parse: {}", s
);
}
/// Correlated subqueries — outer alias referenced from the inner
/// WHERE — never panic. FIXME-gated like the scalar-subquery
/// proptest because the outer `=` operator hits the same hole.
#[test]
fn proptest_correlated_subquery_no_panic(
s in subquery_grammar::correlated_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
/// Arbitrary suffix glued to a subquery-keyword prefix never
/// panics. `Err` is fine.
#[test]
fn proptest_subquery_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("SELECT * FROM t WHERE x IN (".to_string()),
Just("SELECT * FROM t WHERE EXISTS (".to_string()),
Just("SELECT * FROM t WHERE x = (SELECT ".to_string()),
Just("FROM (SELECT ".to_string()),
Just("FROM (SELECT id FROM t) AS ".to_string()),
],
suffix in ".{0,256}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<SubqueryParser>(&s);
}
/// Tighter `max_depth` always refuses a generated nested-SELECT
/// chain. The depth guard is the load-bearing safety net against
/// the SELECT-recursion DoS this slice pins.
#[test]
fn proptest_subquery_depth_limit_enforced(
depth in 40usize..120,
) {
let limits = ParserLimits {
max_depth: 16,
..ParserLimits::default()
};
let input = subquery_grammar::nested_in_subquery(depth);
let r = SubqueryParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "deeply nested subquery must error under tight depth");
}
}