archive 11 stale skills: move to skills/archived/ to match local layout
This commit is contained in:
parent
bf4b5a9a94
commit
eb19c1c27d
11 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
name: asyncpg-string-timestamp-param
|
||||
description: |
|
||||
Fix for asyncpg.exceptions.DataError "invalid input for query argument: expected a
|
||||
datetime.date or datetime.datetime instance, got 'str'" when passing ISO timestamp
|
||||
strings to TIMESTAMPTZ query parameters. Use when: (1) asyncpg query fails with
|
||||
DataError on a timestamp parameter, (2) the same query works in psql or SQLAlchemy
|
||||
because PostgreSQL accepts string casts, (3) using FastAPI query params or any string
|
||||
source for timestamp values passed to asyncpg conn.fetch/fetchrow/execute. Root cause:
|
||||
asyncpg uses binary protocol with strict Python type binding, unlike text-based SQL
|
||||
clients. Also covers the URL-encoding gotcha where + in +00:00 becomes a space in
|
||||
query parameters.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-03-14
|
||||
---
|
||||
|
||||
# asyncpg: String Timestamp Parameter Rejection
|
||||
|
||||
## Problem
|
||||
|
||||
asyncpg rejects ISO timestamp strings (e.g., `"2026-03-14T10:00:00+00:00"`) when
|
||||
passed as query arguments for `TIMESTAMPTZ` columns, even though PostgreSQL itself
|
||||
accepts them in raw SQL via implicit casting.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
|
||||
- Error: `asyncpg.exceptions.DataError: invalid input for query argument $N: '<iso_string>' (expected a datetime.date or datetime.datetime instance, got 'str')`
|
||||
- Using asyncpg directly (not through SQLAlchemy ORM which handles conversion)
|
||||
- Passing timestamps from FastAPI query params, HTTP headers, or JSON request bodies
|
||||
- The query works fine in psql, pgAdmin, or any text-protocol client
|
||||
- Common with `WHERE updated_at > $1::timestamptz` or similar patterns
|
||||
- Even `$1::timestamptz` cast in the SQL doesn't help — asyncpg validates the Python type before sending
|
||||
|
||||
## Solution
|
||||
|
||||
Parse the ISO string to a `datetime` object before passing to asyncpg:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
# Before (fails):
|
||||
await conn.fetch("SELECT * FROM t WHERE updated_at > $1", since_str)
|
||||
|
||||
# After (works):
|
||||
since_dt = datetime.fromisoformat(since_str)
|
||||
await conn.fetch("SELECT * FROM t WHERE updated_at > $1", since_dt)
|
||||
```
|
||||
|
||||
Remove the `::timestamptz` cast from the SQL — it's unnecessary when passing a proper
|
||||
`datetime` object, and misleading since it suggests the cast would handle string conversion.
|
||||
|
||||
### URL-Encoding Gotcha
|
||||
|
||||
When accepting timestamps via URL query parameters (e.g., `?since=2026-03-14T10:00:00+00:00`),
|
||||
the `+` sign is URL-decoded to a space, producing `2026-03-14T10:00:00 00:00` which is
|
||||
an invalid ISO format.
|
||||
|
||||
**Solutions (pick one):**
|
||||
1. Use `Z` suffix instead of `+00:00` for UTC: `?since=2026-03-14T10:00:00Z`
|
||||
2. URL-encode the `+` as `%2B`: `?since=2026-03-14T10:00:00%2B00:00`
|
||||
3. Accept both formats in the API:
|
||||
```python
|
||||
since_str = since_str.replace(" ", "+") # Fix URL-decoded +
|
||||
since_dt = datetime.fromisoformat(since_str)
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```python
|
||||
# Test that the parameter is accepted
|
||||
from datetime import datetime, timezone
|
||||
dt = datetime.fromisoformat("2026-03-14T10:00:00Z")
|
||||
assert isinstance(dt, datetime)
|
||||
# This should now work without DataError
|
||||
rows = await conn.fetch("SELECT * FROM memories WHERE updated_at > $1", dt)
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
FastAPI endpoint accepting a timestamp query parameter:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@app.get("/api/items/sync")
|
||||
async def sync_items(since: Optional[str] = None):
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
if since:
|
||||
since_dt = datetime.fromisoformat(since)
|
||||
rows = await conn.fetch(
|
||||
"SELECT * FROM items WHERE updated_at > $1 ORDER BY updated_at",
|
||||
since_dt,
|
||||
)
|
||||
else:
|
||||
rows = await conn.fetch("SELECT * FROM items ORDER BY updated_at")
|
||||
return {"items": rows}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This only affects asyncpg's binary protocol. SQLAlchemy ORM, psycopg2, and psql all
|
||||
use text protocol where PostgreSQL handles string-to-timestamp conversion automatically.
|
||||
- `datetime.fromisoformat()` was significantly improved in Python 3.11+ to handle more
|
||||
ISO 8601 formats, including `Z` suffix. On Python < 3.11, `Z` must be replaced with
|
||||
`+00:00` manually.
|
||||
- asyncpg also rejects strings for `DATE`, `TIME`, `INTERVAL`, and other temporal types.
|
||||
Always pass proper Python objects (`datetime.date`, `datetime.time`, `datetime.timedelta`).
|
||||
|
||||
See also: `asyncpg-sqlalchemy-temp-table` (different asyncpg gotcha: temp tables in autocommit mode)
|
||||
Loading…
Add table
Add a link
Reference in a new issue