4. El historial de Git y las ejecuciones de CI como grafo
Secciones
Ajustes de lectura
Voz natural grabada para este curso, la misma en todos los navegadores.
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Un segundo grafo, del mismo repositorio: su historial de Git hasta el commit cbcbb42, y las ejecuciones de CI de la instantánea del curso de DuckDB. Todas las consultas están en cypher/04-git-history.cypher.
El modelo
Sección titulada «El modelo»| Tabla | Tipo | Origen |
|---|---|---|
Commit(sha, committed_at, subject) |
nodo | commits.csv |
File(path) |
nodo | los archivos distintos de changes.csv |
PARENT |
relación, de Commit a Commit |
parents.csv: cada commit apunta a su padre |
CHANGED |
relación, de Commit a File |
changes.csv |
Run(databaseId, workflowName, conclusion, headBranch) |
nodo | code/duckdb/data/runs.json |
RAN_ON |
relación, de Run a Commit |
runs.json: el commit del que cada ejecución hizo checkout |
En SQL, PARENT y CHANGED serían tablas de unión, y RAN_ON una columna de clave foránea en la tabla de ejecuciones. En el grafo, las tres son relaciones, y una consulta puede seguirlas en un solo patrón.
Las primeras líneas del script crean y cargan la parte de Git (líneas 3-10):
CREATE NODE TABLE Commit(sha STRING PRIMARY KEY, committed_at TIMESTAMP, subject STRING);CREATE NODE TABLE File(path STRING PRIMARY KEY);CREATE REL TABLE PARENT(FROM Commit TO Commit);CREATE REL TABLE CHANGED(FROM Commit TO File);COPY Commit FROM 'data/commits.csv' (HEADER = true, QUOTE = '"');COPY File FROM (LOAD FROM 'data/changes.csv' (HEADER = true) RETURN DISTINCT file);COPY PARENT FROM 'data/parents.csv' (HEADER = true);COPY CHANGED FROM 'data/changes.csv' (HEADER = true);74 commits, 710 archivos, 73 relaciones PARENT y 1.019 relaciones CHANGED. El archivo tiene 74 líneas, así que el sniffer de CSV de la lección 2 las lee todas y encuentra por sí solo los asuntos entre comillas; QUOTE = '"' solo lo hace explícito, para un historial más largo.
Marcas de tiempo
Sección titulada «Marcas de tiempo»MATCH (c:Commit) RETURN count(*) AS commits, min(c.committed_at) AS first_commit, max(c.committed_at) AS last_commit;┌─────────┬─────────────────────┬─────────────────────┐│ commits │ first_commit │ last_commit ││ INT64 │ TIMESTAMP │ TIMESTAMP │├─────────┼─────────────────────┼─────────────────────┤│ 74 │ 2026-09-13 15:52:51 │ 2026-09-14 16:03:28 │└─────────┴─────────────────────┴─────────────────────┘El archivo tiene 2026-09-13T11:52:51-04:00, la hora local del commit con su desfase. TIMESTAMP no tiene zona horaria: LadybugDB convirtió el valor a UTC y descartó el desfase. Todo el historial del sitio, hasta este commit, cabe en poco más de un día.
Un camino largo que no devuelve nada
Sección titulada «Un camino largo que no devuelve nada»Del último commit al primero: un camino de relaciones PARENT hasta un commit sin padre (líneas 16-22):
MATCH (last:Commit)-[p:PARENT*1..30]->(first:Commit)WHERE last.sha STARTS WITH 'cbcbb42' AND NOT EXISTS { MATCH (first)-[:PARENT]->(:Commit) }RETURN first.subject, length(p) AS commits_between;CALL var_length_extend_max_depth = 100;MATCH (last:Commit)-[p:PARENT*1..100]->(first:Commit)WHERE last.sha STARTS WITH 'cbcbb42' AND NOT EXISTS { MATCH (first)-[:PARENT]->(:Commit) }RETURN first.subject, length(p) AS commits_between;┌───────────────┬─────────────────┐│ first.subject │ commits_between ││ STRING │ INT64 │├───────────────┼─────────────────┤└───────────────┴─────────────────┘┌───────────────────────────────────────────────────────────────────────┬─────────────────┐│ first.subject │ commits_between ││ STRING │ INT64 │├───────────────────────────────────────────────────────────────────────┼─────────────────┤│ Initial learn site: Starlight, bilingual en/fr, WSL containers course │ 73 │└───────────────────────────────────────────────────────────────────────┴─────────────────┘La primera consulta no devuelve ninguna fila, ni ningún error. El primer commit está a 73 relaciones, más allá de las 30 de *1..30: ningún camino encaja en el patrón. Con el límite subido a 100 (lección 3), el mismo patrón lo encuentra. Cuando un patrón de longitud variable no encuentra nada, comprueba su cota superior antes que los datos. Este historial no tiene ramas, así que hay un solo camino: ningún riesgo de la explosión de recorridos de la lección 3.
STARTS WITH 'cbcbb42' coincide con el hash abreviado, como git show cbcbb42.
Padres por commit, y un bug en las subconsultas COUNT
Sección titulada «Padres por commit, y un bug en las subconsultas COUNT»Un commit de fusión tiene dos padres. ¿Hay alguno? Líneas 26-27:
MATCH (c:Commit) RETURN COUNT { MATCH (c)-[:PARENT]->(:Commit) } AS parents, count(*) AS commits ORDER BY parents;MATCH (c:Commit) OPTIONAL MATCH (c)-[:PARENT]->(p:Commit) WITH c, count(p) AS parents RETURN parents, count(*) AS commits ORDER BY parents;┌─────────┬─────────┐│ parents │ commits ││ INT64 │ INT64 │├─────────┼─────────┤│ 1 │ 73 │└─────────┴─────────┘┌─────────┬─────────┐│ parents │ commits ││ INT64 │ INT64 │├─────────┼─────────┤│ 0 │ 1 ││ 1 │ 73 │└─────────┴─────────┘Ningún commit de fusión: el historial es lineal. Pero la primera consulta cuenta 73 commits de 74. El commit raíz, cuyo COUNT es 0, falta en el resultado agrupado, aunque la misma subconsulta devuelve 0 para ese commit cuando la consulta no agrupa por ella. La segunda consulta cuenta lo mismo con OPTIONAL MATCH y count(p), y obtiene los dos grupos. Otro bug de la 0.20.4, silencioso como los de las lecciones 2 y 3: cuando una consulta agrupa por una subconsulta COUNT, comprueba que la suma de los grupos dé el número de nodos.
Archivos modificados juntos
Sección titulada «Archivos modificados juntos»Los archivos que cambian en los mismos commits que astro.config.mjs, donde se registran los cursos (líneas 30-33):
MATCH (a:File {path: 'astro.config.mjs'})<-[:CHANGED]-(c:Commit)-[:CHANGED]->(b:File)RETURN b.path, count(*) AS commitsORDER BY commits DESC, b.pathLIMIT 5;┌───────────────────────────────┬─────────┐│ b.path │ commits ││ STRING │ INT64 │├───────────────────────────────┼─────────┤│ astro.config.mjs │ 11 ││ src/content/docs/fr/index.mdx │ 9 ││ src/content/docs/index.mdx │ 9 ││ AGENTS.md │ 7 ││ README.md │ 7 │└───────────────────────────────┴─────────┘La primera fila es el propio archivo. El patrón tiene dos relaciones CHANGED, y nada impide que sean la misma: con la semántica de recorrido de la lección 3, b puede ser a. WHERE b <> a elimina esa fila; las siguientes son las páginas de inicio, que también enumeran los cursos, y AGENTS.md, donde viven las convenciones de los cursos. La versión SQL, una autocombinación de la tabla de cambios sobre el commit, tiene la misma trampa y la misma corrección (b.file <> a.file).
Los archivos modificados más a menudo
Sección titulada «Los archivos modificados más a menudo»MATCH (c:Commit)-[:CHANGED]->(f:File)RETURN f.path, count(*) AS commitsORDER BY commits DESC, f.pathLIMIT 5;┌───────────────────────────────────────────────┬─────────┐│ f.path │ commits ││ STRING │ INT64 │├───────────────────────────────────────────────┼─────────┤│ src/content/docs/fr/wsl-containers/journal.md │ 16 ││ src/content/docs/wsl-containers/journal.md │ 15 ││ README.md │ 13 ││ astro.config.mjs │ 11 ││ src/content/docs/fr/index.mdx │ 9 │└───────────────────────────────────────────────┴─────────┘El diario del curso WSL containers va primero: 16 commits modificaron el francés, 15 el inglés.
Ejecuciones de CI: una relación hacia un commit ausente
Sección titulada «Ejecuciones de CI: una relación hacia un commit ausente»Las ejecuciones vienen de JSON, así que primero la extensión json (líneas 42-45):
LOAD json;CREATE NODE TABLE Run(databaseId INT64 PRIMARY KEY, workflowName STRING, conclusion STRING, headBranch STRING);CREATE REL TABLE RAN_ON(FROM Run TO Commit);COPY Run FROM (LOAD FROM '../duckdb/data/runs.json' RETURN databaseId, workflowName, conclusion, headBranch);125 ejecuciones. Luego las relaciones, del id de la ejecución al commit sobre el que se ejecutó (líneas 48-52):
COPY RAN_ON FROM (LOAD FROM '../duckdb/data/runs.json' RETURN databaseId, headSha);LOAD FROM '../duckdb/data/runs.json'WITH headBranch, headShaWHERE NOT EXISTS { MATCH (c:Commit) WHERE c.sha = headSha }RETURN headBranch, count(*) AS runs;Error: Copy exception: Unable to find primary key value bd17932fe35899909b8d983ab289e1ea4ff344a8.┌────────────────┬───────┐│ headBranch │ runs ││ STRING │ INT64 │├────────────────┼───────┤│ gha-06-invalid │ 2 │└────────────────┴───────┘Dos ejecuciones se ejecutaron sobre un commit de la rama gha-06-invalid, una rama temporal del curso de GitHub Actions, usada para probar un workflow no válido sin ponerlo en main: sus commits no están en este historial. La respuesta de la lección 2 sería IGNORE_ERRORS, pero no está soportado en un COPY desde una subconsulta: el intento falla con bad variant access. La alternativa obvia es quedarse solo con las ejecuciones cuyo commit existe.
Un bug de la 0.20.4: un MATCH en una subconsulta de COPY
Sección titulada «Un bug de la 0.20.4: un MATCH en una subconsulta de COPY»Esa alternativa, con un MATCH en la subconsulta (líneas 55-61):
COPY RAN_ON FROM ( LOAD FROM '../duckdb/data/runs.json' WITH databaseId, headSha MATCH (c:Commit) WHERE c.sha = headSha RETURN databaseId, headSha);MATCH (r:Run)-[:RAN_ON]->(c:Commit) RETURN count(*) AS links, count(DISTINCT r) AS runs, count(DISTINCT c) AS commits;┌──────────────────────────────────────────────────┐│ result ││ STRING │├──────────────────────────────────────────────────┤│ 123 tuples have been copied to the RAN_ON table. │└──────────────────────────────────────────────────┘┌───────┬───────┬─────────┐│ links │ runs │ commits ││ INT64 │ INT64 │ INT64 │├───────┼───────┼─────────┤│ 123 │ 1 │ 64 │└───────┴───────┴─────────┘123 relaciones, el número correcto, hacia los 64 commits correctos, pero todas desde la misma ejecución. La subconsulta sola, ejecutada como consulta, devuelve 123 ids de ejecución distintos; dentro de COPY, la columna FROM de cada relación recibe uno de ellos. El mismo COPY funciona con un archivo CSV en lugar de JSON, y con JSON cuando el filtro no usa MATCH. La solución alternativa mantiene JSON y filtra por una columna: los dos commits que faltan son los de la otra rama (líneas 64-71):
MATCH ()-[x:RAN_ON]->() DELETE x;COPY RAN_ON FROM ( LOAD FROM '../duckdb/data/runs.json' WITH databaseId, headSha, headBranch WHERE headBranch = 'main' RETURN databaseId, headSha);MATCH (r:Run)-[:RAN_ON]->(c:Commit) RETURN count(*) AS links, count(DISTINCT r) AS runs, count(DISTINCT c) AS commits;┌──────────────────────────────────────────────────┐│ result ││ STRING │├──────────────────────────────────────────────────┤│ 123 tuples have been copied to the RAN_ON table. │└──────────────────────────────────────────────────┘┌───────┬───────┬─────────┐│ links │ runs │ commits ││ INT64 │ INT64 │ INT64 │├───────┼───────┼─────────┤│ 123 │ 123 │ 64 │└───────┴───────┴─────────┘123 enlaces desde 123 ejecuciones. MATCH ()-[x:RAN_ON]->() DELETE x borró primero las relaciones erróneas: las relaciones se pueden borrar sin DETACH. La consulta de comprobación es lo importante de esta sección: count(*) solo era correcto en ambos casos; count(DISTINCT r) mostró el bug.
Los fallos y los archivos que tocaron
Sección titulada «Los fallos y los archivos que tocaron»Todo el grafo en un solo patrón: ejecuciones, commits, archivos (líneas 74-78):
MATCH (r:Run)-[:RAN_ON]->(c:Commit)-[:CHANGED]->(f:File)WHERE r.conclusion = 'failure'RETURN f.path, count(DISTINCT c) AS failed_commitsORDER BY failed_commits DESC, f.pathLIMIT 5;┌────────────────────────────────────────┬────────────────┐│ f.path │ failed_commits ││ STRING │ INT64 │├────────────────────────────────────────┼────────────────┤│ .github/workflows/gha-03-triggers.yml │ 2 ││ .github/workflows/gha-05-exercises.yml │ 2 ││ astro.config.mjs │ 2 ││ code/github-actions/.gitignore │ 2 ││ src/content/docs/fr/index.mdx │ 2 │└────────────────────────────────────────┴────────────────┘count(DISTINCT c), no count(*): un commit con tres ejecuciones fallidas cuenta una vez. Los archivos de workflow de las lecciones de GitHub Actions van primero; los archivos del sitio que los acompañan se modificaron en los mismos commits. Un grafo de cambios muestra qué falló junto, no qué causó el fallo: para eso, los logs de las ejecuciones.
Ejecuciones por workflow en los commits que modificaron el código del curso
Sección titulada «Ejecuciones por workflow en los commits que modificaron el código del curso»MATCH (r:Run)-[:RAN_ON]->(c:Commit)-[:CHANGED]->(f:File)WHERE f.path STARTS WITH 'code/github-actions/'RETURN r.workflowName, count(DISTINCT r) AS runsORDER BY runs DESC, r.workflowName;┌─────────────────────────────────────┬───────┐│ r.workflowName │ runs ││ STRING │ INT64 │├─────────────────────────────────────┼───────┤│ Deploy to GitHub Pages │ 2 ││ GHA 02: build and test │ 2 ││ Rust course examples │ 2 ││ GHA 01: hello │ 1 ││ GHA 03: triggers │ 1 ││ GHA 04: data between steps and jobs │ 1 ││ GHA 05: caches and artifacts │ 1 ││ GHA 05: exercise checks │ 1 │└─────────────────────────────────────┴───────┘Qué workflows desencadenó un cambio en code/github-actions/: los workflows de GitHub Actions, pero también el despliegue y los ejemplos del curso de Rust, que se ejecutaron sobre los mismos commits porque otros archivos cambiaron con ellos. count(DISTINCT r) otra vez: a una ejecución se llega una vez por cada archivo de su commit.
Puntos clave
Sección titulada «Puntos clave»- Un historial de Git es un grafo: los commits, los archivos y las ejecuciones son nodos; los padres, los cambios y «se ejecutó sobre» son relaciones.
- Un patrón de longitud variable cuya cota superior es demasiado pequeña no devuelve nada, sin error.
TIMESTAMPalmacena UTC: un desfase en el archivo se aplica y luego se descarta.- En un patrón con dos relaciones de la misma tabla, ambas pueden ser la misma relación: exclúyela con
WHERE b <> a. - Dos bugs silenciosos más de la 0.20.4: agrupar por una subconsulta
COUNTpierde el grupo cero, y unMATCHdentro de una subconsulta deCOPYdesde JSON asigna todas las relaciones a un solo nodo.IGNORE_ERRORStampoco está disponible con subconsultas.count(DISTINCT …)en cada extremo es una comprobación barata después de una carga.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en cypher/04-exercises.cypher, comprobadas por la CI. Cargan el mismo grafo, con la solución alternativa de main para RAN_ON.
- ¿Qué tres commits modificaron más archivos?
Solución
De 04-exercises.cypher, líneas 23-26:
MATCH (c:Commit)-[:CHANGED]->(f:File)RETURN substring(c.sha, 1, 7) AS sha, c.subject, count(*) AS filesORDER BY files DESC, shaLIMIT 3;┌─────────┬──────────────────────────────────────────────────────────────┬───────┐│ sha │ c.subject │ files ││ STRING │ STRING │ INT64 │├─────────┼──────────────────────────────────────────────────────────────┼───────┤│ d32b186 │ Java lessons 5-8 and a Spanish locale for the whole site │ 160 ││ f43ba7e │ Import Streeling University modules from Demerzel │ 103 ││ 529c946 │ Java for C# developers: lessons 1-4, tested code and journal │ 80 │└─────────┴──────────────────────────────────────────────────────────────┴───────┘El commit que añadió la configuración regional española añadió 75 páginas en español de una vez. substring(c.sha, 1, 7) es el hash abreviado.
- ¿Cuántos commits antes de
cbcbb42se modificóAGENTS.mdpor última vez, y en qué commit?
Solución
De 04-exercises.cypher, líneas 29-34:
CALL var_length_extend_max_depth = 100;MATCH (last:Commit)-[p:PARENT* SHORTEST 1..100]->(c:Commit)-[:CHANGED]->(:File {path: 'AGENTS.md'})WHERE last.sha STARTS WITH 'cbcbb42'RETURN length(p) AS commits_before, substring(c.sha, 1, 7) AS sha, c.subjectORDER BY commits_beforeLIMIT 1;┌────────────────┬─────────┬──────────────────────────────────────────────────────────┐│ commits_before │ sha │ c.subject ││ INT64 │ STRING │ STRING │├────────────────┼─────────┼──────────────────────────────────────────────────────────┤│ 42 │ d32b186 │ Java lessons 5-8 and a Spanish locale for the whole site │└────────────────┴─────────┴──────────────────────────────────────────────────────────┘42 commits: git log --oneline cbcbb42 muestra d32b186 en la posición 43. SHORTEST encuentra un camino por cada commit que modificó AGENTS.md, y ORDER BY … LIMIT 1 se queda con el más cercano. El límite de 100 vuelve a ser necesario: el historial es más largo que 30. La cota inferior 1 excluye al propio cbcbb42, que de todos modos no modificó el archivo.
- ¿Qué commits no tienen ninguna ejecución de CI? Para cada uno, ¿cuánto tiempo pasó hasta el commit siguiente, y tuvo este ejecuciones?
Solución
De 04-exercises.cypher, líneas 37-45:
MATCH (c:Commit)WHERE NOT EXISTS { MATCH (:Run)-[:RAN_ON]->(c) }RETURN c.committed_at, substring(c.sha, 1, 7) AS sha, c.subjectORDER BY c.committed_at;MATCH (c:Commit)-[:PARENT]->(p:Commit)WHERE NOT EXISTS { MATCH (:Run)-[:RAN_ON]->(p) }RETURN substring(p.sha, 1, 7) AS without_runs, substring(c.sha, 1, 7) AS next_commit, c.committed_at - p.committed_at AS gap, COUNT { MATCH (:Run)-[:RAN_ON]->(c) } AS next_commit_runsORDER BY without_runs;┌─────────────────────┬─────────┬──────────────────────────────────────────────────────────────────────────────────────┐│ c.committed_at │ sha │ c.subject ││ TIMESTAMP │ STRING │ STRING │├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────┤│ 2026-09-13 17:13:23 │ 00f2ca4 │ Rust course: lessons 9-12 (lifetimes, modules, smart pointers, threads) ││ 2026-09-14 14:08:24 │ 3f3b219 │ DuckDB course: data snapshot of this repository's runs, lesson 1 SQL, CI on 3 OSes ││ 2026-09-14 14:24:14 │ 742c967 │ DuckDB course: SQL and expected output for lessons 1-4 and their exercises ││ 2026-09-14 14:25:34 │ 36207da │ DuckDB course: don't compare Parquet compressed sizes, they differ on macOS arm64 ││ 2026-09-14 14:43:42 │ 3e9530f │ DuckDB course: mission, lessons 1-4 and journal (en/fr/es) ││ 2026-09-14 15:13:02 │ a96fd27 │ DuckDB course: lesson 5 C# program (DuckDB.NET, Dapper) and CI on 3 OSes ││ 2026-09-14 15:16:11 │ 5a26d1a │ DuckDB course: lesson 6 Java program (JDBC) and CI on 3 OSes ││ 2026-09-14 15:33:19 │ bdf2692 │ DuckDB course: lesson 7 scripts (plans, pushdown, row groups) and CI timings ││ 2026-09-14 15:48:40 │ 86e34d9 │ DuckDB course: lesson 7 exercises, lesson 8 persistence script and Java transactions ││ 2026-09-14 16:03:28 │ cbcbb42 │ DuckDB course: lessons 5-8 (C#, Java, performance, persistence) in en/fr/es │└─────────────────────┴─────────┴──────────────────────────────────────────────────────────────────────────────────────┘┌──────────────┬─────────────┬──────────┬──────────────────┐│ without_runs │ next_commit │ gap │ next_commit_runs ││ STRING │ STRING │ INTERVAL │ INT64 │├──────────────┼─────────────┼──────────┼──────────────────┤│ 00f2ca4 │ 95a42da │ 00:00:00 │ 2 ││ 36207da │ 3e9530f │ 00:18:08 │ 0 ││ 3e9530f │ a96fd27 │ 00:29:20 │ 0 ││ 3f3b219 │ 742c967 │ 00:15:50 │ 0 ││ 5a26d1a │ bdf2692 │ 00:17:08 │ 0 ││ 742c967 │ 36207da │ 00:01:20 │ 0 ││ 86e34d9 │ cbcbb42 │ 00:14:48 │ 0 ││ a96fd27 │ 5a26d1a │ 00:03:09 │ 0 ││ bdf2692 │ 86e34d9 │ 00:15:21 │ 0 │└──────────────┴─────────────┴──────────┴──────────────────┘Dos razones. Nueve commits son posteriores a la instantánea de las ejecuciones, cuya última ejecución se creó a las 14:01 UTC: tuvieron ejecuciones, después de tomar la instantánea. 00f2ca4 es el otro caso: 95a42da lo siguió en el mismo segundo, se subieron juntos con un push, y GitHub Actions ejecuta los workflows de un push solo sobre su último commit. Restar dos valores TIMESTAMP da un INTERVAL. cbcbb42 no tiene hijo en este historial, así que no está en el segundo resultado.
Fuentes
Sección titulada «Fuentes»- Tipos de datos:
TIMESTAMP,INTERVAL MATCH: relaciones de longitud variable y caminos más cortosCOPY FROMdesde una subconsulta y extensión JSON- Subconsultas y
OPTIONAL MATCH - Diferencias entre LadybugDB y Neo4j
- Eventos que desencadenan workflows:
push