dot_files/dot_claude/skills/asyncpg-string-timestamp-param/SKILL.md
2026-03-14 13:20:20 +00:00

4.3 KiB

name description author version date
asyncpg-string-timestamp-param 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. Claude Code 1.0.0 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:

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:
    since_str = since_str.replace(" ", "+")  # Fix URL-decoded +
    since_dt = datetime.fromisoformat(since_str)
    

Verification

# 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:

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)