"""Devuelve el schema (columnas y tipos) de una tabla DuckDB en modo solo lectura.
Funcion impura: abre un archivo DuckDB con `duckdb.connect(db_path, read_only=True)`,
de modo que nunca crea ni modifica la base. La conexion se cierra siempre en un
bloque try/finally. Ejecuta `DESCRIBE
` (con el identificador de tabla
validado y citado, ya que DESCRIBE no admite parametros posicionales) y devuelve
las columnas con su tipo DuckDB. Devuelve un dict sin lanzar excepciones,
siguiendo el estilo del grupo duckdb del registry: {status:'ok', ...} en exito y
{status:'error', error:str} en fallo.
Complementa a `duckdb_list_tables_py_infra` (que tablas hay) y a
`duckdb_query_readonly_py_infra` (lectura de filas). Es la introspeccion de
columnas del grupo duckdb, util para mapear tipos a otro motor (p.ej. PostgreSQL).
"""
import re
# Un identificador de tabla valido: letras, digitos y guion bajo, sin empezar por
# digito. Suficiente para tablas creadas por el propio ecosistema; rechaza
# cualquier cosa que pudiera inyectarse en el DESCRIBE (que no admite parametros).
_VALID_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
def duckdb_table_schema(db_path: str, table: str) -> dict:
"""Devuelve el schema de una tabla DuckDB en modo solo lectura.
Args:
db_path: ruta al archivo DuckDB. Debe existir: el modo read_only NO crea
la base. Un path inexistente devuelve {status:'error', ...}.
table: nombre de la tabla a inspeccionar. Se valida contra
^[A-Za-z_][A-Za-z0-9_]*$ antes de interpolarlo en el DESCRIBE (que no
admite parametros posicionales). Un identificador invalido devuelve
{status:'error', ...} sin tocar la base.
Returns:
dict. En exito: {status:'ok', table:str, columns:[{name:str, type:str},...]}
donde type es el tipo DuckDB tal cual lo reporta el motor (BIGINT, DOUBLE,
VARCHAR, ...). En error (sin lanzar): {status:'error', error:str}.
"""
if not isinstance(table, str) or not _VALID_IDENT.match(table):
return {
"status": "error",
"error": f"invalid table identifier: {table!r}",
}
conn = None
try:
conn = __import__("duckdb").connect(db_path, read_only=True)
# DESCRIBE no admite parametros; el identificador ya esta validado y se
# cita con dobles comillas (escapando comillas internas, imposible aqui
# por el regex pero defensivo).
quoted = '"' + table.replace('"', '""') + '"'
rows = conn.execute(f"DESCRIBE {quoted}").fetchall()
# DESCRIBE devuelve: (column_name, column_type, null, key, default, extra)
columns = [{"name": row[0], "type": row[1]} for row in rows]
return {"status": "ok", "table": table, "columns": columns}
except Exception as e: # noqa: BLE001
return {"status": "error", "error": str(e)}
finally:
if conn is not None:
conn.close()