--- name: detect_declared_keys_duckdb kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def detect_declared_keys_duckdb(db_path: str, table: str = None) -> dict" description: "Detecta las claves DECLARADAS (constraints reales) de un schema DuckDB leyendo la table function duckdb_constraints(): extrae PRIMARY KEY, FOREIGN KEY y UNIQUE (ignora NOT NULL y CHECK) y las devuelve normalizadas con sus columnas, y para las FK con su tabla y columnas referenciadas. Con table=None procesa todas las tablas; con table='X' filtra a PK/UNIQUE de X y a FK cuyo origen es X (case-sensitive). A diferencia de infer_fk_containment_duckdb (que INFIERE FKs candidatas por containment de valores cuando el schema no las declara), esta funcion devuelve las relaciones de clave REALES del schema. Estilo dict-no-throw: nunca lanza. Parte del grupo eda (relaciones de clave)." tags: [eda, duckdb, datascience, relations, primary-key, foreign-key, schema, exploratory-data-analysis] params: - name: db_path desc: "Ruta al archivo DuckDB. Debe existir (lectura read-only via duckdb_query_readonly; no se crea). Un path inexistente devuelve {status:'error', ...}." - name: table desc: "Si se pasa, filtra los resultados a esa tabla: incluye PRIMARY KEY y UNIQUE cuya tabla sea `table`, y FOREIGN KEY cuya tabla ORIGEN sea `table` (no la referenciada). None (default) devuelve los constraints de todas las tablas. La comparacion es case-sensitive (nombres tal cual los devuelve DuckDB)." output: "dict dict-no-throw. En exito {status:'ok', primary_keys:[{table:str, columns:[str,...]}, ...], foreign_keys:[{table:str, columns:[str,...], referenced_table:str, referenced_columns:[str,...]}, ...], unique:[{table:str, columns:[str,...]}, ...], tables:[str,...]} donde tables es la lista ordenada de tablas (origen) que poseen al menos un constraint PK/FK/UNIQUE emitido. Solo se emiten constraints de clave: NOT NULL y CHECK se ignoran. En error {status:'error', error:str}." uses_functions: [duckdb_query_readonly_py_infra] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: true tests: ["test_golden_detecta_pks_y_fk", "test_golden_ignora_not_null_y_check", "test_edge_filtra_por_tabla_orders", "test_edge_filtra_por_tabla_customers", "test_edge_unique_declarado", "test_edge_sin_constraints_listas_vacias", "test_error_db_inexistente_no_lanza", "test_shape_resultado"] test_file_path: "python/functions/datascience/detect_declared_keys_duckdb_test.py" file_path: "python/functions/datascience/detect_declared_keys_duckdb.py" --- ## Ejemplo ```python import sys, os, duckdb sys.path.insert(0, os.path.join("python", "functions")) from datascience import detect_declared_keys_duckdb # Base de ejemplo en /tmp: orders.customer_id -> customers.id (FK declarada) path = "/tmp/declared_keys_demo.duckdb" if os.path.exists(path): os.remove(path) con = duckdb.connect(path) con.execute("CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT)") con.execute( "CREATE TABLE orders(" " id INTEGER PRIMARY KEY," " customer_id INTEGER REFERENCES customers(id)," " amt DOUBLE)" ) con.close() res = detect_declared_keys_duckdb(path) if res["status"] == "ok": for pk in res["primary_keys"]: print(f"PK {pk['table']}({', '.join(pk['columns'])})") for fk in res["foreign_keys"]: print(f"FK {fk['table']}({', '.join(fk['columns'])}) -> " f"{fk['referenced_table']}({', '.join(fk['referenced_columns'])})") # PK customers(id) # PK orders(id) # FK orders(customer_id) -> customers(id) else: print("error:", res["error"]) # Filtrar a una tabla concreta (PK/UNIQUE de orders + FK con origen orders): solo_orders = detect_declared_keys_duckdb(path, table="orders") print(solo_orders["tables"]) # ['orders'] ``` ## Cuando usarla - Cuando exploras un esquema DuckDB y quieres mostrar las relaciones de clave REALES (PK/FK/UNIQUE) que el schema ha declarado, sin inferir nada. - Como paso del capitulo RELACIONES del grupo `eda`: primero mira las claves declaradas con esta funcion; si el schema no declara FKs, complementa con `infer_fk_containment_duckdb` (inferencia por containment). - Antes de documentar o migrar un esquema, para listar el contrato de integridad referencial que el motor ya conoce. - Para validar que las constraints que esperas (esa FK que creaste con `REFERENCES`) realmente estan declaradas en la base materializada. ## Gotchas - **Impura**: lee de disco via la primitiva read-only `duckdb_query_readonly` (no crea ni modifica la base). El `db_path` debe existir; un path inexistente devuelve `{status:'error'}` (read_only NO crea la base). - **Requiere `duckdb_constraints()`**: usa la table function `duckdb_constraints()`, disponible en DuckDB modernos (verificado en 1.5.2). En versiones antiguas sin esa funcion, la query falla y se devuelve `{status:'error'}`. - **Solo claves DECLARADAS**: devuelve lo que el schema declaro con `PRIMARY KEY` / `FOREIGN KEY (... REFERENCES ...)` / `UNIQUE`. Una tabla materializada con `CREATE TABLE AS SELECT` NO lleva constraints — para esos casos no habra claves que mostrar y hay que INFERIRLAS (`infer_fk_containment_duckdb`). - **NOT NULL y CHECK se ignoran**: `duckdb_constraints()` tambien emite filas `NOT NULL` (DuckDB genera una por cada columna PK) y `CHECK`; esta funcion las descarta y solo conserva PK/FK/UNIQUE. - **Nombres case-sensitive**: el filtro `table='Orders'` no casa con una tabla `orders`. Se comparan los nombres tal cual los devuelve DuckDB. - **FK atribuida al origen**: una FOREIGN KEY se atribuye a su tabla ORIGEN (el `table` de la entrada), no a la referenciada. El filtro `table='X'` trae las FK cuyo origen es X, no las que apuntan a X. - **`tables` = tablas dueñas de constraints emitidos**: la lista `tables` contiene solo las tablas que poseen al menos un PK/FK/UNIQUE en el resultado (su campo `table`), ordenadas. No incluye tablas referenciadas que no tengan constraint propio en la salida. - **Columnas como listas**: `constraint_column_names` y `referenced_column_names` son columnas LIST de DuckDB; en 1.5.2 llegan como listas Python. La funcion las normaliza a listas de strings con una red de seguridad por si llegaran como string. ## Notas `duckdb_constraints()` devuelve una fila por constraint con los campos `table_name`, `constraint_type`, `constraint_column_names`, `referenced_table`, `referenced_column_names`. Mapeo a la salida: ```text PRIMARY KEY -> primary_keys[]: {table, columns} UNIQUE -> unique[]: {table, columns} FOREIGN KEY -> foreign_keys[]: {table, columns, referenced_table, referenced_columns} NOT NULL -> ignorado CHECK -> ignorado ``` Para una FK, `referenced_table` y `referenced_column_names` vienen poblados; para PK/UNIQUE, `referenced_table` es NULL y `referenced_column_names` una lista vacia. Complementa a `infer_fk_containment_duckdb`: esta funcion devuelve las relaciones de clave REALES del schema (declaradas); la otra INFIERE FKs candidatas por containment de valores cuando el schema no las declaro. En el capitulo RELACIONES de AutomaticEDA se usan en orden: primero las declaradas, luego la inferencia como respaldo.