Skip to content

Commit 09c0966

Browse files
committed
fix(pgwire): preserve procedural BEGIN...END blocks during SQL splitting
The SQL statement splitter treated semicolons inside procedural function bodies as statement separators, breaking CREATE FUNCTION with IF/ELSIF control flow. Detect DDL containing a BEGIN...END block and return it as a single statement without splitting on internal semicolons. Add tests covering: - Procedural body with IF/ELSIF/END IF not split on semicolons - Trailing semicolon after END stripped correctly - Procedural body parse roundtrip via parse_create_function - DML rejection validation inside function bodies
1 parent 4ce9a54 commit 09c0966

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

nodedb/src/control/server/pgwire/ddl/function/create.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,49 @@ mod tests {
395395
let sql = "CREATE FUNCTION f(x INT) RETURNS INT AS";
396396
assert!(parse_create_function(sql).is_err());
397397
}
398+
399+
#[test]
400+
fn parse_procedural_body() {
401+
let sql = "CREATE FUNCTION classify(score INT) RETURNS TEXT AS \
402+
BEGIN \
403+
IF score > 90 THEN RETURN 'excellent'; \
404+
ELSIF score > 70 THEN RETURN 'good'; \
405+
ELSE RETURN 'needs improvement'; \
406+
END IF; \
407+
END";
408+
let parsed = parse_create_function(sql).unwrap();
409+
assert_eq!(parsed.name, "classify");
410+
assert!(parsed.body_sql.starts_with("BEGIN"));
411+
412+
// Verify the procedural parser can handle the body.
413+
use crate::control::planner::procedural::ast::BodyKind;
414+
assert!(matches!(
415+
BodyKind::detect(&parsed.body_sql),
416+
BodyKind::Procedural
417+
));
418+
let block = crate::control::planner::procedural::parse_block(&parsed.body_sql);
419+
assert!(block.is_ok(), "procedural parse failed: {:?}", block.err());
420+
}
421+
422+
#[test]
423+
fn parse_dml_in_procedural_body() {
424+
let sql = "CREATE FUNCTION bad_func(x INT) RETURNS INT AS \
425+
BEGIN INSERT INTO t (id) VALUES (x); RETURN x; END";
426+
let parsed = parse_create_function(sql).unwrap();
427+
428+
use crate::control::planner::procedural::ast::BodyKind;
429+
assert!(matches!(
430+
BodyKind::detect(&parsed.body_sql),
431+
BodyKind::Procedural
432+
));
433+
let block = crate::control::planner::procedural::parse_block(&parsed.body_sql).unwrap();
434+
435+
let result = crate::control::planner::procedural::validate_function_block(&block);
436+
assert!(result.is_err(), "should reject DML: {:?}", result);
437+
let err_msg = format!("{}", result.unwrap_err());
438+
assert!(
439+
err_msg.contains("DML"),
440+
"error should mention DML, got: {err_msg}"
441+
);
442+
}
398443
}

nodedb/src/control/server/pgwire/handler/sql_exec.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,27 @@ impl NodeDbPgHandler {
441441
/// query message (e.g. heredoc input), ensuring `parts[2]` in DDL handlers
442442
/// never contains a trailing semicolon.
443443
pub(super) fn split_sql_statements(sql: &str) -> Vec<String> {
444+
// Procedural function bodies (AS BEGIN ... END) contain semicolons that
445+
// are NOT statement separators. Detect and return as single statement.
446+
let upper = sql.to_uppercase();
447+
// Detect procedural blocks: any DDL containing a BEGIN...END body.
448+
// Standalone "BEGIN" (transaction) is handled separately before split.
449+
let has_procedural_block = (upper.contains(" BEGIN ") || upper.contains(" BEGIN\n"))
450+
&& upper.trim() != "BEGIN"
451+
&& !upper.starts_with("BEGIN");
452+
if has_procedural_block {
453+
let trimmed = sql.trim().trim_end_matches(';').trim().to_string();
454+
if trimmed.is_empty() {
455+
return Vec::new();
456+
}
457+
return vec![trimmed];
458+
}
459+
444460
let mut stmts = Vec::new();
445461
let mut current = String::new();
446462
let mut chars = sql.chars().peekable();
447463
let mut in_single_quote = false;
448464
let mut in_double_quote = false;
449-
// Simple line-comment skip flag.
450465
let mut in_line_comment = false;
451466
let mut in_block_comment = false;
452467
let mut prev = '\0';
@@ -551,4 +566,24 @@ mod tests {
551566
let stmts = split_sql_statements(";; ;SELECT 1;;");
552567
assert_eq!(stmts, vec!["SELECT 1"]);
553568
}
569+
570+
#[test]
571+
fn procedural_begin_end_not_split() {
572+
let sql = "CREATE FUNCTION f(x INT) RETURNS TEXT AS \
573+
BEGIN IF x > 0 THEN RETURN 'pos'; ELSE RETURN 'neg'; END IF; END";
574+
let stmts = split_sql_statements(sql);
575+
assert_eq!(stmts.len(), 1);
576+
assert!(stmts[0].starts_with("CREATE FUNCTION"));
577+
assert!(stmts[0].ends_with("END"));
578+
}
579+
580+
#[test]
581+
fn procedural_with_trailing_semicolon() {
582+
let sql = "CREATE FUNCTION f(x INT) RETURNS INT AS \
583+
BEGIN RETURN x; END;";
584+
let stmts = split_sql_statements(sql);
585+
assert_eq!(stmts.len(), 1);
586+
assert!(stmts[0].contains("BEGIN"));
587+
assert!(stmts[0].ends_with("END"));
588+
}
554589
}

0 commit comments

Comments
 (0)