Skip to content

Commit 2a74984

Browse files
committed
test: add integration tests for parser and planner correctness
Cover the bug-fixes landed in this series: - sql_arithmetic_overflow: checked arithmetic in const-fold and eval - sql_default_expressions: DEFAULT parsing and const-fold fallback - sql_join_correctness: non-equi join predicates, NATURAL JOIN error - sql_parser_string_handling: escaped single-quote infinite-loop fix - sql_procedure_cache_safety: plan-cache hash-collision eviction - sql_recursive_cte: join-link tree-traversal correctness - sql_rls_predicate_parse: UTF-8 safe NEW. rewriting in RLS/DDL parsers - sql_trigger_fuel: trigger budget cap prevents unbounded execution - sql_utf8_expressions: multi-byte identifiers in constraint expressions
1 parent ed2058c commit 2a74984

10 files changed

Lines changed: 1627 additions & 2 deletions

nodedb/tests/procedure_execution.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,10 @@ fn execution_budget_exhaustion() {
390390
}
391391

392392
#[test]
393-
fn execution_budget_unlimited() {
393+
fn execution_budget_trigger_default() {
394394
use nodedb::control::planner::procedural::executor::fuel::ExecutionBudget;
395395

396-
let mut budget = ExecutionBudget::unlimited();
396+
let mut budget = ExecutionBudget::trigger_default();
397397
for _ in 0..1000 {
398398
assert!(budget.consume_iteration().is_ok());
399399
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! Integration coverage for integer overflow handling in SQL expressions.
2+
//!
3+
//! The const-folder and procedural executor must detect integer overflow
4+
//! and return an error rather than panicking (debug) or silently wrapping
5+
//! (release). Float divide-by-zero must return an error, not ±Inf.
6+
7+
mod common;
8+
9+
use common::pgwire_harness::TestServer;
10+
11+
// ---------------------------------------------------------------------------
12+
// Const-folder overflow (nodedb-sql planner)
13+
// ---------------------------------------------------------------------------
14+
15+
/// `i64::MAX + 1` in a constant expression must not panic (debug) or wrap
16+
/// to a negative number (release). An error or null are both acceptable.
17+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
18+
async fn const_fold_addition_overflow_does_not_wrap() {
19+
let server = TestServer::start().await;
20+
21+
let result = server.query_text("SELECT 9223372036854775807 + 1").await;
22+
23+
match result {
24+
Err(_) => { /* error is acceptable */ }
25+
Ok(rows) => {
26+
if let Some(val) = rows.first() {
27+
assert!(
28+
!val.contains("-9223372036854775808"),
29+
"i64::MAX + 1 must not silently wrap to i64::MIN: got {val}"
30+
);
31+
}
32+
}
33+
}
34+
}
35+
36+
/// `i64::MAX * 2` must not panic or wrap.
37+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
38+
async fn const_fold_multiplication_overflow_does_not_wrap() {
39+
let server = TestServer::start().await;
40+
41+
let result = server.query_text("SELECT 9223372036854775807 * 2").await;
42+
43+
match result {
44+
Err(_) => { /* error is acceptable */ }
45+
Ok(rows) => {
46+
if let Some(val) = rows.first() {
47+
// Wrapped value would be -2.
48+
assert!(
49+
!val.contains("\"-2\""),
50+
"i64::MAX * 2 must not silently wrap: got {val}"
51+
);
52+
}
53+
}
54+
}
55+
}
56+
57+
/// `i64::MIN - 1` must not panic or wrap.
58+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
59+
async fn const_fold_subtraction_overflow_does_not_wrap() {
60+
let server = TestServer::start().await;
61+
62+
let result = server.query_text("SELECT -9223372036854775808 - 1").await;
63+
64+
match result {
65+
Err(_) => { /* error is acceptable */ }
66+
Ok(rows) => {
67+
if let Some(val) = rows.first() {
68+
// Wrapped value would be i64::MAX = 9223372036854775807.
69+
assert!(
70+
!val.contains("9223372036854775807"),
71+
"i64::MIN - 1 must not silently wrap to i64::MAX: got {val}"
72+
);
73+
}
74+
}
75+
}
76+
}
77+
78+
// ---------------------------------------------------------------------------
79+
// Const-folder in INSERT context
80+
// ---------------------------------------------------------------------------
81+
82+
/// Overflow in an INSERT VALUES expression should be caught before storage.
83+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
84+
async fn const_fold_overflow_in_insert_values() {
85+
let server = TestServer::start().await;
86+
87+
server
88+
.exec(
89+
"CREATE COLLECTION overflow_tbl TYPE DOCUMENT STRICT (\
90+
id TEXT PRIMARY KEY, \
91+
v BIGINT)",
92+
)
93+
.await
94+
.unwrap();
95+
96+
let result = server
97+
.exec("INSERT INTO overflow_tbl (id, v) VALUES ('k', 9223372036854775807 * 2)")
98+
.await;
99+
100+
assert!(
101+
result.is_err(),
102+
"INSERT with overflowing constant should fail, not store wrapped value"
103+
);
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// Procedural executor overflow (triggers / DO blocks)
108+
// ---------------------------------------------------------------------------
109+
110+
/// Integer overflow in a DO block's variable arithmetic should error, not
111+
/// panic or silently wrap.
112+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
113+
async fn procedural_eval_integer_overflow_returns_error() {
114+
let server = TestServer::start().await;
115+
116+
server
117+
.exec("CREATE COLLECTION proc_ov TYPE DOCUMENT STRICT (id TEXT PRIMARY KEY, v BIGINT)")
118+
.await
119+
.unwrap();
120+
121+
// A DO block (or procedure) that overflows during evaluation.
122+
let result = server
123+
.exec(
124+
"DO $$ \
125+
DECLARE x BIGINT := 9223372036854775807; \
126+
BEGIN \
127+
x := x + 1; \
128+
INSERT INTO proc_ov (id, v) VALUES ('k', x); \
129+
END $$",
130+
)
131+
.await;
132+
133+
assert!(
134+
result.is_err(),
135+
"integer overflow in procedural block should error, not wrap"
136+
);
137+
}
138+
139+
/// `i64::MIN / -1` is undefined behavior in two's complement and must error.
140+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
141+
async fn procedural_eval_min_div_neg1_returns_error() {
142+
let server = TestServer::start().await;
143+
144+
let result = server
145+
.exec(
146+
"DO $$ \
147+
DECLARE x BIGINT := -9223372036854775808; \
148+
DECLARE y BIGINT; \
149+
BEGIN \
150+
y := x / -1; \
151+
END $$",
152+
)
153+
.await;
154+
155+
assert!(
156+
result.is_err(),
157+
"i64::MIN / -1 in procedural block should error, not panic"
158+
);
159+
}
160+
161+
/// Float divide by negative zero should return an error or NULL, not ±Inf.
162+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
163+
async fn procedural_eval_float_div_neg_zero() {
164+
let server = TestServer::start().await;
165+
166+
server
167+
.exec("CREATE COLLECTION fdiv TYPE DOCUMENT STRICT (id TEXT PRIMARY KEY, v FLOAT)")
168+
.await
169+
.unwrap();
170+
171+
// -0.0 passes the `!= 0.0` guard in f64 comparison but produces -inf.
172+
let result = server
173+
.exec(
174+
"DO $$ \
175+
DECLARE a FLOAT := 1.0; \
176+
DECLARE b FLOAT := -0.0; \
177+
DECLARE c FLOAT; \
178+
BEGIN \
179+
c := a / b; \
180+
INSERT INTO fdiv (id, v) VALUES ('k', c); \
181+
END $$",
182+
)
183+
.await;
184+
185+
// Either error, or if it succeeds, the stored value must not be infinity.
186+
if result.is_ok() {
187+
let rows = server
188+
.query_text("SELECT v FROM fdiv WHERE id = 'k'")
189+
.await
190+
.unwrap();
191+
if let Some(val) = rows.first() {
192+
assert!(
193+
!val.to_lowercase().contains("inf"),
194+
"float / -0.0 should not produce infinity: got {val}"
195+
);
196+
}
197+
}
198+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//! Integration coverage for DEFAULT expression evaluation in INSERT.
2+
//!
3+
//! The planner's `evaluate_default_expr` recognizes only a fixed keyword list
4+
//! (UUID_V7, NOW(), NANOID, literals). Any other expression returns None,
5+
//! causing the column to be silently omitted. These tests verify that
6+
//! expression-based defaults are evaluated, not dropped.
7+
8+
mod common;
9+
10+
use common::pgwire_harness::TestServer;
11+
12+
/// `DEFAULT upper('x')` — a scalar function call as a default value.
13+
/// The planner should evaluate this rather than dropping the column.
14+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
15+
async fn default_scalar_function_upper() {
16+
let server = TestServer::start().await;
17+
18+
server
19+
.exec(
20+
"CREATE COLLECTION def_fn TYPE DOCUMENT STRICT (\
21+
id TEXT PRIMARY KEY, \
22+
a TEXT DEFAULT upper('x'))",
23+
)
24+
.await
25+
.unwrap();
26+
27+
server
28+
.exec("INSERT INTO def_fn (id) VALUES ('k1')")
29+
.await
30+
.unwrap();
31+
32+
let rows = server
33+
.query_text("SELECT a FROM def_fn WHERE id = 'k1'")
34+
.await
35+
.unwrap();
36+
assert_eq!(rows.len(), 1, "row should exist");
37+
// The default should produce 'X'. If the column was silently dropped,
38+
// the value will be null/absent.
39+
assert!(
40+
rows[0].contains('X'),
41+
"DEFAULT upper('x') should produce 'X', got {:?}",
42+
rows[0]
43+
);
44+
}
45+
46+
/// `DEFAULT lower('HELLO')` — another scalar function.
47+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
48+
async fn default_scalar_function_lower() {
49+
let server = TestServer::start().await;
50+
51+
server
52+
.exec(
53+
"CREATE COLLECTION def_lower TYPE DOCUMENT STRICT (\
54+
id TEXT PRIMARY KEY, \
55+
tag TEXT DEFAULT lower('HELLO'))",
56+
)
57+
.await
58+
.unwrap();
59+
60+
server
61+
.exec("INSERT INTO def_lower (id) VALUES ('k1')")
62+
.await
63+
.unwrap();
64+
65+
let rows = server
66+
.query_text("SELECT tag FROM def_lower WHERE id = 'k1'")
67+
.await
68+
.unwrap();
69+
assert_eq!(rows.len(), 1);
70+
assert!(
71+
rows[0].contains("hello"),
72+
"DEFAULT lower('HELLO') should produce 'hello', got {:?}",
73+
rows[0]
74+
);
75+
}
76+
77+
/// `DEFAULT 1 + 2` — a binary arithmetic expression as default.
78+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
79+
async fn default_arithmetic_expression() {
80+
let server = TestServer::start().await;
81+
82+
server
83+
.exec(
84+
"CREATE COLLECTION def_arith TYPE DOCUMENT STRICT (\
85+
id TEXT PRIMARY KEY, \
86+
v INT DEFAULT 1 + 2)",
87+
)
88+
.await
89+
.unwrap();
90+
91+
server
92+
.exec("INSERT INTO def_arith (id) VALUES ('k1')")
93+
.await
94+
.unwrap();
95+
96+
let rows = server
97+
.query_text("SELECT v FROM def_arith WHERE id = 'k1'")
98+
.await
99+
.unwrap();
100+
assert_eq!(rows.len(), 1);
101+
assert!(
102+
rows[0].contains('3'),
103+
"DEFAULT 1 + 2 should produce 3, got {:?}",
104+
rows[0]
105+
);
106+
}
107+
108+
/// `DEFAULT concat('a', 'b')` — a multi-arg function.
109+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
110+
async fn default_concat_function() {
111+
let server = TestServer::start().await;
112+
113+
server
114+
.exec(
115+
"CREATE COLLECTION def_concat TYPE DOCUMENT STRICT (\
116+
id TEXT PRIMARY KEY, \
117+
label TEXT DEFAULT concat('hello', '_', 'world'))",
118+
)
119+
.await
120+
.unwrap();
121+
122+
server
123+
.exec("INSERT INTO def_concat (id) VALUES ('k1')")
124+
.await
125+
.unwrap();
126+
127+
let rows = server
128+
.query_text("SELECT label FROM def_concat WHERE id = 'k1'")
129+
.await
130+
.unwrap();
131+
assert_eq!(rows.len(), 1);
132+
assert!(
133+
rows[0].contains("hello_world"),
134+
"DEFAULT concat should produce 'hello_world', got {:?}",
135+
rows[0]
136+
);
137+
}
138+
139+
/// Verify that recognized defaults (literal string, NOW(), UUID_V7) still work.
140+
/// This is a baseline — not a new bug, just ensures we don't regress.
141+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
142+
async fn default_recognized_expressions_still_work() {
143+
let server = TestServer::start().await;
144+
145+
server
146+
.exec(
147+
"CREATE COLLECTION def_known TYPE DOCUMENT STRICT (\
148+
id TEXT PRIMARY KEY, \
149+
status TEXT DEFAULT 'active', \
150+
uid TEXT DEFAULT UUID_V7)",
151+
)
152+
.await
153+
.unwrap();
154+
155+
server
156+
.exec("INSERT INTO def_known (id) VALUES ('k1')")
157+
.await
158+
.unwrap();
159+
160+
let rows = server
161+
.query_text("SELECT status FROM def_known WHERE id = 'k1'")
162+
.await
163+
.unwrap();
164+
assert_eq!(rows.len(), 1);
165+
assert!(
166+
rows[0].contains("active"),
167+
"DEFAULT 'active' should work: got {:?}",
168+
rows[0]
169+
);
170+
}

0 commit comments

Comments
 (0)