--- id: build_join_graph_py_datascience name: build_join_graph kind: function lang: py domain: datascience version: "1.0.0" purity: pure signature: "def build_join_graph(fk_candidates: list, tables: list = None) -> dict" description: "Construye un grafo de relaciones inter-tabla a partir de FK candidatas (salida fk_candidates de infer_fk_containment_duckdb): nodos con grados y rol (fact/dimension/bridge/standalone), aristas por FK, hubs (candidatas a tabla de hechos) y un diagrama Mermaid graph LR pegable. Funcion pura, sin deps externas, no muta el input." tags: [eda, relations, join, schema, graph, mermaid, star-schema, datascience] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" imports: [] example: | from datascience import build_join_graph fks = [ {"from_table": "orders", "from_col": "customer_id", "to_table": "customers", "to_col": "id", "inclusion": 1.0, "cardinality": "many-to-one"}, {"from_table": "orders", "from_col": "product_id", "to_table": "products", "to_col": "id", "inclusion": 0.98, "cardinality": "many-to-one"}, ] g = build_join_graph(fks) # g["hubs"] == ["orders"]; orders -> role "fact", customers/products -> "dimension" print(g["mermaid"]) tested: true tests: - "test_star_schema_roles_and_hub" - "test_two_edges_built" - "test_mermaid_contains_tables_and_arrows" - "test_bridge_role" - "test_standalone_node_from_tables_list" - "test_empty_list_does_not_crash" - "test_none_input_does_not_crash" - "test_malformed_entries_skipped" - "test_does_not_mutate_input" test_file_path: "python/functions/datascience/build_join_graph_test.py" file_path: "python/functions/datascience/build_join_graph.py" params: - name: fk_candidates desc: > lista de dicts, cada uno una FK candidata con al menos las claves from_table, from_col, to_table, to_col, inclusion, cardinality. Suele ser la salida `fk_candidates` de infer_fk_containment_duckdb. Las claves se leen de forma defensiva con .get(...); entradas que no son dict o que no tienen from_table/to_table se ignoran sin fallar. None se trata como []. - name: tables desc: > lista opcional de nombres de TODAS las tablas. Sirve para incluir como nodos aislados (role "standalone") las tablas que no aparecen en ninguna FK. Si es None, los nodos se derivan solo de las aristas. output: > dict con nodes (list[dict] con table, out_degree, in_degree, role donde role es "fact"|"dimension"|"bridge"|"standalone"), edges (list[dict] con from_table, from_col, to_table, to_col, inclusion, cardinality, una por FK valida), mermaid (str con un diagrama `graph LR` pegable en un bloque ```mermaid, una arista por FK etiquetada `from_col->to_col`) y hubs (list[str] de tablas con out_degree>0 ordenadas por out_degree descendente, candidatas a tabla de hechos / star schema). --- ## Ejemplo ```python from datascience import build_join_graph # fk_candidates concreto: orders apunta a customers y a products (estrella). fks = [ {"from_table": "orders", "from_col": "customer_id", "to_table": "customers", "to_col": "id", "inclusion": 1.0, "cardinality": "many-to-one"}, {"from_table": "orders", "from_col": "product_id", "to_table": "products", "to_col": "id", "inclusion": 0.98, "cardinality": "many-to-one"}, ] g = build_join_graph(fks) g["hubs"] # ["orders"] # nodes: orders -> role "fact" (out_degree 2, in_degree 0), # customers/products -> role "dimension" (in_degree 1, out_degree 0) print(g["mermaid"]) ``` El campo `mermaid` se pega tal cual en un bloque ```mermaid: ```mermaid graph LR orders["orders"] -->|customer_id->id| customers["customers"] orders["orders"] -->|product_id->id| products["products"] ``` ## Cuando usarla Cuando hayas inferido las foreign keys de una base de datos con `infer_fk_containment_duckdb` (grupo `eda`) y necesites **visualizar el esquema relacional**: ver de un vistazo que tabla es la de hechos (hub/star schema), cuales son dimensiones y cuales quedan sueltas. Devuelve un diagrama Mermaid pegable en docs, un report o un dashboard, mas el grafo en dict para razonar sobre los grados (priorizar joins, detectar tablas puente, planear el modelo dimensional). Es la capa de grafo sobre las FK crudas: lee las candidatas, no toca la base de datos. ## Notas Funcion pura, sin I/O ni dependencias externas (solo stdlib), no muta `fk_candidates`. Tolera lista vacia o `None` (devuelve grafo vacio con un mermaid minimo `graph LR` con nota `empty`) y entradas malformadas (no-dict o sin from_table/to_table se ignoran). Heuristica de `role` por nodo, basada solo en grados: - **fact** — `out_degree > 0` y `in_degree == 0`: apunta a otras tablas y nadie le apunta. Es la candidata a tabla de hechos. - **dimension** — `in_degree > 0` y `out_degree == 0`: solo recibe referencias (tabla maestra / catalogo). - **bridge** — `out_degree > 0` e `in_degree > 0`: apunta y recibe (tabla puente o asociativa de una relacion many-to-many). - **standalone** — sin aristas (solo aparece si se paso en `tables`). `hubs` ordena por `out_degree` descendente las tablas con `out_degree > 0`. Para un star schema limpio, `hubs[0]` es la tabla de hechos. Los IDs de nodo en el Mermaid se sanean (no-alfanumerico -> `_`) pero la etiqueta visible conserva el nombre original de la tabla. ```