Cómo Creé un Lenguaje Similar a SQL para Ejecutar Consultas en Repositorios Git Locales

Cómo Creé un Lenguaje Inspirado en SQL para Ejecutar Consultas en Repositorios Locales de Git

¡Hola a todos! Soy un ingeniero de software interesado en la programación de bajo nivel, compiladores y desarrollo de herramientas.

Hace tres meses decidí aprender el lenguaje de programación Rust y construir un cliente de Git que se enfoque en la simplicidad y la productividad. Empecé a pensar en cómo podría construir el cliente de Git para proporcionar algunas características únicas y útiles.

Por ejemplo, me gusta la página de análisis en GitHub que muestra cuántos commits ha realizado cada desarrollador y cuántas líneas han insertado o eliminado. Pero ¿qué pasa si quiero obtener este análisis para algún período de tiempo, o ordenarlo todo por líneas insertadas y no por número de commits? ¿O ordenarlos por cuántos commits se hicieron por semana o mes?

¿Se puede agregar una opción de ordenamiento personalizada para el cliente, verdad? Pero empecé a pensar en cómo podría hacerlo más dinámico. Esto me motivó a preguntarme si podría ejecutar consultas similares a SQL en los archivos .git locales para poder consultar cualquier información que quisiera.

Así que imagina si pudieras ejecutar una consulta como esta en tus repositorios de git locales:

SELECT nombre, COUNT(nombre) AS num_commits FROM commits GROUP BY nombre ORDER BY num_commits DESC LIMIT 10

He implementado esta idea en un proyecto que llamé GQL (Git Query Language). Y en este artículo, te voy a mostrar cómo diseñé e implementé esta funcionalidad.

¿Cómo puedes tomar una consulta similar a SQL y ejecutarla en archivos .git?

La primera idea que se me ocurrió fue usar SQLite. Pero había algunos problemas que no pude resolver.

Por ejemplo, no pude personalizar la sintaxis y no quería leer los archivos .git y almacenarlos en una base de datos SQLite para luego realizar la consulta. Quería que todo se ejecutara sobre la marcha.

También quería poder usar no solo los comandos SELECT, DELETE y UPDATE, sino también proporcionar comandos relacionados con Git como push, pull, etc.

Ya he creado diferentes herramientas como compiladores antes, así que ¿por qué no crear un lenguaje similar a SQL desde cero y hacer que realice consultas sobre la marcha para ver si funciona?

Cómo diseñé e implementé un lenguaje de consulta desde cero

Quería empezar poco a poco, solo admitiendo el comando SELECT sin funciones avanzadas como agregaciones, agrupaciones, uniones, etc.

Así que planeé analizar la consulta en una estructura de datos que facilitara la validación y la evaluación (como la verificación de tipos y mostrar mensajes de error útiles si algo salía mal). Después de eso, pasaría esta estructura de datos al evaluador que aplicaría la consulta en mis archivos .git.

Elegir una estructura de datos para usar

La mejor estructura de datos para este caso es representar la consulta utilizando un árbol de sintaxis abstracta (AST). Esta es una estructura de datos muy común utilizada en compiladores porque es flexible y facilita el recorrido y la composición de nodos dentro de otros.

También en este caso, no necesitaba mantener toda la información sobre la consulta, solo la información necesaria para los siguientes pasos (de ahí el nombre de “Abstracta”).

Decidir qué validación realizar

La validación más importante en este caso sería la verificación de tipos para asegurarse de que cada valor sea válido y se utilice en el lugar correcto.

Por ejemplo, ¿qué pasa si la consulta quiere multiplicar texto por otro texto? ¿Sería esto válido?

SELECT "UNO" * "DOS"

El operador de multiplicación espera que ambos lados sean números. Entonces, en este caso, quería informar al usuario que su consulta es inválida e intentar ayudarlo a comprender el problema lo mejor posible.

¿Cómo funcionaría eso? Cuando veo un operador como *, debes verificar ambos lados para ver si los valores son tipos válidos para este operador o no. Si no lo son, informa un mensaje como este:

SELECT "UNO" * "DOS"-------------^ERROR: El operador `*` espera que ambos lados sean de tipo Número pero se recibió Texto.

Además de los operadores, sabía que debía verificar si cada identificador era una tabla, campo, alias de un nombre de función o si debería estar sin definir. También necesitaba informar un error si, por ejemplo, una tabla de branches solo contenía 2 campos como el siguiente ejemplo:

Branches {   Text nombre,   Número cantidad_commits,}

Así que creé una tabla que contenía representaciones de todas las tablas y campos para poder realizar fácilmente la comprobación de tipos. Si el usuario intentaba seleccionar un campo que no estaba definido en este esquema, se informaba un error:

SELECT invalid_field_name FROM branches-------------^Error: El campo `invalid_field_name` no está definido en la tabla branches.

Tuve que asegurarme de que se realizaran las mismas comprobaciones en las condiciones, los nombres de las funciones y los argumentos. Luego, si todo estaba correctamente definido y tenía los tipos correctos, el AST sería válido y podríamos pasar al siguiente paso.

¿Qué sucede después de validar el Árbol de Sintaxis Abstracto?

Después de asegurarme de que todo fuera válido, era hora de evaluar la consulta y cómo obtenía el resultado.

Para hacer eso, simplemente recorrí el árbol de sintaxis y evalué cada nodo. Después de terminar, debería tener el resultado correcto en una lista.

Vamos a través de ese proceso paso a paso para ver cómo funciona.

Por ejemplo, en una consulta como esta:

SELECT * FROM branches WHEER name LIKE "%/main" ORDER BY commit_count LIMIE BY 5

La representación del AST se verá así:

AbstractSyntaxTree {  Select(*, "branches")   Where(Like(name, "%/main"))  OrderBy(commit_count)  Limit(5) }

Ahora necesitamos recorrer y evaluar cada nodo, pero en un orden específico. No vamos simplemente de principio a fin o de fin a principio porque necesitamos hacer esto en el mismo orden en que SQL lo haría para obtener el mismo resultado.

Por ejemplo, en SQL, la declaración WHERE debe ejecutarse antes de GROUP BY, y HAVING debe ejecutarse después.

En el ejemplo anterior, todo está en el orden correcto para ejecutarse, así que veamos qué hará cada declaración.

  • Select(*, "branches")

Esto seleccionará todos los campos de la tabla con el nombre branches y los agregará a una lista, llamémosla objects. Pero, ¿cómo puedo seleccionarlos desde el repositorio local?

Toda la información sobre confirmaciones, ramas, etiquetas, etc., se almacena por Git en archivos dentro de una carpeta llamada .git en cada repositorio. Una opción es escribir un analizador completo desde cero para extraer la información necesaria. Pero usar una biblioteca para hacer esto en su lugar funcionó para mí.

Decidí utilizar la biblioteca libgit2 para realizar esta tarea. Es una implementación pura en C de los métodos principales de Git, por lo que puedes leer toda la información que necesites y usarla desde Rust. Existe una caja (biblioteca de Rust) creada por el equipo oficial de Rust llamada git2, por lo que puedes obtener fácilmente la información de la rama de esta manera:

let local_branches = repo.branches(Some(BranchType::Local));let remote_branches = repo.branches(Some(BranchType::Remote));let local_and_remote_branches = repository.branches(None);

y luego iterar sobre cada rama para obtener su información y almacenarla de esta manera:

for branch in local_and_remote_branches {   // Extraer información de la rama y almacenarla}

Ahora nos quedamos con una lista de todas las ramas que usaremos en los siguientes pasos.

  • Where(Like(name, "%/main"))

Esto filtrará la lista de objetos y eliminará todos los elementos que no cumplan las condiciones, en nuestro caso, aquellos que terminen con “/main”.

  • OrderBy(commit_count)

Esto ordena la lista de objetos por el valor del campo commit_count.

  • Limit(5)

Esto toma solo los primeros cinco elementos y elimina el resto de la lista de objetos.

¡Eso es todo! Y ahora obtenemos un resultado válido, que puedes ver a continuación:

gql_demo

Los ejemplos a continuación son válidos y se ejecutan correctamente:

SELECT 1SELECT 1 + 2SELECT LEN("Git Query Language")SELECT "One" IN ("One", "Two", "Three")SELECT "Git Query Language" LIKE "%Query%"SELECT commit_count FROM branches WHERE commit_count BETWEEN 0 .. 10SELECT * FROM refs WHERE type = "branch"SELECT * FROM refs ORDER BY typeSELECT * FROM commitsSELECT name, email FROM commitsSELECT name, email FROM commits ORDER BY name DESCSELECT name, email FROM commits WHERE name LIKE "%gmail%" ORDER BY nameSELECT * FROM commits WHERE LOWER(name) = "amrdeveloper"SELECT name FROM commits GROUP By nameSELECT name FROM commits GROUP By name having name = "AmrDeveloper"SELECT * FROM branchesSELECT * FROM branches WHERE is_head = trueSELECT name, LEN(name) FROM branchesSELECT * FROM tagsSELECT * FROM tags OFFSET 1 LIMIT 1

Cómo soportar la ejecución en múltiples repositorios al mismo tiempo

Después de publicar GQL, recibí comentarios increíbles de la gente. También recibí algunas solicitudes de funciones, como querer soporte para múltiples repositorios y filtrar por ruta de repositorio.

Pensé que esta era una gran idea, porque podría obtener análisis para múltiples proyectos y también porque podría hacerlo en múltiples hilos. Además, no parecía ser muy difícil de implementar.

Así que después de completar el paso de validación para el AST, es hora del paso de evaluación, pero en lugar de evaluarlo una vez, se evaluará una vez por cada repositorio y luego se fusionarán todos los resultados en una lista.

Pero, ¿qué hay de soportar la capacidad de filtrar por ruta de repositorio?

Eso fue bastante fácil. ¿Recuerdas el esquema de tabla de branches? Lo único que necesitaba hacer era introducir un nuevo campo llamado repository_path para representar la ruta local del repositorio para esta branch y también introducirlo en otras tablas.

Entonces, el esquema final se verá así:

Branches { Text name, Number commit_count, Text repository_path }

Ahora podemos ejecutar una consulta que utiliza este campo:

SELECT * FROM branches WHERE repository_path LIKE "%GQL"

¡Y eso es todo! 😉

¡Gracias por leer!

Si te gustó el proyecto, puedes darle una estrella ⭐ en github.com/AmrDeveloper/GQL.

Puedes visitar el sitio web github.io/GQL para ver cómo descargar y usar el proyecto en diferentes sistemas operativos.

El proyecto aún no ha terminado, esto es solo el inicio. Todos son bienvenidos a unirse y contribuir al proyecto, y sugerir ideas o reportar errores.


Leave a Reply

Your email address will not be published. Required fields are marked *