Módulo 7: Macros Docs And Lineage
Leyendo el grafo de linaje
Descripción
Desde el módulo 3, dbt ls --select +fact_orders fue tu herramienta para ver de qué depende un modelo — un comando que le pregunta a dbt, en el momento, cuál es el árbol de dependencias. Esta lección aprende una forma distinta de responder la misma pregunta: leer el linaje directamente desde manifest.json, el artefacto que dbt docs generate ya produjo en la lección 5. La diferencia no es cosmética. Un comando como dbt ls se ejecuta una vez y desaparece; un artefacto como manifest.json queda en disco, se puede versionar, comparar entre dos corridas, o alimentar una herramienta externa que ni siquiera tiene instalado dbt — todo lo que necesita es un archivo JSON y la estructura que esta lección te enseña a leer.
Conexión con el módulo. La lección 5 generó los artefactos; esta lección los lee con un propósito específico — reconstruir, sin ejecutar ningún comando de dbt, el mismo árbol de dependencias que ya conoces desde el módulo 3. Y cierra el círculo de la analogía del módulo: el linaje como mapa del metro, no como diagrama dibujado a mano — las líneas de colores ya están en manifest.json, solo hace falta seguirlas.
Dos direcciones del mismo grafo: depends_on y child_map
manifest.json guarda las relaciones de dependencia en dos direcciones distintas, y cada una responde una pregunta distinta:
depends_on.nodes, dentro de cada nodo, responde "¿de qué depende este modelo?" — mirando hacia atrás, hacia sus ancestros.fact_orders["depends_on"]["nodes"]ya lo viste en la lección 5:stg_orders,dim_store,dim_date.child_map, a nivel del manifiesto completo (no dentro de cada nodo), responde "¿qué depende de este modelo?" — mirando hacia adelante, hacia sus descendientes.child_map["model.kiosko_analytics.dim_store"]te dice qué modelos y tests dependen dedim_store.
Las dos direcciones del mismo grafo, guardadas por separado para que no tengas que recorrer todo el manifiesto cada vez que necesites una de las dos preguntas.
Ejemplo trabajado: el árbol completo de fact_orders
Reconstruye el árbol de dependencias de fact_orders con un script recursivo, sin ejecutar ningún dbt ls:
import json
manifest = json.load(open("target/manifest.json"))
nodes = manifest["nodes"]
sources = manifest["sources"]
def label(unique_id):
if unique_id in nodes:
return nodes[unique_id]["name"]
if unique_id in sources:
source = sources[unique_id]
return f"source:{source['source_name']}.{source['name']}"
return unique_id
def print_tree(unique_id, indent=0):
print(" " * indent + "- " + label(unique_id))
node = nodes.get(unique_id)
if not node:
return
for dependency in node.get("depends_on", {}).get("nodes", []):
print_tree(dependency, indent + 1)
print_tree("model.kiosko_analytics.fact_orders")
Qué esperar.
- fact_orders
- stg_orders
- source:kiosko_raw.orders
- dim_store
- stg_stores
- source:kiosko_raw.stores
- dim_date
Compara esto con dbt ls --select +fact_orders, tal como lo corriste en el módulo 3: el árbol de dependencias es el mismo, pero el conteo no coincide número por número, y vale la pena distinguir por qué. Este script sigue únicamente depends_on.nodes de modelos y sources —nunca de tests—, así que cuenta siete nodos en total (fact_orders, stg_orders, dim_store, stg_stores, dim_date, y los dos sources), reconstruido enteramente desde un archivo JSON, sin ningún comando adicional de dbt. dbt ls --select +fact_orders, en cambio, reportó once recursos en el módulo 3 —el mismo comando incluye, por indirect selection, los cuatro data_tests de staging que dependen de esos mismos modelos—. Los siete nodos de este script son el grafo de datos puro; los once de dbt ls son ese mismo grafo más los tests que lo validan — la sección de Errores comunes de esta lección vuelve sobre esta misma distinción con más detalle. dim_date no tiene ninguna rama debajo —no depende de ningún ref() ni source(), exactamente como ya sabes desde el módulo 3: se genera con una CTE recursiva pura, sin ninguna dependencia externa.
La pieza nueva: macros dentro del árbol
dbt ls nunca mostró de qué macros depende un modelo —solo de qué otros modelos y sources—. manifest.json sí lo guarda, en el mismo campo depends_on que ya usaste para reconstruir el árbol de arriba:
fact_orders = nodes["model.kiosko_analytics.fact_orders"]
print("Macros que invoca fact_orders:")
for macro_id in fact_orders["depends_on"]["macros"]:
print(" -", macro_id)
Qué esperar.
Macros que invoca fact_orders:
- macro.kiosko_analytics.calculate_revenue
- macro.dbt.is_incremental
Dos macros: calculate_revenue, la que escribiste en la lección 3, con el prefijo kiosko_analytics porque es propia del proyecto; e is_incremental, con el prefijo dbt porque viene incluida en dbt-core, la misma macro que el módulo 6 entero usó sin que tuvieras que escribirla. Esta es la evidencia, dentro del propio artefacto, de por qué extraer calculate_revenue en la lección 3 fue un cambio real y rastreable: antes de esa lección, este mismo script solo hubiera mostrado is_incremental en la lista.
Mirando hacia adelante: qué depende de dim_store
Invierte la pregunta con child_map, a nivel del manifiesto completo:
child_map = manifest["child_map"]
print("Qué depende de dim_store:")
for child_id in child_map["model.kiosko_analytics.dim_store"]:
print(" -", child_id)
Qué esperar (el identificador único completo de cada recurso, sin resolver a un nombre corto):
Qué depende de dim_store:
- model.kiosko_analytics.fact_orders
- test.kiosko_analytics.relationships_fact_orders_store_id__store_id__ref_dim_store_.c14f2be544
Dos descendientes: fact_orders (el modelo que ya conocías) y el test relationships del módulo 4 —el que valida que todo store_id de fact_orders exista en dim_store—. Fíjate en el prefijo de cada identificador: model.kiosko_analytics.... contra test.kiosko_analytics...., con un sufijo hexadecimal adicional en el segundo (c14f2be544 en este ejemplo — va a ser distinto en tu máquina, es un hash calculado a partir de la configuración exacta del test). Esto confirma algo que ya sabías conceptualmente pero nunca habías visto explícito en un artefacto: un data_test es, para efectos del grafo de dependencias, un nodo más, con su propio identificador único y su propia entrada en depends_on — no es una propiedad adjunta a dim_store, es un recurso independiente que depende de él. Si quisieras el nombre corto en vez del identificador completo, la misma función label() del ejemplo anterior lo resuelve: tanto para modelos como para tests, devuelve nodes[unique_id]["name"].
El linaje como diagrama, generado desde el mismo artefacto
El árbol que reconstruiste arriba se puede expresar como un diagrama, directamente a partir de la misma información:
flowchart LR
SO["source:kiosko_raw.orders"] --> STO[stg_orders]
SS["source:kiosko_raw.stores"] --> STS[stg_stores]
STS --> DS[dim_store]
STO --> FO[fact_orders]
DS --> FO
DD[dim_date] --> FO
FO -.calculate_revenue.-> FO
La flecha punteada en fact_orders -.calculate_revenue.-> fact_orders representa algo distinto de las demás: no es una dependencia de otro modelo, es una dependencia de una macro invocada dentro del propio archivo —por eso apunta al mismo nodo, y por eso dbt docs serve no la dibuja en su grafo visual (que solo muestra modelos, sources y tests, no macros). Esta es exactamente la ventaja de leer el artefacto en vez de solo mirar el diagrama: manifest.json guarda información —como las dependencias de macro— que ni siquiera el sitio de documentación navegable elige mostrar visualmente.
Por qué esto es "el linaje como artefacto", no como diagrama a mano
Vuelve, por un momento, a la analogía de la introducción del módulo: el linaje de dbt docs es el mapa del metro, no un plano dibujado por alguien que memoriza cada túnel. Cada vez que agregas un modelo nuevo con un ref() o un source() nuevo, ese cambio queda reflejado automáticamente en manifest.json la próxima vez que corras dbt docs generate — nadie tiene que actualizar ningún diagrama a mano, porque el diagrama nunca existió como un objeto separado del código: es una proyección del código, reconstruida desde cero en cada corrida. Si mañana el módulo 8 agrega dim_category, fact_sessions y los demás marts restantes, el mismo script de esta lección —sin ningún cambio— reconstruiría un árbol más grande, automáticamente, con solo apuntar a un manifest.json regenerado.
Errores comunes
Buscar dependencias de macro dentro de child_map, en vez de depends_on. Qué pasa: alguien, después de ver que child_map responde "qué depende de este nodo", intenta usarlo para encontrar "qué modelos usan la macro calculate_revenue". Por qué pasa: child_map sí incluye modelos y tests como descendientes de otros modelos, así que parece razonable esperar que también funcione para macros. Cómo detectarlo: manifest["child_map"].get("macro.kiosko_analytics.calculate_revenue") devuelve una lista vacía o None — child_map solo rastrea relaciones entre nodos ejecutables (modelos, sources, tests, snapshots), no entre una macro y quien la invoca. Cómo corregirlo: para encontrar qué modelos usan una macro específica, recorré manifest["nodes"] completo, buscando esa macro dentro del campo depends_on.macros de cada nodo — el mismo patrón que ya usaste para leer las macros de fact_orders, aplicado en sentido inverso.
Asumir que el orden de depends_on.nodes refleja el orden de ejecución del DAG. Qué pasa: alguien lee depends_on.nodes de fact_orders (stg_orders, dim_store, dim_date) y asume que ese es el orden en que dbt construye esos tres modelos. Por qué pasa: la lista parece ordenada, y es fácil confundir "orden en que aparecen en el archivo" con "orden de ejecución". Cómo detectarlo: revisa la salida real de dbt run sobre el proyecto completo, desde el módulo 3 — dim_date y las vistas de staging corren en paralelo, sin ningún orden fijo entre ellas, porque ninguna depende de la otra. Cómo corregirlo: depends_on.nodes es un conjunto de dependencias, no una secuencia — el orden real de ejecución lo decide el motor de dbt en tiempo de corrida, según cuántos threads tengas configurados (profiles.yml, módulo 1) y qué tan pronto se libere cada dependencia, nunca según el orden en que aparecen dentro de la lista del manifiesto.
Comparar el conteo de nodos de este script contra el de dbt ls --select +fact_orders, y esperar que coincidan exactamente. Qué pasa: alguien corre ambos —el script de esta lección y dbt ls --select +fact_orders— y se sorprende si los números no coinciden en algún proyecto con más tests declarados que el de esta lección. Por qué pasa: dbt ls con el operador + incluye, por comportamiento por omisión, cualquier test cuyas dependencias estén todas ya seleccionadas —una regla llamada indirect selection—, mientras que el script de esta lección solo sigue depends_on.nodes de modelos y sources, sin tests. Cómo detectarlo: si un proyecto tiene tests declarados sobre fact_orders o sobre cualquiera de sus ancestros, dbt ls --select +fact_orders puede incluirlos en su lista, mientras que este script nunca los muestra —porque un test es descendiente de un modelo, no ancestro—. Cómo corregirlo: los dos son correctos, responden preguntas distintas: dbt ls --select +fact_orders responde "qué recursos necesito construir o correr para tener fact_orders listo y probado"; el script de esta lección responde, con precisión, "de qué otros modelos y sources depende el SELECT de fact_orders" — el grafo de datos puro, sin mezclar los tests que lo validan.
Ejercicios
Ejercicio 1 — Reconstruye el árbol de dim_store. Usando la función print_tree del ejemplo trabajado, imprime el árbol de dependencias de dim_store (no de fact_orders). ¿Cuántos niveles tiene?
Ver solución
print_tree("model.kiosko_analytics.dim_store")
- dim_store
- stg_stores
- source:kiosko_raw.stores
Tres niveles: dim_store depende de stg_stores, que depende de source:kiosko_raw.stores — la cadena más corta de todo el proyecto, porque dim_store es el mart más simple: ningún JOIN, una sola dependencia directa.
Ejercicio 2 — Encuentra todos los modelos que invocan is_incremental. Recorre manifest["nodes"], y para cada nodo de tipo model, revisa si is_incremental aparece en su depends_on.macros. ¿Cuántos modelos lo usan hoy?
Ver solución
for node_id, node in nodes.items():
if node["resource_type"] != "model":
continue
macro_ids = node.get("depends_on", {}).get("macros", [])
if any("is_incremental" in m for m in macro_ids):
print(node["name"])
El resultado es un solo modelo: fact_orders — el único modelo incremental del proyecto de Kiosko desde el módulo 6. Este script, sin ningún cambio, seguiría funcionando correctamente si el módulo 8 agregara más modelos incrementales al proyecto: cada uno aparecería en la lista automáticamente, sin que tengas que actualizar nada a mano.
Ejercicio 3 — Argumenta por qué manifest.json es más útil que dbt ls para una herramienta externa. En 2-3 frases, explica por qué un sistema de gobierno de datos, o un catálogo corporativo que consolida el linaje de varios proyectos dbt a la vez, preferiría leer artefactos JSON en vez de ejecutar dbt ls repetidamente.
Ver solución
dbt ls requiere tener dbt instalado, el proyecto completo disponible, y una conexión activa al warehouse configurada correctamente — condiciones que una herramienta externa, corriendo en otro sistema, no siempre puede cumplir. manifest.json es un archivo JSON plano, portable, que se puede copiar, versionar o subir a cualquier sistema sin ninguna de esas dependencias — cualquier lenguaje que sepa leer JSON puede reconstruir el mismo linaje que este script armó en Python. Esta es, con precisión, la frontera que ya nombró el diseño de esta guía: el linaje de dbt docs es el de un proyecto, leído como artefacto local — un catálogo corporativo que combina el linaje de varios proyectos y herramientas a la vez es el trabajo de data-reliability-and-governance-guide, no de este módulo.
Resumen y siguiente paso
En esta lección leíste el linaje de fact_orders directamente desde manifest.json, sin ejecutar ningún dbt ls: reconstruiste su árbol completo de dependencias con un script recursivo sobre depends_on.nodes, encontraste sus dos dependencias de macro con depends_on.macros —incluida calculate_revenue, la evidencia dentro del propio artefacto del cambio que hiciste en la lección 3—, e invertiste la pregunta con child_map para ver qué depende de dim_store. Confirmaste que el mismo árbol que ya conocías desde el módulo 3 vive, completo, dentro de un archivo JSON versionable.
Antes de avanzar deberías poder: explicar la diferencia entre depends_on.nodes (mirar hacia atrás) y child_map (mirar hacia adelante); y nombrar por qué un data_test aparece como un nodo independiente del grafo, no como una propiedad del modelo que prueba.
La lección 7 cambia de tema por última vez en este módulo: vas a nombrar, con evidencia ya vivida en los módulos 6 y 7, el momento exacto en que correr todo esto a mano —dbt build, dbt docs generate, recordar --vars cada vez— deja de ser suficiente, y hacia dónde apunta esta guía cuando eso pasa.
Recursos
- dbt Developer Hub — "Manifest", el esquema completo de
manifest.json, incluidosdepends_on,parent_mapychild_map. docs.getdbt.com/reference/artifacts/manifest-json. En inglés. - dbt Developer Hub — "Node selection syntax", sección sobre indirect selection, la regla que explica por qué
dbt ls --select +fact_orderspuede incluir tests que el script de esta lección no muestra. docs.getdbt.com/reference/node-selection/syntax. En inglés. data-reliability-and-governance-guide, la guía hermana del ecosistema que extiende el linaje de un solo proyecto a un catálogo de gobierno entre varios sistemas y herramientas.