From 65b4764868fd90d85b8765f77ba000ac2eeb0315 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sun, 27 Sep 2026 16:11:26 +0900 Subject: [PATCH 1/2] Preserve SQL routine statement boundaries Signed-off-by: minleejae --- .../parser/RoutineBodyBoundary.java | 214 ++++++++++++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 21 +- .../create/SqlRoutineBodyBoundaryTest.java | 189 ++++++++++++++++ 3 files changed, 410 insertions(+), 14 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java new file mode 100644 index 000000000..150353887 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java @@ -0,0 +1,214 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Finds the boundary of an opaque routine without interpreting its statements. In particular, + * expression CASE and procedural END qualifiers must not close an enclosing BEGIN block. + */ +final class RoutineBodyBoundary { + private enum Block { + BEGIN, IF, CASE_STATEMENT, CASE_EXPRESSION, LOOP, WHILE, REPEAT + } + + private final Deque blocks = new ArrayDeque<>(); + private int parentheses; + private int previousKind = -1; + private int statementKind = -1; + private boolean hasParameters; + private boolean standardReturns; + private boolean returnTypeName; + private boolean afterDot; + private boolean declarations; + private boolean bodyStarted; + private boolean compoundBody; + private boolean quotedBody; + private boolean completed; + private boolean handlerAction; + private boolean statementStart = true; + + boolean endsAt(Token token) throws ParseException { + if (token.kind == EOF) { + if (!blocks.isEmpty()) { + throw new ParseException("Unterminated routine block"); + } + return true; + } + return token.kind == ST_SEMICOLON && parentheses == 0 && blocks.isEmpty() + && !declarations && (hasParameters || quotedBody || completed); + } + + boolean startsQuotedBody(Token token, Token next) { + return parentheses == 0 && !compoundBody && !quotedBody && token.kind == K_AS + && next.kind == S_CHAR_LITERAL + && (!bodyStarted || statementKind == K_RETURN || statementKind == K_SET); + } + + void quotedBody() { + quotedBody = true; + bodyStarted = true; + declarations = false; + } + + void accept(Token token, Token next) { + int kind = token.kind; + if (kind == OPENING_BRACKET) { + parentheses++; + } else if (kind == CLOSING_BRACKET) { + if (--parentheses == 0 && !bodyStarted) { + hasParameters = true; + } + } else if (parentheses == 0 && !quotedBody && !completed) { + acceptTopLevel(token, next); + } + previousKind = kind; + afterDot = ".".equals(token.image); + } + + private void acceptTopLevel(Token token, Token next) { + int kind = token.kind; + if (afterDot) { + return; // Qualified type/column names may use otherwise significant keywords. + } + // END IF/CASE/LOOP/WHILE/REPEAT is one terminator, not another opener. + if (previousKind == K_END && (kind == K_IF || kind == K_CASE || kind == K_LOOP + || keyword(token, "WHILE") || keyword(token, "REPEAT"))) { + return; + } + if (kind == K_RETURNS && !bodyStarted) { + standardReturns = true; + returnTypeName = true; + return; + } + if (returnTypeName) { + returnTypeName = keyword(token, "SETOF"); + return; + } + if (!compoundBody && (kind == K_AS || kind == K_IS) + && (!bodyStarted || statementKind == K_RETURN && !standardReturns + || statementKind == K_SET)) { + declarations = true; + bodyStarted = false; + return; + } + if (kind == ST_SEMICOLON) { + startStatement(); + return; + } + if (kind == K_END) { + if (!blocks.isEmpty()) { + Block closed = blocks.pop(); + completed = blocks.isEmpty() && closed != Block.CASE_EXPRESSION; + } else { + // Retain legacy opaque declarations without a parameter list or BEGIN. + completed = true; + } + statementStart = false; + return; + } + if (!(hasParameters || declarations || bodyStarted)) { + return; + } + if (handlerAction && isSimpleStatementStart(kind)) { + startStatement(); + } + if (kind == K_BEGIN + && (!bodyStarted || statementStart || statementKind == K_DECLARE)) { + blocks.push(Block.BEGIN); + declarations = false; + compoundBody = true; + bodyStarted = true; + startStatement(); + return; + } + if (kind == K_CASE) { + boolean statementCase = !declarations && (statementStart || handlerAction); + blocks.push(statementCase ? Block.CASE_STATEMENT : Block.CASE_EXPRESSION); + if (statementCase) { + bodyStarted = true; + compoundBody = true; + handlerAction = false; + } + statementStart = false; + return; + } + if (declarations) { + return; + } + if ((statementStart || handlerAction) && (kind == K_IF || kind == K_LOOP + || keyword(token, "WHILE") || keyword(token, "REPEAT"))) { + Block block = kind == K_IF ? Block.IF + : kind == K_LOOP ? Block.LOOP + : keyword(token, "WHILE") ? Block.WHILE : Block.REPEAT; + blocks.push(block); + bodyStarted = true; + compoundBody = true; + handlerAction = false; + statementKind = kind; + statementStart = block == Block.LOOP || block == Block.REPEAT; + return; + } + if (kind == K_LOOP && (statementKind == K_FOR || blocks.peek() == Block.WHILE)) { + // Oracle FOR/WHILE ... LOOP shares the same END LOOP boundary. + if (blocks.peek() != Block.WHILE) { + blocks.push(Block.LOOP); + } + startStatement(); + return; + } + if ((kind == K_THEN || kind == K_ELSE) && !blocks.isEmpty() + && blocks.peek() != Block.CASE_EXPRESSION + || kind == K_DO && blocks.peek() == Block.WHILE) { + startStatement(); + return; + } + if (previousKind == K_BEGIN && keyword(token, "ATOMIC")) { + return; + } + if (statementStart && (":".equals(next.image) || ":".equals(token.image))) { + return; // A label precedes the statement it names. + } + if (!bodyStarted) { + // SET may also be a PostgreSQL header option; AS remains recognizable above. + if (!isSimpleStatementStart(kind)) { + return; + } + bodyStarted = true; + } + if (statementStart) { + statementKind = kind; + } + if (statementKind == K_DECLARE && keyword(token, "HANDLER")) { + handlerAction = true; + } + statementStart = false; + } + + private void startStatement() { + statementStart = true; + statementKind = -1; + handlerAction = false; + } + + private static boolean keyword(Token token, String keyword) { + return token.kind == S_IDENTIFIER && keyword.equalsIgnoreCase(token.image); + } + + private static boolean isSimpleStatementStart(int kind) { + return kind == K_RETURN || kind == K_SELECT || kind == K_INSERT || kind == K_UPDATE + || kind == K_DELETE || kind == K_REPLACE || kind == K_SET || kind == K_CALL + || kind == K_WITH || kind == K_DO; + } +} diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d07c9e0fa..0574f8bf3 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -20573,41 +20573,34 @@ List captureRest() { /** * Captures opaque routine declarations through their terminating semicolon. * PostgreSQL AS string bodies can use ordinary, escape or dollar quotes. - * Strings and END tokens in parenthesized arguments are not body boundaries. + * The boundary tracker distinguishes simple statements, declaration sections, + * compound bodies, and expression CASE from procedural END qualifiers. */ JAVACODE List captureFunctionBody() { List tokens = new LinkedList(); Token tok; - boolean foundEnd = false; - boolean bodyStarted = false; - int parentheses = 0; + RoutineBodyBoundary boundary = new RoutineBodyBoundary(); while(true) { tok = getToken(1); int l = tokens.size(); - if( tok.kind == EOF || ( foundEnd && tok.kind == ST_SEMICOLON) ) { + if (boundary.endsAt(tok)) { if (tok.kind == ST_SEMICOLON) { tokens.add(tok.image); } break; - } else if (parentheses == 0 && !bodyStarted && tok.kind == K_AS - && getToken(2).kind == S_CHAR_LITERAL) { + } else if (boundary.startsQuotedBody(tok, getToken(2))) { tokens.add(getNextToken().image); tokens.add(captureRoutineBodyLiteral()); - foundEnd = true; - bodyStarted = true; + boundary.quotedBody(); continue; } else if ( l>0 && ( tok.image.equals(".") || tokens.get(l-1).endsWith(".")) ) { tokens.set(l-1, tokens.get(l-1) + tok.image); } else { tokens.add(tok.image); } - foundEnd |= parentheses == 0 && tok.kind == K_END; - if (parentheses == 0 && (tok.kind == K_AS || tok.kind == K_BEGIN)) bodyStarted = true; - if (tok.kind == OPENING_BRACKET) parentheses++; - if (tok.kind == CLOSING_BRACKET) parentheses--; - + boundary.accept(tok, getToken(2)); tok = getNextToken(); } return tokens; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java new file mode 100644 index 000000000..a6a36650f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/SqlRoutineBodyBoundaryTest.java @@ -0,0 +1,189 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlRoutineBodyBoundaryTest { + private static final String FOLLOWING = "SELECT 2718 AS after_routine"; + + static Stream sqlRoutines() { + Stream postgres = Stream.of( + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL IMMUTABLE RETURN x + 1", + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL RETURN " + + "CASE WHEN x IS NULL THEN 0 ELSE x END + CASE WHEN x > 0 THEN 1 ELSE 0 END", + "CREATE FUNCTION f(x int) RETURNS boolean LANGUAGE SQL RETURN x IS NULL", + "CREATE FUNCTION f(x begin) RETURNS begin LANGUAGE SQL RETURN x", + "CREATE FUNCTION f(x public.begin) RETURNS public.begin LANGUAGE SQL RETURN x", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT t.end, t.case FROM t; SELECT 42; END", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN (SELECT max(id) FROM hidden)", + "CREATE FUNCTION f(x int) RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT CASE WHEN x > 0 THEN CASE WHEN x > 1 THEN x ELSE 1 END ELSE 0 END; " + + "SELECT x + 1; END", + "CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC " + + "INSERT INTO t VALUES (CASE WHEN true THEN 1 ELSE 0 END); SELECT 2; END", + "CREATE FUNCTION f() RETURNS int SET search_path TO public " + + "AS 'SELECT 1;' LANGUAGE sql") + .map(sql -> Arguments.of(Dialect.POSTGRESQL, sql)); + Stream mysql = Stream.of( + "CREATE PROCEDURE p() SELECT 1", + "CREATE PROCEDURE p() SET @result = IF(1, 2, 3)", + "CREATE PROCEDURE p() INSERT INTO t VALUES (1)", + "CREATE FUNCTION f(x int) RETURNS int DETERMINISTIC RETURN x + 1", + "CREATE FUNCTION f(x int) RETURNS int RETURN CASE WHEN x IS NULL THEN 0 ELSE x END", + "CREATE PROCEDURE p() BEGIN SELECT CASE WHEN 1 THEN IF(1, 2, 3) ELSE 0 END; " + + "BEGIN SELECT 'END; SELECT 0;'; END; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN IF (1) THEN SELECT 1; " + + "ELSEIF 2 THEN SELECT 2; ELSE SELECT 3; END IF; SELECT 4; END", + "CREATE PROCEDURE p() BEGIN WHILE (1) DO IF (1) THEN SELECT 1; END IF; " + + "END WHILE; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN REPEAT SELECT 1; " + + "UNTIL CASE WHEN 1 THEN true ELSE false END END REPEAT; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN CASE 1 WHEN 1 THEN IF (1) THEN SELECT 1; END IF; " + + "ELSE SELECT 2; END CASE; SELECT 3; END", + "CREATE PROCEDURE p() outer_block: BEGIN outer_loop: LOOP " + + "IF (1) THEN LEAVE outer_loop; END IF; END LOOP outer_loop; " + + "SELECT 2; END outer_block", + "CREATE PROCEDURE p() IF (1) THEN SELECT 1; ELSE SELECT 2; END IF", + "CREATE PROCEDURE p() CASE WHEN 1 THEN SELECT 1; ELSE SELECT 2; END CASE", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "BEGIN SET @done = 1; END; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "IF @done IS NULL THEN SET @done = 1; END IF; SELECT 2; END", + "CREATE PROCEDURE p() BEGIN DECLARE CONTINUE HANDLER FOR NOT FOUND " + + "SET @done = IF(1, 2, 3); SELECT 2; END") + .map(sql -> Arguments.of(Dialect.MYSQL, sql)); + return Stream.concat(postgres, mysql); + } + + @ParameterizedTest + @MethodSource("sqlRoutines") + void preservesRoutineBodyAndFollowingStatement(Dialect dialect, String sql) throws Exception { + String script = sql + "; " + FOLLOWING + ";"; + Statements statements = parse(script, dialect); + assertRoutineAndFollowing(statements, sql); + assertRoutineAndFollowing(parser(script, dialect).Statements(), sql); + assertRoutineAndFollowing(CCJSqlParserUtil.parseStatements(script), sql); + assertRoutineAndFollowing(parse(statements.toString(), dialect), sql); + + StringBuilder deparsed = new StringBuilder(); + for (Statement statement : statements) { + statement.accept(new StatementDeParser(deparsed), null); + deparsed.append(";\n"); + } + assertRoutineAndFollowing(parse(deparsed.toString(), dialect), sql); + assertThat(parse(deparsed.toString(), dialect).get(0).toString()) + .isEqualTo(statements.get(0).toString()); + assertThat(CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)).toString()) + .isEqualTo(statements.get(0).toString().replaceFirst(";$", "")); + } + + @Test + void directStatementParserLeavesFollowingStatementAvailable() throws Exception { + CCJSqlParser parser = parser("CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; " + + FOLLOWING + ";", Dialect.POSTGRESQL); + assertThat(parser.Statement()).isInstanceOf(CreateFunction.class); + assertThat(parser.getToken(1).kind).isEqualTo(CCJSqlParserConstants.K_SELECT); + assertThat(parser.Statement().toString()).isEqualTo(FOLLOWING); + assertThat(parser.getToken(1).kind).isEqualTo(CCJSqlParserConstants.EOF); + } + + @Test + void keepsAdjacentRoutinesAndDdlSeparate() throws Exception { + String sql = "CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1;" + + "CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC SELECT CASE WHEN true " + + "THEN 1 ELSE 0 END; SELECT 2; END; CREATE TABLE after_routines(id int);" + + FOLLOWING + ";"; + Statements statements = parse(sql, Dialect.POSTGRESQL); + assertThat(statements).hasSize(4); + assertThat(statements.get(0)).isExactlyInstanceOf(CreateFunction.class); + assertThat(statements.get(1)).isExactlyInstanceOf(CreateProcedure.class); + assertThat(statements.get(2).toString()).isEqualTo("CREATE TABLE after_routines (id int)"); + assertThat(statements.get(3).toString()).isEqualTo(FOLLOWING); + assertThat(parse(statements.toString(), Dialect.POSTGRESQL)).hasSize(4); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC SELECT 1;", + "CREATE FUNCTION f() RETURNS int LANGUAGE SQL BEGIN ATOMIC " + + "SELECT CASE WHEN true THEN 1 END; SELECT 2;", + "CREATE PROCEDURE p() BEGIN IF 1 THEN SELECT 1; END", + "CREATE PROCEDURE p() BEGIN SELECT CASE WHEN 1 THEN 2; END" + }) + void rejectsUnterminatedBlocks(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseStatements(sql)); + } + + @Test + void reportsInvalidFollowingStatementInsteadOfCapturingIt() { + assertThrows(JSQLParserException.class, + () -> parse("CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; SELEC 2;", + Dialect.POSTGRESQL)); + assertThrows(JSQLParserException.class, + () -> parse("CREATE PROCEDURE p() SELECT 1; SELEC 2;", Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE PROCEDURE p AS v int; BEGIN NULL; END", + "CREATE FUNCTION f(x int) RETURN int IS v int; " + + "BEGIN IF x > 0 THEN RETURN x; END IF; RETURN 0; END", + "CREATE PROCEDURE p AS BEGIN FOR i IN 1 .. 3 LOOP NULL; END LOOP; NULL; END", + "CREATE PROCEDURE p AS BEGIN WHILE 1 = 1 LOOP NULL; END LOOP; NULL; END" + }) + void retainsOracleDeclarationAndBlockBoundaries(String sql) throws Exception { + assertRoutineAndFollowing(CCJSqlParserUtil.parseStatements(sql + "; " + FOLLOWING + ";"), + sql); + } + + private static CCJSqlParser parser(String sql, Dialect dialect) { + return CCJSqlParserUtil.newParser(sql).withDialect(dialect) + .withUnsupportedStatements(false); + } + + private static Statements parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withDialect(dialect).withUnsupportedStatements(false)); + } + + private static void assertRoutineAndFollowing(Statements statements, String source) { + assertThat(statements).hasSize(2); + assertThat(statements.get(0)).isInstanceOf(CreateFunctionalStatement.class); + CreateFunctionalStatement routine = (CreateFunctionalStatement) statements.get(0); + assertThat(routine.getFunctionDeclarationParts()).doesNotContain("after_routine", "2718"); + // The entire body must survive, including statements after an inner END. + assertThat(routine.toString().replaceAll("\\s+", "").replaceFirst(";$", "")) + .isEqualTo(source.replaceAll("\\s+", "")); + assertThat(statements.get(1)).isExactlyInstanceOf(PlainSelect.class); + assertThat(statements.get(1).toString()).isEqualTo(FOLLOWING); + } +} From 04b9362a3bf847a5ca5689cf76bf74d73a589f27 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sun, 27 Sep 2026 16:34:29 +0900 Subject: [PATCH 2/2] Refactor routine boundary transitions into focused stages Signed-off-by: minleejae --- .../parser/RoutineBodyBoundary.java | 127 +++++++++++++----- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java index 150353887..4a5935fd4 100644 --- a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java +++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java @@ -78,35 +78,61 @@ void accept(Token token, Token next) { } private void acceptTopLevel(Token token, Token next) { - int kind = token.kind; if (afterDot) { return; // Qualified type/column names may use otherwise significant keywords. } // END IF/CASE/LOOP/WHILE/REPEAT is one terminator, not another opener. - if (previousKind == K_END && (kind == K_IF || kind == K_CASE || kind == K_LOOP - || keyword(token, "WHILE") || keyword(token, "REPEAT"))) { + if (previousKind == K_END && isEndQualifier(token)) { return; } + if (!acceptRoutineHeader(token)) { + acceptBodyToken(token, next); + } + } + + private boolean acceptRoutineHeader(Token token) { + int kind = token.kind; if (kind == K_RETURNS && !bodyStarted) { standardReturns = true; returnTypeName = true; - return; + return true; } if (returnTypeName) { returnTypeName = keyword(token, "SETOF"); - return; + return true; } if (!compoundBody && (kind == K_AS || kind == K_IS) && (!bodyStarted || statementKind == K_RETURN && !standardReturns || statementKind == K_SET)) { declarations = true; bodyStarted = false; + return true; + } + return false; + } + + private void acceptBodyToken(Token token, Token next) { + int kind = token.kind; + if (acceptStatementBoundary(kind)) { return; } - if (kind == ST_SEMICOLON) { + if (!(hasParameters || declarations || bodyStarted)) { + return; + } + if (handlerAction && isSimpleStatementStart(kind)) { startStatement(); + } + if (acceptBeginOrCase(kind) || declarations || acceptControlFlow(token)) { return; } + acceptStatementToken(token, next); + } + + private boolean acceptStatementBoundary(int kind) { + if (kind == ST_SEMICOLON) { + startStatement(); + return true; + } if (kind == K_END) { if (!blocks.isEmpty()) { Block closed = blocks.pop(); @@ -116,14 +142,12 @@ private void acceptTopLevel(Token token, Token next) { completed = true; } statementStart = false; - return; - } - if (!(hasParameters || declarations || bodyStarted)) { - return; - } - if (handlerAction && isSimpleStatementStart(kind)) { - startStatement(); + return true; } + return false; + } + + private boolean acceptBeginOrCase(int kind) { if (kind == K_BEGIN && (!bodyStarted || statementStart || statementKind == K_DECLARE)) { blocks.push(Block.BEGIN); @@ -131,7 +155,7 @@ private void acceptTopLevel(Token token, Token next) { compoundBody = true; bodyStarted = true; startStatement(); - return; + return true; } if (kind == K_CASE) { boolean statementCase = !declarations && (statementStart || handlerAction); @@ -142,44 +166,56 @@ private void acceptTopLevel(Token token, Token next) { handlerAction = false; } statementStart = false; - return; - } - if (declarations) { - return; + return true; } - if ((statementStart || handlerAction) && (kind == K_IF || kind == K_LOOP - || keyword(token, "WHILE") || keyword(token, "REPEAT"))) { - Block block = kind == K_IF ? Block.IF - : kind == K_LOOP ? Block.LOOP - : keyword(token, "WHILE") ? Block.WHILE : Block.REPEAT; + return false; + } + + private boolean acceptControlFlow(Token token) { + int kind = token.kind; + Block block = controlBlock(token); + if ((statementStart || handlerAction) && block != null) { blocks.push(block); bodyStarted = true; compoundBody = true; handlerAction = false; statementKind = kind; statementStart = block == Block.LOOP || block == Block.REPEAT; - return; + return true; } + if (acceptOracleLoop(kind)) { + return true; + } + if (isStatementBranch(kind)) { + startStatement(); + return true; + } + return false; + } + + private boolean acceptOracleLoop(int kind) { if (kind == K_LOOP && (statementKind == K_FOR || blocks.peek() == Block.WHILE)) { // Oracle FOR/WHILE ... LOOP shares the same END LOOP boundary. if (blocks.peek() != Block.WHILE) { blocks.push(Block.LOOP); } startStatement(); - return; + return true; } - if ((kind == K_THEN || kind == K_ELSE) && !blocks.isEmpty() + return false; + } + + private boolean isStatementBranch(int kind) { + return (kind == K_THEN || kind == K_ELSE) && !blocks.isEmpty() && blocks.peek() != Block.CASE_EXPRESSION - || kind == K_DO && blocks.peek() == Block.WHILE) { - startStatement(); - return; - } - if (previousKind == K_BEGIN && keyword(token, "ATOMIC")) { + || kind == K_DO && blocks.peek() == Block.WHILE; + } + + private void acceptStatementToken(Token token, Token next) { + int kind = token.kind; + if (isStatementPrefix(token, next)) { return; } - if (statementStart && (":".equals(next.image) || ":".equals(token.image))) { - return; // A label precedes the statement it names. - } if (!bodyStarted) { // SET may also be a PostgreSQL header option; AS remains recognizable above. if (!isSimpleStatementStart(kind)) { @@ -196,6 +232,29 @@ private void acceptTopLevel(Token token, Token next) { statementStart = false; } + private boolean isStatementPrefix(Token token, Token next) { + // ATOMIC and a label precede the statement they qualify. + return previousKind == K_BEGIN && keyword(token, "ATOMIC") + || statementStart && (":".equals(next.image) || ":".equals(token.image)); + } + + private static boolean isEndQualifier(Token token) { + return token.kind == K_CASE || controlBlock(token) != null; + } + + private static Block controlBlock(Token token) { + if (token.kind == K_IF) { + return Block.IF; + } + if (token.kind == K_LOOP) { + return Block.LOOP; + } + if (keyword(token, "WHILE")) { + return Block.WHILE; + } + return keyword(token, "REPEAT") ? Block.REPEAT : null; + } + private void startStatement() { statementStart = true; statementKind = -1;