Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
61 changes: 61 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,61 @@
"""Postgres query must coerce LIMIT before SQL interpolation.

``postgres`` imports ``psycopg`` at module load. Unit CI does not install the
``postgres`` extra, so stub ``psycopg`` in ``sys.modules`` before importing the
connector module (same pattern as ``test_postgres_semicolon_unlimited``).
"""

from __future__ import annotations

import sys
import types
from unittest.mock import MagicMock, patch

import pytest


def _ensure_psycopg_stub() -> None:
if "psycopg" in sys.modules:
return
mod = types.ModuleType("psycopg")
errors = types.ModuleType("psycopg.errors")
sys.modules["psycopg"] = mod
sys.modules["psycopg.errors"] = errors

Comment on lines +17 to +24

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'test_postgres_coerce_limit|postgres\.py$|settings|pytest|requirements|pyproject' || true

echo "== target test =="
if [ -f core/wren/tests/unit/test_postgres_coerce_limit.py ]; then
  wc -l core/wren/tests/unit/test_postgres_coerce_limit.py
  sed -n '1,90p' core/wren/tests/unit/test_postgres_coerce_limit.py | nl -ba
fi

echo "== postgres connector references =="
if [ -f core/wren/src/wren/connector/postgres.py ]; then
  wc -l core/wren/src/wren/connector/postgres.py
  rg -n "psycopg|QueryCanceled|coerce|limit" core/wren/src/wren/connector/postgres.py || true
  echo "--- relevant context ---"
  rg -n -C 4 "psycopg|QueryCanceled|execute|limit" core/wren/src/wren/connector/postgres.py || true
fi

echo "== imports/usages of coerce_limit tests or module =="
rg -n "test_postgres_coerce_limit|psycopg\.errors|QueryCanceled|_ensure_psycopg_stub|stub_psycopg" . || true

echo "== python semantic probe =="
python3 - <<'PY'
import types, sys

mod = types.ModuleType("psycopg")
errors = types.ModuleType("psycopg.errors")
sys.modules["psycopg"] = mod
sys.modules["psycopg.errors"] = errors

try:
    errors.QueryCanceled
except AttributeError as e:
    print("empty stub missing errors.QueryCanceled:", e)

try:
    mod.QueryCanceled
except AttributeError as e:
    pass

errors.QueryCanceled = Exception
mod.errors = errors

from psycopg.errors import QueryCanceled
print("fixed stub has errors.QueryCanceled:", QueryCanceled)
PY

Repository: Canner/WrenAI

Length of output: 602


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target test =="
cat -n core/wren/tests/unit/test_postgres_coerce_limit.py

echo "== postgres connector relevant symbols/context =="
wc -l core/wren/src/wren/connector/postgres.py
rg -n -C 5 "psycopg|QueryCanceled|execute|limit|get_limit|coerce" core/wren/src/wren/connector/postgres.py || true

echo "== imports/usages =="
rg -n "test_postgres_coerce_limit|psycopg\.errors|QueryCanceled|_ensure_psycopg_stub|stub_psycopg" core/wren/tests/unit/test_postgres_coerce_limit.py core/wren/tests/connectors/test_postgres.py core/wren/src/wren/connector/postgres.py || true

echo "== python semantic probe =="
python3 - <<'PY'
import types, sys

mod = types.ModuleType("psycopg")
errors = types.ModuleType("psycopg.errors")
sys.modules["psycopg"] = mod
sys.modules["psycopg.errors"] = errors

try:
    errors.QueryCanceled
except AttributeError as e:
    print("current_stub_missing_errors_QueryCanceled")
    print(repr(e))

try:
    mod.errors
except AttributeError:
    print("current_stub_missing_mod_errors")

errors.QueryCanceled = Exception
mod.errors = errors

from psycopg.errors import QueryCanceled
print("fixed_import_has_errors_QueryCanceled")
print(QueryCanceled)
PY

Repository: Canner/WrenAI

Length of output: 9269


Complete the psycopg stub’s error surface.

core/wren/src/wren/connector/postgres.py catches psycopg.errors.QueryCanceled, but _ensure_psycopg_stub() only registers psycopg.errors without exposing it on psycopg or providing QueryCanceled. Connector errors raised during query() can trigger AttributeError while evaluating the handler.

Proposed fix
     mod = types.ModuleType("psycopg")
     errors = types.ModuleType("psycopg.errors")
+    errors.QueryCanceled = type("QueryCanceled", (Exception,), {})
+    mod.errors = errors
     sys.modules["psycopg"] = mod
     sys.modules["psycopg.errors"] = errors
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _ensure_psycopg_stub() -> None:
if "psycopg" in sys.modules:
return
mod = types.ModuleType("psycopg")
errors = types.ModuleType("psycopg.errors")
sys.modules["psycopg"] = mod
sys.modules["psycopg.errors"] = errors
def _ensure_psycopg_stub() -> None:
if "psycopg" in sys.modules:
return
mod = types.ModuleType("psycopg")
errors = types.ModuleType("psycopg.errors")
errors.QueryCanceled = type("QueryCanceled", (Exception,), {})
mod.errors = errors
sys.modules["psycopg"] = mod
sys.modules["psycopg.errors"] = errors
🤖 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_postgres_coerce_limit.py` around lines 17 - 24,
Update _ensure_psycopg_stub() to expose the created errors module as the psycopg
module’s errors attribute and define a QueryCanceled exception on it, matching
the symbol caught by the PostgreSQL connector’s query error handler.


_ensure_psycopg_stub()

from wren.connector import postgres as pg_mod # noqa: E402


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