diff --git a/changelog.md b/changelog.md index 4e5608816..a34c52fa4 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ Upcoming (TBD) Bug Fixes -------- * Open the audit log and the `\T`/`\o` output files as UTF-8, so non-ASCII output no longer fails under non-UTF-8 locales. +* Support `DELIMITER` in batch input (`mycli < file.sql`, `--batch`), so dumps with triggers or routines restore. v2.30.4 (2026/11/12) diff --git a/mycli/AUTHORS.txt b/mycli/AUTHORS.txt index 6f58f6195..1172a4ebd 100644 --- a/mycli/AUTHORS.txt +++ b/mycli/AUTHORS.txt @@ -123,6 +123,7 @@ Contributors: * Wei-Lun Li * Shaurya Kesarwani * Alex Castillo + * Jay Hemnani Created by: diff --git a/mycli/packages/utils/batch_utils.py b/mycli/packages/utils/batch_utils.py index d0ebd218e..36803c3a6 100644 --- a/mycli/packages/utils/batch_utils.py +++ b/mycli/packages/utils/batch_utils.py @@ -1,3 +1,4 @@ +import re from typing import IO, Generator import sqlglot @@ -5,21 +6,47 @@ MAX_MULTILINE_BATCH_STATEMENT = 5000 +DELIMITER_RE = re.compile(r'^\s*delimiter\s+(\S+)\s*$', re.IGNORECASE) + + +def _has_tokens(text: str) -> bool: + try: + return bool(sqlglot.tokenize(text, read='mysql')) + except sqlglot.errors.TokenError: + return True + def statements_from_filehandle(file_h: IO) -> Generator[tuple[str, int], None, None]: statements = '' line_counter = 0 batch_counter = 0 + delimiter = ';' for batch_text in file_h: line_counter += 1 if line_counter > MAX_MULTILINE_BATCH_STATEMENT: raise ValueError(f'Saw single input statement greater than {MAX_MULTILINE_BATCH_STATEMENT} lines; assuming a parsing error.') + # DELIMITER is a client command, as in the mysql client: switch delimiters, don't send it + delimiter_match = DELIMITER_RE.match(batch_text) + if delimiter_match and not _has_tokens(statements): + delimiter = delimiter_match.group(1) + statements = '' + line_counter = 0 + continue statements += batch_text + if delimiter != ';': + statement = statements.rstrip() + # A custom delimiter is only recognized at the end of a line, so + # two statements on one line are sent together as one + if statement.endswith(delimiter): + yield (statement[: -len(delimiter)].strip(), batch_counter) + batch_counter += 1 + statements = '' + line_counter = 0 + continue try: tokens = sqlglot.tokenize(statements, read='mysql') if not tokens: continue - # we don't yet handle changing the delimiter within the batch input if tokens[-1].text == ';': # The advantage of sqlparse for splitting is that it preserves the input. # https://github.com/tobymao/sqlglot/issues/2587#issuecomment-1823109501 diff --git a/mycli_test/pytests/test_batch_utils.py b/mycli_test/pytests/test_batch_utils.py index 840a322c0..9766ef61e 100644 --- a/mycli_test/pytests/test_batch_utils.py +++ b/mycli_test/pytests/test_batch_utils.py @@ -99,3 +99,12 @@ def fake_tokenize(sql: str, read: str): ('select 1;', 0), ('select 2;', 1), ] + + +def test_statements_from_filehandle_handles_delimiter_blocks() -> None: + statements = collect_statements('DELIMITER //\nCREATE PROCEDURE p1()\nBEGIN\n SELECT 1;\nEND //\nDELIMITER ;\nCALL p1();\n') + + assert statements == [ + ('CREATE PROCEDURE p1()\nBEGIN\n SELECT 1;\nEND', 0), + ('CALL p1();', 1), + ]