From dfc954032b60830bde473db76107c3e1ee1a32dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle?= Date: Wed, 7 Oct 2026 13:33:43 +0200 Subject: [PATCH 1/2] build: update sqlparse to 0.6.0 Adapt the CASE tracking workaround to the upstream block-stack splitter and cover escaped semicolons and nested CASE expressions. Upstream fix: https://github.com/andialbrecht/sqlparse/pull/839 --- setup.cfg | 2 +- src/firebolt/common/statement_formatter.py | 71 +++------------------- tests/unit/common/test_typing_format.py | 17 ++++++ 3 files changed, 27 insertions(+), 63 deletions(-) diff --git a/setup.cfg b/setup.cfg index b876ace5324..537be6b936a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = pydantic>=2.13.5,<3.0.0 python-dateutil>=2.9.0.post0 readerwriterlock>=1.0.9 - sqlparse==0.5.5 + sqlparse==0.6.0 trio>=0.31.0 truststore>=0.10.4 python_requires = >=3.10 diff --git a/src/firebolt/common/statement_formatter.py b/src/firebolt/common/statement_formatter.py index bc617d88ca1..d35da723dbe 100644 --- a/src/firebolt/common/statement_formatter.py +++ b/src/firebolt/common/statement_formatter.py @@ -27,73 +27,20 @@ NotSupportedError, ) +_original_change_splitlevel = _StatementSplitter._change_splitlevel -def _patched_change_splitlevel(self, ttype, value): # type: ignore[no-untyped-def] - """Patched version of StatementSplitter._change_splitlevel. - - Fixes CASE...END level tracking outside of CREATE blocks. - See: https://github.com/andialbrecht/sqlparse/pull/839 - """ - if ttype is _T.Punctuation and value == "(": - return 1 - elif ttype is _T.Punctuation and value == ")": - return -1 - elif ttype not in _T.Keyword: - return 0 - - unified = value.upper() - - if ttype is _T.Keyword.DDL and unified.startswith("CREATE"): - self._is_create = True - return 0 - - if unified == "DECLARE" and self._is_create and self._begin_depth == 0: - self._in_declare = True - return 1 - - if unified == "BEGIN": - self._begin_depth += 1 - self._seen_begin = True - if self._is_create: - return 1 - return 0 +def _patched_change_splitlevel(self, ttype, value): # type: ignore[no-untyped-def] + """Track CASE outside BEGIN blocks until sqlparse fixes PR 839.""" + # https://github.com/andialbrecht/sqlparse/pull/839 if ( - self._seen_begin - and (ttype is _T.Keyword or ttype is _T.Name) - and unified - in ( - "TRANSACTION", - "WORK", - "TRAN", - "DISTRIBUTED", - "DEFERRED", - "IMMEDIATE", - "EXCLUSIVE", - ) + ttype in _T.Keyword + and value.upper() == "CASE" + and "BEGIN" not in self._block_stack ): - self._begin_depth = max(0, self._begin_depth - 1) - self._seen_begin = False - return 0 - - if unified == "END": - if not self._in_case: - self._begin_depth = max(0, self._begin_depth - 1) - else: - self._in_case = False - return -1 - - if unified == "CASE": - self._in_case = True + self._block_stack.append("CASE") return 1 - - if unified in ("IF", "FOR", "WHILE") and self._is_create and self._begin_depth > 0: - return 1 - - if unified in ("END IF", "END FOR", "END WHILE"): - return -1 - - return 0 + return _original_change_splitlevel(self, ttype, value) setattr(_StatementSplitter, "_change_splitlevel", _patched_change_splitlevel) diff --git a/tests/unit/common/test_typing_format.py b/tests/unit/common/test_typing_format.py index cc543cac675..ea7273d1c83 100644 --- a/tests/unit/common/test_typing_format.py +++ b/tests/unit/common/test_typing_format.py @@ -307,6 +307,23 @@ def test_create_statement_formatter_invalid_version() -> None: assert "Unsupported version: 3" in str(excinfo.value) +@mark.parametrize( + "expression", + [ + "CASE WHEN 1 THEN 'a' ELSE 'b' END", + "CASE WHEN 1 THEN CASE WHEN 2 THEN 'a' END ELSE 'b' END", + ], +) +def test_case_followed_by_quoted_semicolon( + formatter: StatementFormatter, expression: str +) -> None: + sql = f"SELECT {expression}, test IN ('foo \\', 'foo;') FROM t; SELECT 2;" + assert formatter.split_format_sql(sql, None) == [ + f"SELECT {expression}, test IN ('foo \\', 'foo;') FROM t", + "SELECT 2", + ] + + def test_patched_change_splitlevel(formatter: StatementFormatter) -> None: # Testing CREATE, DECLARE, BEGIN, END, CASE, IF, FOR, WHILE # These exercise _patched_change_splitlevel via split_format_sql which calls parse_sql From 845562c6347ed13fe22bc4e6f3a2396718617db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle?= Date: Wed, 7 Oct 2026 13:36:14 +0200 Subject: [PATCH 2/2] build: update dependencies for Python 3.10 Update runtime, test, documentation, and formatting dependencies. Remove redundant marks on fixtures rejected by pytest 9 while retaining the usage-tracker test group. --- .pre-commit-config.yaml | 12 ++++++------ examples/dbapi.ipynb | 12 ++++-------- setup.cfg | 18 +++++++++--------- src/firebolt/common/cursor/base_cursor.py | 2 +- tests/integration/utils/test_usage_tracker.py | 2 -- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 100ac84f0f7..21898284703 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-merge-conflict - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.1.0 + rev: v3.2.0 hooks: - id: setup-cfg-fmt args: @@ -14,7 +14,7 @@ repos: - --min-py-version=3.10 - repo: https://github.com/myint/autoflake - rev: v2.3.1 + rev: v2.4.0 hooks: - id: autoflake args: @@ -26,12 +26,12 @@ repos: - --remove-unused-variables - repo: https://github.com/PyCQA/isort - rev: "6.1.0" + rev: "9.0.2" hooks: - id: isort - repo: https://github.com/psf/black - rev: 25.11.0 + rev: 26.10.0 hooks: - id: black language_version: python3.10 @@ -40,10 +40,10 @@ repos: rev: v0.4.0 hooks: - id: black_nbconvert - additional_dependencies: ["setuptools>=69,<82", "black==25.11.0", "tomli"] # 82+ removes pkg_resources + additional_dependencies: ["setuptools>=69,<82", "black==26.10.0", "tomli"] # 82+ removes pkg_resources - repo: https://github.com/PyCQA/flake8 - rev: "7.3.0" + rev: "7.4.1" hooks: - id: flake8 additional_dependencies: diff --git a/examples/dbapi.ipynb b/examples/dbapi.ipynb index a9f20d601ae..53aa135ad15 100644 --- a/examples/dbapi.ipynb +++ b/examples/dbapi.ipynb @@ -179,12 +179,10 @@ "metadata": {}, "outputs": [], "source": [ - "cursor.execute(\n", - " \"\"\"\n", + "cursor.execute(\"\"\"\n", " select * from test_table where id < 4;\n", " select * from test_table where id > 2;\n", - "\"\"\"\n", - ")\n", + "\"\"\")\n", "print(\"First query: \", cursor.fetchall())\n", "assert cursor.nextset()\n", "print(\"Second query: \", cursor.fetchall())\n", @@ -209,13 +207,11 @@ "outputs": [], "source": [ "try:\n", - " cursor.execute(\n", - " \"\"\"\n", + " cursor.execute(\"\"\"\n", " select * from test_table where id < 4;\n", " select * from test_table where wrong_field > 2;\n", " select * from test_table\n", - " \"\"\"\n", - " )\n", + " \"\"\")\n", "except OperationalError:\n", " pass\n", "cursor.fetchall()" diff --git a/setup.cfg b/setup.cfg index 537be6b936a..cf78da109e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ project_urls = packages = find: install_requires = aiorwlock==1.5.1 - anyio>=4.12.1 + anyio>=4.15.1 appdirs>=1.4.4 appdirs-stubs>=0.2.0 async-generator>=1.10 @@ -37,7 +37,7 @@ install_requires = python-dateutil>=2.9.0.post0 readerwriterlock>=1.0.9 sqlparse==0.6.0 - trio>=0.31.0 + trio>=0.34.0 truststore>=0.10.4 python_requires = >=3.10 include_package_data = True @@ -52,21 +52,21 @@ ciso8601 = ciso8601==2.3.3 dev = devtools==0.12.2 - mypy>=1.19.1,<2 - pre-commit==4.3.0 + mypy>=2.4.0,<3 + pre-commit==4.6.2 psutil==7.2.2 - pyfakefs>=5.10.2,<6 - pytest==8.4.2 + pyfakefs>=6.2.0,<7 + pytest==9.1.1 pytest-cov==7.1.0 - pytest-httpx>=0.35.0 - pytest-mock==3.15.1 + pytest-httpx>=0.36.2 + pytest-mock==3.16.0 pytest-timeout==2.4.0 pytest-trio==0.8.0 pytest-xdist==3.8.0 trio-typing[mypy]>=0.10,<0.11 types-cryptography==3.3.23.2 docs = - sphinx>=7.4.7,<10 + sphinx>=8.1.3,<10 sphinx-rtd-theme>=3.1.0,<4 [options.package_data] diff --git a/src/firebolt/common/cursor/base_cursor.py b/src/firebolt/common/cursor/base_cursor.py index c7b5f4647f9..7e7ee360042 100644 --- a/src/firebolt/common/cursor/base_cursor.py +++ b/src/firebolt/common/cursor/base_cursor.py @@ -293,7 +293,7 @@ def setoutputsize(self, size: int, column: Optional[int] = None) -> None: def close(self) -> None: """Terminate an ongoing query (if any) and mark connection as closed.""" self._state = CursorState.CLOSED - self.connection._remove_cursor(self) # type:ignore + self.connection._remove_cursor(self) # type: ignore def __del__(self) -> None: self.close() diff --git a/tests/integration/utils/test_usage_tracker.py b/tests/integration/utils/test_usage_tracker.py index 20d8f82e2ad..0338ee0ec9d 100644 --- a/tests/integration/utils/test_usage_tracker.py +++ b/tests/integration/utils/test_usage_tracker.py @@ -17,7 +17,6 @@ ] -@mark.xdist_group(name="usage_tracker") @fixture(scope="module") def create_cli_mock(): # Cleanup before starting @@ -33,7 +32,6 @@ def create_cli_mock(): rmtree(TEST_FOLDER) -@mark.xdist_group(name="usage_tracker") @fixture(scope="module") def test_model(): with open(TEST_SCRIPT_MODEL) as f: