Analizando código PL/SQL con antlr4 en Python

Supongamos que queremos listar las llamadas a función que se realizan en un paquete PL/SQL, o realizar algún tipo de validación sobre el mismo antes de ejecutarlo en BBDD. Necesitamos construir un analizador de código o parser que nos permita reconocer las construcciones del lenguaje y actuar en consecuencia.

 

Un parser trabaja habitualmente con un lexer. Mientras el lexer segmenta el texto entrante en los posibles lexemas o tokens, el parser los representa en una estructura de datos en forma de árbol según las construcciones sintácticas permitidas por la gramática:

Ilustración 1: Reconocimiento del lenguaje

 

 

Para lenguajes como HTML, Python 3 tiene html.parse integrado, que nos da el trabajo hecho. También existen módulos como Lark que nos pueden ayudar a generar parsers eficientes a partir de gramáticas EBNF.

 

En este caso utilizaremos antlr4, una herramienta java que permite generar parsers en múltiples lenguajes a partir de una notación cercana a EBNF, debido al vasto repositorio de gramáticas que mantiene su comunidad.

 

Ilustración 2: Diagrama de ferrocarril para la cláusula “insert into”

 

Una vez instalados Java y Python 3, añadiremos los módulos necesarios con:

 

pip install antlr4-python3-runtime

 

Clonamos el siguiente repositorio, que utilizaremos como base:

 

Git clone https://github.com/basicarrero/pyplsqlparser

 

Entramos al directorio y generamos el parser con la herramienta:

 

java -jar antlr-4.9.2-complete.jar -Dlanguage=Python3 -visitor pyPlSqlLexer.g4

java -jar antlr-4.9.2-complete.jar -Dlanguage=Python3 -visitor pyPlSqlParser.g4

 

 

Definimos un listener que sobrescriba la función enterFunction_call() la cual será invocada al entrar a una llamada a función en nuestro código PL/SQL:

 

import sys
from antlr4 import *
from pyPlSqlLexer import pyPlSqlLexer
from pyPlSqlParser import pyPlSqlParser
from pyPlSqlParserListener import pyPlSqlParserListener
from CaseChangingStream import CaseChangingStream

class myListener(pyPlSqlParserListener):
    def enterFunction_call(self, ctx:pyPlSqlParser.Function_specContext):
        print('Llamada a función reconocida:\n\t', ctx.getText())

def getPLSQLparser(input):
    casedInput = CaseChangingStream(input, True)
    lexer = pyPlSqlLexer(casedInput)
    stream = CommonTokenStream(lexer)
    return pyPlSqlParser(stream)

if __name__ == '__main__':
    stream = FileStream(sys.argv[1], encoding='utf-8')
    parser = getPLSQLparser(stream)
    tree = parser.sql_script()
    ParseTreeWalker().walk(myListener(), tree)

 

Finalmente ejecutaremos nuestro parser sobre el código:

 

python funCallFinder.py ./examples-sql-script/anonymous_block.sql

 

 

Como resultado obtenemos:

 

Llamada a función reconocida:
         DBMS_OUTPUT.PUT_LINE('HELLO')
Llamada a función reconocida:
         DBMS_OUTPUT.PUT_LINE('SOMETHING WENT WRONG!')

 

Reflexiones finales:

La definición de la gramática no está completa, pero aún así cubre buena parte de la especificación para 11g y proporciona un buen punto de partida. El parser generado tampoco es especialmente rápido en Python, sobre todo si lo comparamos con el resultado producido en otros lenguajes. Si la velocidad es importante sería más conveniente optar por C, o por java para un compromiso entre velocidad y portabilidad.

Twitter
LinkedIn
Evolución, innovación y transformación
32 Service Expertise avalados por Oracle 
Nuestra propuesta de valor
Posts 100% Oracle
Sigue nuestro día a día