Skip to content

fix(trino): strip trailing semicolon on unlimited query path - #2555

Closed
Bartok9 wants to merge 1 commit into
Canner:mainfrom
Bartok9:fix/trino-strip-unlimited-query
Closed

fix(trino): strip trailing semicolon on unlimited query path#2555
Bartok9 wants to merge 1 commit into
Canner:mainfrom
Bartok9:fix/trino-strip-unlimited-query

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Always strip trailing statement terminators in TrinoConnector.query before the LIMIT branch so unlimited "SELECT 1;" pastes match dry_run and postgres/canner.

Motivation

LIMIT and dry_run already called strip_trailing_semicolon. Unlimited execute used the raw SQL string only.

Real behavior proof

no tests ran in 0.00s

Testустрои

  • Source-level pin: strip assignment before limit branch
  • strip helper multi-semicolon cases
  • Apache-2.0 path: core/wren
  • Author Bartok9

Summary by CodeRabbit

  • Bug Fixes
    • Improved Trino query handling by consistently removing trailing semicolons before execution.
    • Ensured consistent SQL behavior for both unlimited queries and queries using a row limit.
  • Tests
    • Added coverage for multiple trailing semicolons, surrounding whitespace, and semicolons within quoted strings.

Always strip before LIMIT branch so bare execute matches dry_run and
postgres/canner composition for client-pasted statement terminators.
@github-actions github-actions Bot added python Pull requests that update Python code core labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

TrinoConnector.query() now strips trailing SQL semicolons before limit handling. Tests cover repeated trailing semicolons, quoted semicolons, and the ordering of preprocessing before the limit branch.

Changes

Trino semicolon handling

Layer / File(s) Summary
Preprocess SQL before limit handling
core/wren/src/wren/connector/trino.py
TrinoConnector.query() strips trailing semicolons before direct execution or LIMIT subquery wrapping.
Validate stripping behavior
core/wren/tests/unit/test_trino_unlimited_semicolon.py
Tests verify repeated trailing-semicolon removal, quoted-string preservation, and preprocessing before the limit branch.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • Canner/WrenAI#2480 — Directly changes Trino’s query() semicolon handling around LIMIT wrapping.
  • Canner/WrenAI#2488 — Adds unconditional semicolon stripping and related connector tests.
  • Canner/WrenAI#2534 — Applies unconditional SQL preprocessing before connector-specific limit handling.

Suggested reviewers: goldmedal

Poem

A rabbit found a semicolon trail,
And swept the SQL without fail.
Before limits wrap the query tight,
Quoted marks remain just right.
“Hop!” said the hare, “the tests now sing!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: stripping trailing semicolons on Trino unlimited query execution.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
core/wren/tests/unit/test_trino_unlimited_semicolon.py (2)

28-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Skip test on import failure instead of testing a dummy implementation.

If the actual strip_trailing_semicolon cannot be imported due to missing dependencies, defining a fallback regex here means the test is verifying its own local mock rather than the real application code. If the upstream regex in base.py changes in the future, this test will continue to blindly pass in fallback environments.

Consider using a test-skipping mechanism rather than maintaining a duplicate implementation.

♻️ Proposed refactor to skip the test
     except ModuleNotFoundError:
-        # Fallback: pure copy of the regex used by strip_trailing_semicolon.
-        trailing = re.compile(r"[;\s]+\Z")
-
-        def _strip(sql: str) -> str:
-            return trailing.sub("", sql)
-
-        return _strip
+        import pytest
+        pytest.skip("Missing dependencies to load base.py")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/tests/unit/test_trino_unlimited_semicolon.py` around lines 28 - 35,
Replace the fallback implementation in the import setup for
strip_trailing_semicolon with the test framework’s module-skip mechanism when
ModuleNotFoundError occurs. Remove the local trailing regex and _strip helper so
the tests only exercise the real application function when its dependencies are
available.

44-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the ast module to verify source code structure.

Relying on exact string matching (source.index(...)) to assert the order of operations is highly brittle. The test will break on any routine code formatting changes, such as moving function arguments to new lines or slight whitespace modifications.

Instead of parsing raw text, you can use the standard library ast module to safely analyze the structural syntax tree of the file without needing to import the actual dependencies.

♻️ Proposed refactor to use `ast` for structural testing
-def test_query_method_always_strips_before_limit_branch():
-    source = TRINO_PATH.read_text(encoding="utf-8")
-    start = source.index("def query(self, sql: str, limit: int | None = None)")
-    end = source.index("def dry_run(self, sql: str)", start)
-    body = source[start:end]
-    assert "sql = strip_trailing_semicolon(sql)" in body
-    assert "SELECT * FROM ({sql}) AS _sub LIMIT {limit}" in body
-    strip_at = body.index("sql = strip_trailing_semicolon(sql)")
-    limit_at = body.index("if limit is not None:")
-    assert strip_at < limit_at
+def test_query_method_always_strips_before_limit_branch():
+    import ast
+    source = TRINO_PATH.read_text(encoding="utf-8")
+    tree = ast.parse(source)
+    
+    class_node = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "TrinoConnector")
+    query_node = next(n for n in class_node.body if isinstance(n, ast.FunctionDef) and n.name == "query")
+    
+    strip_idx = limit_idx = -1
+    for i, stmt in enumerate(query_node.body):
+        if isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.Call) and getattr(stmt.value.func, "id", "") == "strip_trailing_semicolon":
+            strip_idx = i
+        elif isinstance(stmt, ast.If) and isinstance(stmt.test, ast.Compare):
+            limit_idx = i
+            
+    assert strip_idx != -1, "strip_trailing_semicolon assignment not found"
+    assert limit_idx != -1, "limit check if-statement not found"
+    assert strip_idx < limit_idx, "strip_trailing_semicolon must occur before limit branch"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/tests/unit/test_trino_unlimited_semicolon.py` around lines 44 - 53,
Refactor test_query_method_always_strips_before_limit_branch to parse TRINO_PATH
source with Python’s ast module and inspect the query method’s statement
structure. Locate the query function and verify that its body contains the
strip_trailing_semicolon assignment before the limit-condition branch, while
preserving the existing assertions for the limited-query wrapper behavior
without relying on exact source-text positions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/wren/tests/unit/test_trino_unlimited_semicolon.py`:
- Around line 28-35: Replace the fallback implementation in the import setup for
strip_trailing_semicolon with the test framework’s module-skip mechanism when
ModuleNotFoundError occurs. Remove the local trailing regex and _strip helper so
the tests only exercise the real application function when its dependencies are
available.
- Around line 44-53: Refactor
test_query_method_always_strips_before_limit_branch to parse TRINO_PATH source
with Python’s ast module and inspect the query method’s statement structure.
Locate the query function and verify that its body contains the
strip_trailing_semicolon assignment before the limit-condition branch, while
preserving the existing assertions for the limited-query wrapper behavior
without relying on exact source-text positions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f6ff3356-6cd4-427d-833a-332b89f67284

📥 Commits

Reviewing files that changed from the base of the PR and between 3d1be24 and c92c003.

📒 Files selected for processing (2)
  • core/wren/src/wren/connector/trino.py
  • core/wren/tests/unit/test_trino_unlimited_semicolon.py

@goldmedal

Copy link
Copy Markdown
Collaborator

The fix itself is the most valuable one in this group — Trino's client protocol rejects a trailing statement terminator, so the unlimited path really is broken today, and the change matches what already landed for Athena in #2535. But I can't merge it with this test.

test_trino_unlimited_semicolon.py doesn't test the connector.

  1. Both assertions are on the source text of trino.pyassert "sql = strip_trailing_semicolon(sql)" in body and assert "SELECT * FROM ({sql}) AS _sub LIMIT {limit}" in body. That passes whenever the string is present, including when the behavior is broken, and fails on any harmless refactor (renaming the local, reordering the f-string). It pins the implementation instead of the contract.
  2. _load_strip() falls back to a re-implemented copy of the regex when the module import raises ModuleNotFoundError. If that branch is taken, the test asserts against a copy of the code rather than base.py, and a green run tells us nothing about the real helper.
  3. The docstring's premise — "Importing wren.connector.trino pulls wren_core" — isn't correct. trino.py imports only stdlib, pyarrow, sqlglot, loguru and wren.* at module level (the trino driver itself is already lazy via _import_trino()), and two existing unit tests import it directly: test_trino_ssl_verify.py and test_trino_parser.py. So the workaround isn't needed.

Please replace it with a mock-cursor test in the style of the Athena one that landed with #2535 (core/wren/tests/unit/test_athena_strip_unlimited_query.py): build the connector with __new__, stub the cursor, call query("SELECT 1;"), and assert cursor.execute.assert_called_once_with("SELECT 1"). Happy to merge once the test exercises the code path.

@goldmedal

Copy link
Copy Markdown
Collaborator

Code review

Duplicate of #2592 — same file, same behaviour change, opened six days apart. I'd suggest keeping #2592 and closing this one, on test quality.

This PR's test reads trino.py and base.py as text and reloads the helper via importlib, asserting on source substrings:

"""Trino unlimited query path must strip trailing semicolons.
Importing ``wren.connector.trino`` pulls ``wren_core``. Assert on source +
stdlib-reload the strip helper from ``base.py`` the same way other native-free
connector tests pin behavior.
"""
from __future__ import annotations
import importlib.util
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
BASE_PATH = ROOT / "src" / "wren" / "connector" / "base.py"
TRINO_PATH = ROOT / "src" / "wren" / "connector" / "trino.py"
def _load_strip():
spec = importlib.util.spec_from_file_location("wren_connector_base_trino_test", BASE_PATH)

The connector is never executed, so the test passes independently of whether the code path works, and it breaks on reformatting. #2592 uses a mocked Trino module and asserts on the SQL actually handed to the cursor, which is what we want.

Both also need a rebase — this branch is 17 commits behind main.

@Bartok9

Bartok9 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Agreed — #2592's mocked-module test is the right approach over my source-substring one. Closing this in favour of #2592.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants