"""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()