Skip to content

Commit 7b88305

Browse files
committed
fix(review): resolve Neon quality comments
1 parent ef53b10 commit 7b88305

3 files changed

Lines changed: 104 additions & 14 deletions

File tree

app/db/readonly.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ def readonly_connection() -> Iterator[psycopg.Connection[Any]]:
6565
raise RuntimeError("DATABASE_URL is not configured.")
6666

6767
with psycopg.connect(database_url, row_factory=dict_row) as conn:
68-
conn.execute("SET default_transaction_read_only = on")
69-
yield conn
68+
with conn.transaction(read_only=True):
69+
yield conn
7070

7171

7272
def fetch_all(sql: str, params: tuple[Any, ...] | None = None) -> list[dict[str, Any]]:

scripts/sync_vercel_neon_env.ps1

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Sync Neon DATABASE_URL to Vercel environments for branch `vercel-dev`.
22
# Requires: vercel CLI logged in, project linked (`vercel link`).
3-
# Usage: pwsh scripts/sync_vercel_neon_env.ps1 [-ProjectName <org/team scope>]
4-
# Note: `-ProjectName` maps to Vercel's `--scope` flag (team/org scope).
3+
# Usage: pwsh scripts/sync_vercel_neon_env.ps1 [-VercelScope <org/team scope>]
4+
# Note: `-VercelScope` maps to Vercel's `--scope` flag (team/org scope).
55

66
param(
7-
[string]$ProjectName = ""
7+
[string]$VercelScope = ""
88
)
99

1010
$ErrorActionPreference = "Stop"
@@ -19,6 +19,9 @@ Get-Content $envFile | ForEach-Object {
1919
if ($_ -match '^\s*([^#=]+)=(.*)$') {
2020
$name = $matches[1].Trim()
2121
$value = $matches[2].Trim()
22+
if ($value -match '^"(.*)"$' -or $value -match "^'(.*)'$") {
23+
$value = $matches[1]
24+
}
2225
Set-Item -Path "env:$name" -Value $value
2326
}
2427
}
@@ -28,16 +31,16 @@ if (-not $env:DATABASE_URL_VERCEL_DEV) {
2831
}
2932

3033
$vercelArgs = @("env", "add", "DATABASE_URL", "preview", "--force")
31-
if ($ProjectName) {
32-
$vercelArgs += @("--scope", $ProjectName)
34+
if ($VercelScope) {
35+
$vercelArgs += @("--scope", $VercelScope)
3336
}
3437

3538
Write-Host "Setting Vercel preview DATABASE_URL from NEON vercel-dev branch..."
3639
$env:DATABASE_URL_VERCEL_DEV | vercel @vercelArgs
3740

3841
$prodArgs = @("env", "add", "DATABASE_URL", "production", "--force")
39-
if ($ProjectName) {
40-
$prodArgs += @("--scope", $ProjectName)
42+
if ($VercelScope) {
43+
$prodArgs += @("--scope", $VercelScope)
4144
}
4245

4346
if ($env:DATABASE_URL) {

tests/app/test_readonly_db.py

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,26 @@
44

55
import pytest
66

7-
from app.db.readonly import assert_select_only
7+
import app.db.readonly as readonly_module
8+
from app.db.readonly import (
9+
assert_select_only,
10+
fetch_all,
11+
fetch_one,
12+
readonly_connection,
13+
)
814

915

1016
def test_assert_select_only_allows_select():
1117
assert_select_only("SELECT 1")
1218

1319

1420
def test_assert_select_only_allows_with_and_leading_comments():
15-
sql = textwrap.dedent(
16-
"""
21+
sql = textwrap.dedent("""
1722
-- leading comment
1823
/* block comment */
1924
WITH x AS (SELECT 1 AS id)
2025
SELECT id FROM x
21-
"""
22-
)
26+
""")
2327
assert_select_only(sql)
2428

2529

@@ -52,3 +56,86 @@ def test_assert_select_only_rejects_writes():
5256
def test_assert_select_only_rejects_dangerous_and_multi_statement_sql(sql: str):
5357
with pytest.raises(ValueError, match="single read-only SELECT or WITH query"):
5458
assert_select_only(sql)
59+
60+
61+
def test_readonly_helpers_use_read_only_transaction_and_params(
62+
monkeypatch: pytest.MonkeyPatch,
63+
) -> None:
64+
calls: list[tuple[str, object]] = []
65+
66+
class _FakeTransaction:
67+
def __enter__(self) -> None:
68+
calls.append(("transaction_enter", True))
69+
70+
def __exit__(self, exc_type, exc, tb) -> bool:
71+
calls.append(("transaction_exit", exc_type))
72+
return False
73+
74+
class _FakeCursor:
75+
def __enter__(self) -> "_FakeCursor":
76+
calls.append(("cursor_enter", None))
77+
return self
78+
79+
def __exit__(self, exc_type, exc, tb) -> bool:
80+
calls.append(("cursor_exit", exc_type))
81+
return False
82+
83+
def execute(self, sql: str, params: tuple[object, ...]) -> None:
84+
calls.append(("execute", (sql, params)))
85+
86+
def fetchall(self) -> list[dict[str, int]]:
87+
return [{"id": 1}, {"id": 2}]
88+
89+
class _FakeConnection:
90+
def __enter__(self) -> "_FakeConnection":
91+
calls.append(("connect_enter", None))
92+
return self
93+
94+
def __exit__(self, exc_type, exc, tb) -> bool:
95+
calls.append(("connect_exit", exc_type))
96+
return False
97+
98+
def transaction(self, read_only: bool = False) -> _FakeTransaction:
99+
calls.append(("transaction", read_only))
100+
return _FakeTransaction()
101+
102+
def cursor(self) -> _FakeCursor:
103+
return _FakeCursor()
104+
105+
monkeypatch.setenv("DATABASE_URL", "postgresql://example")
106+
monkeypatch.setattr(
107+
readonly_module.psycopg,
108+
"connect",
109+
lambda *args, **kwargs: _FakeConnection(),
110+
)
111+
112+
with readonly_connection() as conn:
113+
assert isinstance(conn, _FakeConnection)
114+
115+
rows = fetch_all(" SELECT * FROM memories WHERE id = %s", (7,))
116+
assert rows == [{"id": 1}, {"id": 2}]
117+
118+
first = fetch_one(
119+
"WITH x AS (SELECT 1 AS id) SELECT id FROM x WHERE id = %s",
120+
(1,),
121+
)
122+
assert first == {"id": 1}
123+
124+
assert ("transaction", True) in calls
125+
assert (
126+
"execute",
127+
(" SELECT * FROM memories WHERE id = %s", (7,)),
128+
) in calls
129+
assert (
130+
"execute",
131+
("WITH x AS (SELECT 1 AS id) SELECT id FROM x WHERE id = %s", (1,)),
132+
) in calls
133+
134+
135+
def test_readonly_connection_requires_database_url(
136+
monkeypatch: pytest.MonkeyPatch,
137+
) -> None:
138+
monkeypatch.delenv("DATABASE_URL", raising=False)
139+
with pytest.raises(RuntimeError, match="DATABASE_URL is not configured"):
140+
with readonly_connection():
141+
pass

0 commit comments

Comments
 (0)