Skip to content

refactor(connector): centralize LIMIT coercion - #2624

Open
Bartok9 wants to merge 6 commits into
Canner:mainfrom
Bartok9:fix/connector-coerce-limit-base
Open

refactor(connector): centralize LIMIT coercion#2624
Bartok9 wants to merge 6 commits into
Canner:mainfrom
Bartok9:fix/connector-coerce-limit-base

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Centralize LIMIT handling on a single coerce_limit() in connector/base.py and wire every interpolating connector through it.

User-visible change

Invalid limit values now raise a consistent ValueError instead of a driver-level error after SQL interpolation. This is a refactor of the coercion contract across connectors — not an injection fix (call paths that accept sql already trust the caller equivalently for limit).

Semantics (strictest of the prior batch)

  • None → unlimited
  • reject bool
  • reject non-integral values (e.g. -0.5 must not become LIMIT 0)
  • reject negatives
  • leave ConnectorABC.query as limit: int | None

Connectors wired

postgres, trino, clickhouse, oracle, canner, bigquery, duckdb, redshift, athena, snowflake, datafusion, and mysql (private _coerce_limit removed).

Verification

cd core/wren && .venv/bin/python -m pytest tests/unit/test_coerce_limit.py -q
8 passed

Notes

Consolidates the closed one-PR-per-connector batch (#2625#2627, #2634#2637) per maintainer direction.

Summary by CodeRabbit

  • Bug Fixes

    • Standardized query-limit validation across supported data connectors.
    • Rejects negative, fractional, boolean, non-numeric, and oversized values instead of silently converting them.
    • Preserves unlimited queries when no limit is provided.
    • Ensures consistent behavior across Athena, BigQuery, Canner, ClickHouse, DataFusion, DuckDB, MySQL, Oracle, PostgreSQL, Redshift, Snowflake, Trino, and other connectors.
  • Tests

    • Added coverage for valid limits, absent values, invalid inputs, unsafe strings, and oversized values.

Closes: n/a — inventory fix for LIMIT interpolation safety across connectors.
@github-actions github-actions Bot added python Pull requests that update Python code core labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds shared coerce_limit validation for optional query limits. Applies it across twelve connectors and adds unit tests for valid and invalid inputs.

Changes

Limit coercion

Layer / File(s) Summary
Coerce and validate limits
core/wren/src/wren/connector/base.py, core/wren/tests/unit/test_coerce_limit.py, core/wren/tests/unit/test_mysql_helpers.py
Adds coerce_limit for non-negative integer normalization. Tests cover accepted, rejected, and passthrough inputs. Existing MySQL helper tests now use the shared function.
Apply normalized limits to connectors
core/wren/src/wren/connector/{athena,bigquery,canner,clickhouse,datafusion,duckdb,mysql,oracle,postgres,redshift,snowflake,trino}.py
Normalizes limits before SQL construction. Connectors use the normalized value directly. MySQL removes its local validator.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: goldmedal

Poem

A rabbit checks each limit gate,
None stays clear; numbers normalize.
Fractions and negatives stop,
Safe values reach each query hop.
Tests confirm the bounds.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the centralization of LIMIT coercion across connectors.
Description check ✅ Passed The description covers the change, semantics, affected connectors, testing, and consolidation context, but lacks an explicit duplicate-check section.
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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@core/wren/src/wren/connector/base.py`:
- Around line 23-35: Apply coerce_limit consistently to every connector utility
that interpolates limit into SQL, including wren.connector.oracle’s ROWNUM
condition and connectors with conditional string coercion; normalize limit
before formatting while preserving None handling. Add coerce_limit to the shared
connector import/export surface so all call sites use the same non-negative
validation.
- Around line 23-32: Update the coerce_limit parameter annotation to include
string inputs alongside int and None, matching its documented and tested support
for numeric strings; leave the existing coercion and validation behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6081ff5-99b9-49e3-ba3f-90249db7e76e

📥 Commits

Reviewing files that changed from the base of the PR and between 32d76bf and dd0e407.

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

Comment thread core/wren/src/wren/connector/base.py Outdated
Comment on lines +23 to +35
def coerce_limit(limit: int | None) -> int | None:
"""Validate and coerce a user-supplied ``limit`` to a non-negative ``int``.

Connectors train-plan LIMIT by interpolating the value into SQL. ``int()``
rejects injection strings like ``"5 OR 1=1"``; negatives are rejected so
engines never see ``LIMIT -1`` (undefined / dialect-dependent).
"""
if limit is None:
return None
coerced = int(limit)
if coerced < 0:
raise ValueError(f"limit must be non-negative, got {coerced}")
return coerced

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -C 2 'coerce_limit|strip_trailing_semicolon|__all__' core/wren/src/wren
rg -n -C 3 '\bLIMIT\b|\blimit\b' core/wren/src/wren/connector

Repository: Canner/WrenAI

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== connector files =="
git ls-files 'core/wren/src/wren/connector/*.py' | sort

echo
echo "== limited query locations importing util/using limit =="
python3 - <<'PY'
from pathlib import Path
root = Path("core/wren/src/wren/connector")
for p in sorted(root.glob("*.py")):
    text = p.read_text()
    lines = text.splitlines()
    has_query_sig = any("def query(self, sql: str, limit: int | None = None)" in line for line in lines)
    if not has_query_sig:
        continue
    print(f"\n--- {p} ---")
    for i,l in enumerate(lines, start=1):
        if i in (1,23,24,25) or "def query" in l or "def _apply_limit" in l or "LIMIT" in l or "ROWNUM" in l:
            print(f"{i}: {l}")
PY

echo
echo "== coerce_limit usage/ref =="
rg -n 'from wren\.connector\.base import|coerce_limit|strip_trailing_semicolon|_apply_limit|_coerce_limit' core/wren/src/wren/connector

Repository: Canner/WrenAI

Length of output: 15604


Apply coerce_limit to every connector limit interpolation.

core/wren/src/wren/connector/base.py defines the helper, but connector utilities such as wren.connector.oracle still format limits directly (for example ROWNUM <= {limit}), and several connectors only coerce string-looking values instead of applying the shared non-negative check. Add coerce_limit to the shared import/export surface and normalize limit at each connector call site before SQL interpolation.

🤖 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/src/wren/connector/base.py` around lines 23 - 35, Apply
coerce_limit consistently to every connector utility that interpolates limit
into SQL, including wren.connector.oracle’s ROWNUM condition and connectors with
conditional string coercion; normalize limit before formatting while preserving
None handling. Add coerce_limit to the shared connector import/export surface so
all call sites use the same non-negative validation.

@goldmedal

Copy link
Copy Markdown
Collaborator

Keeping this one open — it is the right shape, and the rest of the batch (#2625, #2626, #2627, #2634, #2635, #2636, #2637) has been closed in favour of folding into it.

To land, please extend this PR to:

  1. Adopt the strictest semantics from the closed PRs: reject bool, reject non-integral values (so -0.5 cannot truncate to LIMIT 0), reject negatives.
  2. Wire every interpolating connector to coerce_limitpostgres.py:256, trino.py:489, clickhouse.py:401, oracle.py:186, canner.py:255, plus bigquery.py, duckdb.py, redshift.py, athena.py, snowflake.py, datafusion.py where the inline int(limit) should be replaced.
  3. Remove the private _coerce_limit in mysql.py:44 in favour of the shared one.
  4. Leave ConnectorABC.query as limit: int | None.
  5. Retitle as refactor(connector): centralize LIMIT coercion, and describe the user-visible change as a consistent ValueError for invalid limits rather than a driver-level error — not as an injection fix, for the call-path reasons given in the closing comments.

Please also rebase onto current main before requesting review.

@Bartok9

Bartok9 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @goldmedal — clear direction, and agreed on consolidating here.

I'll:

  1. Tighten coerce_limit to the strictest contract (reject bool, non-integral values, negatives)
  2. Wire all interpolating connectors through it (including canner.py, and drop mysql._coerce_limit)
  3. Keep ConnectorABC.query as limit: int | None
  4. Retitle/reframe as refactor(connector): centralize LIMIT coercion (consistent ValueError, not injection)
  5. Rebase onto current main and refresh tests around the shared helper + light per-connector smoke

Working on the follow-up push now.

Use one shared coerce_limit across interpolating connectors with strict
semantics (reject bool, non-integral values, negatives). Drop mysql's
private _coerce_limit. Invalid limits raise a consistent ValueError.
@Bartok9 Bartok9 changed the title fix(connector): export shared coerce_limit helper refactor(connector): centralize LIMIT coercion Aug 3, 2026

@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.

Actionable comments posted: 4

🤖 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.

Inline comments:
In `@core/wren/src/wren/connector/athena.py`:
- Line 301: Update the shared coerce_limit() helper to reject any numeric value
that would be truncated by int(), including Decimal("1.5") and Fraction(3, 2),
while preserving valid integral limits and existing validation behavior. Add
shared-helper tests covering both cases. The calls in
core/wren/src/wren/connector/athena.py:301, bigquery.py:54, canner.py:245,
clickhouse.py:395, datafusion.py:32, duckdb.py:76, and mysql.py:79 require no
direct changes because they will inherit the corrected helper behavior.

In `@core/wren/src/wren/connector/base.py`:
- Around line 53-59: Update the limit validation around the coercion logic in
the base connector so non-string numeric values are compared directly with
coerced, avoiding float conversion and ensuring Decimal("1.5") and Fraction(3,
2) are rejected instead of truncated. Preserve the documented ValueError for
oversized integers rather than allowing OverflowError to escape, and add
regression tests covering both oversized integers and fractional
Decimal/Fraction inputs.

In `@core/wren/src/wren/connector/datafusion.py`:
- Around line 31-38: Update DataFusionConnector.query to normalize the input
with strip_trailing_semicolon before branching on limit, then use that
normalized SQL for both the limited wrapper and the direct SessionContext.query
path. Preserve the existing LIMIT behavior while ensuring queries such as SELECT
1; execute consistently when no limit is provided.

In `@core/wren/src/wren/connector/duckdb.py`:
- Around line 75-77: Move the coerce_limit(limit) call in DuckDBConnector.query
below the method’s docstring so the string literal remains the first statement
and is assigned to query.__doc__; preserve the existing limit coercion behavior
after the documentation block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc073e11-0cb6-460f-8258-0e8448f77ef4

📥 Commits

Reviewing files that changed from the base of the PR and between dd0e407 and e6943ef.

📒 Files selected for processing (14)
  • core/wren/src/wren/connector/athena.py
  • core/wren/src/wren/connector/base.py
  • core/wren/src/wren/connector/bigquery.py
  • core/wren/src/wren/connector/canner.py
  • core/wren/src/wren/connector/clickhouse.py
  • core/wren/src/wren/connector/datafusion.py
  • core/wren/src/wren/connector/duckdb.py
  • core/wren/src/wren/connector/mysql.py
  • core/wren/src/wren/connector/oracle.py
  • core/wren/src/wren/connector/postgres.py
  • core/wren/src/wren/connector/redshift.py
  • core/wren/src/wren/connector/snowflake.py
  • core/wren/src/wren/connector/trino.py
  • core/wren/tests/unit/test_coerce_limit.py

Comment thread core/wren/src/wren/connector/athena.py
Comment thread core/wren/src/wren/connector/base.py Outdated
Comment thread core/wren/src/wren/connector/datafusion.py
Comment thread core/wren/src/wren/connector/duckdb.py
…ce_limit

CI: ruff format wanted one-line LIMIT SQL in datafusion/redshift; unit
collection broke after removing mysql._coerce_limit — point helpers tests
at base.coerce_limit.

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@core/wren/tests/unit/test_mysql_helpers.py`:
- Around line 44-68: Update coerce_limit to validate non-string numeric inputs
for exact integrality and reject negative values before calling int(), so
Decimal("1.5"), Fraction(3, 2), and Decimal("-0.5") raise ValueError while
existing None, integer, and numeric-string behavior remains unchanged. Add
shared-helper regression tests covering fractional Decimal/Fraction values and
negative fractional values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fc48f565-2880-4687-baae-b565983e60de

📥 Commits

Reviewing files that changed from the base of the PR and between e6943ef and a6b5abb.

📒 Files selected for processing (3)
  • core/wren/src/wren/connector/datafusion.py
  • core/wren/src/wren/connector/redshift.py
  • core/wren/tests/unit/test_mysql_helpers.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/wren/src/wren/connector/redshift.py
  • core/wren/src/wren/connector/datafusion.py

Comment thread core/wren/tests/unit/test_mysql_helpers.py
Reject Decimal/Fraction truncation without float() overflow side paths.
Keep DuckDB query docstring first; strip DataFusion SQL on both limit paths.

@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 (1)
core/wren/tests/unit/test_coerce_limit.py (1)

66-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the test to match its assertion.

test_oversized_int_is_value_error_not_overflow suggests that coerce_limit(huge) raises ValueError, but Line 69 verifies that the integer is preserved. Rename the test and update the comment to describe the no-float-overflow behavior.

Proposed clarification
-def test_oversized_int_is_value_error_not_overflow() -> None:
+def test_preserves_oversized_int_without_float_overflow() -> None:
     huge = 10**400
-    # int stays int; ensure path still returns or rejects cleanly as ValueError only on bad types
+    # Preserve arbitrary-size integers without converting through float.
     assert coerce_limit(huge) == huge
🤖 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_coerce_limit.py` around lines 66 - 69, Rename
test_oversized_int_is_value_error_not_overflow to reflect that coerce_limit
preserves oversized integers, and update its comment to describe avoiding float
overflow while retaining the integer value. Keep the existing assertion
unchanged.
🤖 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_coerce_limit.py`:
- Around line 66-69: Rename test_oversized_int_is_value_error_not_overflow to
reflect that coerce_limit preserves oversized integers, and update its comment
to describe avoiding float overflow while retaining the integer value. Keep the
existing assertion unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e87e125-ea8c-4853-8362-c5fe9e015abe

📥 Commits

Reviewing files that changed from the base of the PR and between a6b5abb and e72f250.

📒 Files selected for processing (4)
  • core/wren/src/wren/connector/base.py
  • core/wren/src/wren/connector/datafusion.py
  • core/wren/src/wren/connector/duckdb.py
  • core/wren/tests/unit/test_coerce_limit.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/wren/src/wren/connector/base.py
  • core/wren/src/wren/connector/duckdb.py
  • core/wren/src/wren/connector/datafusion.py

Bartok9 added 2 commits August 3, 2026 01:18
Keep CI lint green (I001 isort on connector imports) and pin Decimal("-0.5")
regression coverage for the shared helper / mysql helper suite.
CodeRabbit strip-on-both-paths change correctly strips trailing
semicolons without LIMIT; update unit expectation. Rename oversized-int
test to match preserve-not-raise behavior.
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