Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions core/wren/src/wren/connector/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ def _coerce_decimal(value: PyDecimal | None, target_type: pa.DataType):
return pa.array(values, type=arrow_type, from_pandas=True)


def _coerce_limit(limit: int | None) -> int | None:
"""Validate and coerce a user-supplied ``limit`` to a non-negative ``int``.

``int(limit)`` rejects strings like ``"5 OR 1=1"`` so the value can be
safely interpolated into SQL. Negative limits are also rejected.
"""
if limit is None:
return None
coerced = int(limit)
if coerced < 0:
raise ValueError(f"limit must be non-negative, got {coerced}")
return coerced
Comment on lines +223 to +234

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate negativity before integer truncation.

int(-0.5) produces 0, so the current check allows a negative runtime value and emits LIMIT 0. Check the original numeric value before coercion, or reject non-integral limits explicitly; add a regression test for limit=-0.5.

🤖 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/postgres.py` around lines 223 - 234, Update
_coerce_limit so negative numeric inputs are rejected before int() truncates
them; alternatively reject non-integral limits explicitly while preserving safe
coercion for valid values. Add a regression test verifying limit=-0.5 raises
ValueError rather than producing LIMIT 0.



class PostgresConnector(ConnectorABC):
"""Native psycopg3 implementation of the Wren postgres connector."""

Expand Down Expand Up @@ -251,6 +265,7 @@ def __init__(self, connection_info):
def query(self, sql: str, limit: int | None = None) -> pa.Table:
# Strip terminating ``;`` even when no LIMIT wrapper is applied so
# client-pasted statements match dry_run / limited composition rules.
limit = _coerce_limit(limit)
sql = strip_trailing_semicolon(sql)
if limit is not None:
sql = f"SELECT * FROM ({sql}) AS _sub LIMIT {limit}"
Expand Down
42 changes: 42 additions & 0 deletions core/wren/tests/unit/test_postgres_coerce_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Postgres query must coerce LIMIT before SQL interpolation."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from wren.connector import postgres as pg_mod


def _connector():
c = object.__new__(pg_mod.PostgresConnector)
c.connection = MagicMock()
c._closed = False
return c


def test_reject_negative_limit():
c = _connector()
with pytest.raises(ValueError, match="non-negative"):
c.query("SELECT 1", limit=-1)


def test_reject_injection_string():
c = _connector()
with pytest.raises(ValueError):
c.query("SELECT 1", limit="1; DROP TABLE t")


def test_numeric_string_limit_interpolated():
c = _connector()
cursor = MagicMock()
cursor.__enter__ = MagicMock(return_value=cursor)
cursor.__exit__ = MagicMock(return_value=False)
c.connection.cursor.return_value = cursor
with patch.object(pg_mod, "_build_pg_arrow_table", return_value="tbl"):
out = c.query("SELECT 1", limit="3")
assert out == "tbl"
executed = cursor.execute.call_args[0][0]
assert "LIMIT 3" in executed
assert "DROP" not in executed
Loading