ProvenPin Install

pandas to_sql and read_sql with a SQLAlchemy 2 engine on sqlite, with dtype handling: working versions (tested 2026-10-15)

Write and read DataFrames through a SQLAlchemy 2 engine, control column types with dtype, and handle datetimes and nullable ints, tested offline on in-memory sqlite.

passed6 passed · 0 failed2026-10-15 15:02 UTC
Status
passed
Last passed
2026-10-15 15:02 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions10
pandas@3.0.6sqlalchemy@2.1.4pytest@9.1.1numpy@2.3.4python-dateutil@2.9.0.post0six@1.17.0typing-extensions@4.15.0iniconfig@2.1.0pluggy@1.6.0pygments@2.19.2
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38755782594

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pandas-sqlalchemy-to-sql") (install), or see them on GitHub.

requirements.txt
pandas==3.0.6
sqlalchemy==2.1.4
pytest==9.1.1
numpy==2.3.4
python-dateutil==2.9.0.post0
six==1.17.0
typing-extensions==4.15.0
iniconfig==2.1.0
pluggy==1.6.0
pygments==2.19.2
db.py
import pandas as pd
import sqlalchemy as sa
from sqlalchemy.pool import StaticPool

def make_engine(url: str = "sqlite://") -> sa.Engine:
    # StaticPool keeps one shared connection, so every session sees the same in-memory DB
    return sa.create_engine(url, poolclass=StaticPool)

def save_frame(df: pd.DataFrame, engine: sa.Engine, table: str,
               dtype: dict | None = None, if_exists: str = "replace") -> int | None:
    # index=False: pandas defaults to index=True and would add an "index" column
    # engine.begin() commits on exit; engine.connect() alone would roll back
    with engine.begin() as conn:
        return df.to_sql(table, conn, index=False, dtype=dtype, if_exists=if_exists)

def load_frame(engine: sa.Engine, query: str, **kwargs) -> pd.DataFrame:
    # SQLAlchemy 2 requires text() for raw SQL strings; bind params via params=, never f-strings
    with engine.connect() as conn:
        return pd.read_sql(sa.text(query), conn, **kwargs)
tests/test_db.py
import pandas as pd
import pytest
import sqlalchemy as sa
from db import make_engine, save_frame, load_frame

def orders():
    return pd.DataFrame({
        "sku": ["A1", "B2", "C3"],
        "qty": pd.array([1, None, 3], dtype="Int64"),
        "price": [9.5, 4.0, 12.25],
        "ts": pd.to_datetime(["2024-01-01 10:00", "2024-01-02 11:30", "2024-01-03 09:15"]),
    })

@pytest.fixture
def engine():
    return make_engine()

def test_no_index_column_written(engine):
    save_frame(orders(), engine, "orders")
    cols = [c["name"] for c in sa.inspect(engine).get_columns("orders")]
    assert cols == ["sku", "qty", "price", "ts"]

def test_dtype_controls_column_types(engine):
    save_frame(orders(), engine, "orders", dtype={"sku": sa.String(10), "qty": sa.Integer})
    types = {c["name"]: str(c["type"]) for c in sa.inspect(engine).get_columns("orders")}
    assert types["sku"] == "VARCHAR(10)" and types["qty"] == "INTEGER"

def test_nullable_int_roundtrip(engine):
    save_frame(orders(), engine, "orders")
    # Without dtype_backend="numpy_nullable", NULLs come back as float NaN, not Int64 <NA>
    back = load_frame(engine, "SELECT * FROM orders ORDER BY sku", dtype_backend="numpy_nullable")
    assert str(back["qty"].dtype) == "Int64"
    assert back["qty"].isna().tolist() == [False, True, False]

def test_datetimes_are_text_until_parse_dates(engine):
    save_frame(orders(), engine, "orders")
    # sqlite has no datetime type, so pandas stores timestamps as text
    raw = load_frame(engine, "SELECT ts FROM orders")
    assert isinstance(raw["ts"].iloc[0], str)
    parsed = load_frame(engine, "SELECT ts FROM orders", parse_dates=["ts"])
    assert pd.api.types.is_datetime64_any_dtype(parsed["ts"])

def test_bound_params_instead_of_string_formatting(engine):
    save_frame(orders(), engine, "orders")
    out = load_frame(engine, "SELECT sku FROM orders WHERE sku = :s", params={"s": "B2"})
    assert out["sku"].tolist() == ["B2"]

def test_if_exists_fail_raises(engine):
    save_frame(orders(), engine, "orders")
    with pytest.raises(ValueError):
        save_frame(orders(), engine, "orders", if_exists="fail")
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.