From ab7420a4a320cb9c1df899d35cc64848eeee41c3 Mon Sep 17 00:00:00 2001 From: Rami Abdelrazzaq Date: Sun, 13 Sep 2026 08:07:46 -0500 Subject: [PATCH 1/2] Fix Oracle procedure declaration splitting --- sqlparse/engine/statement_splitter.py | 35 ++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/sqlparse/engine/statement_splitter.py b/sqlparse/engine/statement_splitter.py index bc57d170..6a0ab703 100644 --- a/sqlparse/engine/statement_splitter.py +++ b/sqlparse/engine/statement_splitter.py @@ -21,6 +21,8 @@ def _reset(self): self._parenthesis_level = 0 self._unconfirmed_start = None self._is_create = False + self._is_create_procedure = False + self._procedure_body_started = False self._seen_begin = False self.consume_ws = False @@ -109,6 +111,13 @@ def _change_splitlevel(self, ttype, value): self._is_create = True return 0 + # Oracle-style CREATE PROCEDURE statements can contain declarations + # terminated by semicolons between AS/IS and the BEGIN body. Keep + # those semicolons inside the CREATE statement until its outer END. + if self._is_create and unified == 'PROCEDURE': + self._is_create_procedure = True + return 0 + # Handle DECLARE block start (only for CREATE statements) if unified == 'DECLARE' and self._is_create and not self._block_stack: self._block_stack.append('DECLARE') @@ -117,6 +126,8 @@ def _change_splitlevel(self, ttype, value): # Handle BEGIN block start if unified == 'BEGIN': self._seen_begin = True + if self._is_create_procedure: + self._procedure_body_started = True # Transition DECLARE to BEGIN if present if self._block_stack and self._block_stack[-1] == 'DECLARE': self._block_stack.pop() @@ -145,8 +156,19 @@ def _change_splitlevel(self, ttype, value): if res is not None: return res - # Handle closing keywords - return self._handle_closing_keyword(unified) + # A top-level END closes a CREATE PROCEDURE body. Clear the routine + # state before its following semicolon so the statement can be yielded. + procedure_end = ( + unified == 'END' + and self._is_create_procedure + and self._procedure_body_started + and self._block_stack == ['BEGIN'] + ) + result = self._handle_closing_keyword(unified) + if procedure_end: + self._is_create_procedure = False + self._procedure_body_started = False + return result def process(self, stream): """Process the stream""" @@ -179,8 +201,13 @@ def process(self, stream): # standalone BEGIN; as a transaction statement if ttype is T.Punctuation and value == ';': self._seen_begin = False - # Split on semicolon if not inside a BEGIN...END block - if self.level <= 0 and 'BEGIN' not in self._block_stack: + # Split on semicolon if not inside a BEGIN...END block or the + # declaration section of a CREATE PROCEDURE. + if ( + self.level <= 0 + and 'BEGIN' not in self._block_stack + and not self._is_create_procedure + ): self.consume_ws = True elif ttype is T.Keyword and value.split()[0] == 'GO': self.consume_ws = True From 3006f9db68a903526d9f9d2186a02b3ecdd678df Mon Sep 17 00:00:00 2001 From: Rami Abdelrazzaq Date: Sun, 13 Sep 2026 08:07:55 -0500 Subject: [PATCH 2/2] Add Oracle procedure split regressions --- tests/test_split_issue692.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_split_issue692.py diff --git a/tests/test_split_issue692.py b/tests/test_split_issue692.py new file mode 100644 index 00000000..22080d3a --- /dev/null +++ b/tests/test_split_issue692.py @@ -0,0 +1,33 @@ +import sqlparse + + +def test_split_oracle_procedure_with_declarations(): + sql = """CREATE PROCEDURE remove_emp (employee_id NUMBER) AS + tot_emps NUMBER; + BEGIN + DELETE FROM employees + WHERE employees.employee_id = remove_emp.employee_id; + tot_emps := tot_emps - 1; + END;""" + + statements = sqlparse.split(sql) + + assert len(statements) == 1 + assert statements[0] == sql + + +def test_split_oracle_procedure_then_statement(): + sql = """CREATE OR REPLACE PROCEDURE update_counter AS + counter NUMBER; + label VARCHAR2(20); +BEGIN + counter := 1; + label := 'ready'; +END; +SELECT 42;""" + + statements = sqlparse.split(sql) + + assert len(statements) == 2 + assert statements[0].endswith("END;") + assert statements[1] == "SELECT 42;"