Files
fn_registry/python/functions/infra/pg_apply_sql.py
T
egutierrez e1e9bb7499 feat(shell): auto-commit con 31 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 23:55:16 +02:00

69 lines
2.3 KiB
Python

"""Apply a .sql script file against a PostgreSQL database via psycopg2."""
from __future__ import annotations
from pathlib import Path
def pg_apply_sql(dsn: str, sql_path: str) -> int:
"""Read a .sql file and execute its full contents against PostgreSQL.
The whole script is sent in a single cursor.execute call. psycopg2 runs
multi-statement scripts in one execute when there are no bound parameters,
so DDL files with several statements separated by ";" apply atomically in
one transaction. Designed for idempotent migrations (the SQL itself uses
"IF NOT EXISTS"). Commits on success.
Args:
dsn: Connection string, e.g. "postgresql://user:pass@host:port/dbname".
sql_path: Path to the .sql file to apply (e.g. db/migrations/001_init.sql).
Returns:
Number of non-empty statements applied (counted by splitting on ";").
At minimum 1 when the script is non-empty.
Raises:
RuntimeError: If the file cannot be read, or the connection / execution
fails. The original exception is chained for debugging.
"""
path = Path(sql_path)
try:
script = path.read_text(encoding="utf-8")
except OSError as exc:
raise RuntimeError(
f"pg_apply_sql could not read {sql_path!r}: {exc}"
) from exc
if not script.strip():
return 0
# Lazy import so the module loads even without psycopg2 installed.
try:
import psycopg2
except ImportError as exc: # pragma: no cover - exercised only without dep
raise RuntimeError(
"psycopg2 is required for pg_apply_sql; install psycopg2-binary"
) from exc
# Best-effort statement count (informational return value only). Strip
# blank fragments produced by a trailing semicolon.
statement_count = sum(1 for part in script.split(";") if part.strip())
statement_count = max(statement_count, 1)
conn = None
try:
conn = psycopg2.connect(dsn)
with conn.cursor() as cur:
cur.execute(script)
conn.commit()
return statement_count
except Exception as exc:
if conn is not None:
conn.rollback()
raise RuntimeError(
f"pg_apply_sql failed applying {sql_path!r}: {exc}"
) from exc
finally:
if conn is not None:
conn.close()