PL Python
PL Python
ndice Funciones ("Procedimientos almacenados") 1. Ejemplo simple 2. Recibir tipos compuestos 3. Devolver tipos compuestos 4. Devolver mltiples tipos escalares o compuestos (set-of) 2. Disparadores (Triggers) Acceso a la base de datos 1. Generar mensajes y lanzar errores 2. Preparar y ejecutar consultas Ejemplos Prcticos 1. Analizador de direcciones Temas varios 1. Error con generadores y plpy.execute 2. Ejemplo de Agregados 3. Ejemplo de Disparador
Ejemplo simple
Calcular el valor mximo entre dos enteros, descartando valores nulos: CREATE FUNCTION pymax (a integer, b integer) RETURNS integer AS $$ if (a is None) or (b is None): return None if a > b: return a return b $$ LANGUAGE plpythonu; -- invoco la funcin: SELECT pymax(2, 3); -- devuelve 3
CREATE FUNCTION saludar (mensaje TEXT) RETURNS SETOF saludo AS $$ # devolver una tupla conteniendo lista de tipos compuestos # todas las otras combinaciones son posibles return ( [ mensaje, "Mundo" ], [ mensaje, "PostgreSQL" ], [ mensaje, "PL/Python" ] ) $$ LANGUAGE plpythonu; CREATE FUNCTION saludar_generador (mensaje TEXT) RETURNS SETOF saludo AS $$ for a_quien in [ "Mundo", "PostgreSQL", "PL/Python" ]: yield ( mensaje, a_quien ) $$ LANGUAGE plpythonu;
Disparadores (Triggers)
Cuando una funcin plpython es usada en un disparador, el diccionario TD contiene: TD["new"]: valores nuevos de la fila afectada (diccionario) TD["old"]: valores viejos de la fila afectada (diccionario) TD["event"]: tipo de evento "INSERT", "UPDATE", "DELETE", o "UNKNOWN" TD["when"]: momento en que se ejecut: "BEFORE" (antes del commit), "AFTER" (despues del commit), o "UNKNOWN" TD["level"]: nivel al que se ejecut: "ROW" (por fila), "STATEMENT" (por sentencia), o "UNKNOWN" TD["name"]: nombre del disparador TD["table_name"]: nombre de la tabla en que se dispar TD["table_schema"]: esquema en el que se dispar TD["relid"]: OID de la tabla que dispar Si el comando CREATE TRIGGER incluy argumentos, estos estarn disponibles en la lista TD["args"]
Si TD["when"] es BEFORE, se puede devolver None or "OK" para indicar que la fila no se modific, "SKIP" para abortar el evento, o "MODIFY" para indicar que hemos modificado la fila.
La funcin plpy.prepare(query,[parameter_types]), prepara el plan de ejecucin para una consulta, se le pasa la consulta como string y la lista de tipos de parmetros: plan = plpy.prepare("SELECT apellido FROM usuario WHERE nombre = $1 AND casado = $2 ", [ "text", "boolean" ]) text y boolean son los tipos de la variables que se pasara como parmetros ($1 y $2). Despues de preparar la sentencia, usar la funcin plpy.execute para ejecutarla: rv = plpy.execute(plan, [ "Mariano", True ], 5) Se pasa el plan como primer argumento, los parmetros como segundo (en este caso, busca nombre="Mariano" y si esta casado). El lmite (tercer argumento) es opcional. Al preparar un plan, este se almacena para usarlo posteriormente. Para usarlo eficazmente entre llamada y llamada, se debe usar un diccionario de almacenamiento persistente (SD o GD) para guardarlo: CREATE FUNCTION usar_plan_guardado() RETURNS trigger AS $$ if SD.has_key("plan"): plan = SD["plan"] # est el plan, lo reutilizo else: # no esta el plan, lo creo y almaceno en el diccionario persistente plan = plpy.prepare("SELECT 1") SD["plan"] = plan # continua la funcin... $$ LANGUAGE plpythonu;
Ejemplos Prcticos
Analizador de direcciones
En este ejemplo completo, se utiliza una funcin para analizar y desglozar una direccin ingresada como un texto (string), en tres campos: calle1 (principal), calle2 (transversal) y altura (n en la calle principal). Para ello creamos el tipo direccion para contener estos campos desglozados que devuelve nuestra funcin: CREATE TYPE direccion AS ( calle1 TEXT, calle2 TEXT, altura INTEGER ); Luego creamos la funcion analizar_dir con el cdigo python propiamente dicho: CREATE OR REPLACE FUNCTION analizar_dir(dir text) RETURNS direccion AS $BODY$ def is_numeric(x): return not [y for y in x if not '0'<=x<='9'] nombres = ["","",""] altura = "" ss = dir.strip() + " " calle = 0 # comienzo a procesar por la primer calle; n = p = "" for c in ss: if c in (' ', '.', ',', '/', '-'): # procesar separador: if n: if nombres[calle].strip().lower() in ('ruta', 'r.p.', 'rp', 'r.n.', 'rn'): # tomar el numero de ruta (nacional o provincial)
nombres[calle] += n + ' ' n = '' else: # tomar el numero como altura altura = n n = "" calle+=1 elif p.lower() in ('y', 'e', 'entre', 'u') or c in ('/', '-') or p[0:3]=='esq': # la palabra es separador de calles if nombres[calle]: # pasar a la siguiente calle calle+=1 elif p.lower() not in ('calle', 'avenida','al','n?','nro') and c not in ('.',) and (p or is_numeric(p)): # agregar la palabra como parte de esta calle nombres[calle] += p + " " p = "" elif '0' <= c <= '9': if calle==0 and nombres[calle]: # agregar el caracter como parte del numero n += c else: # agregar el caracter como parte de la palabra actual p += c else: # agrego el caracter a la palabra actual cc = c.lower() if ('a'<=cc<='z' or '0'<=cc<='9'): p += c; else: p += "?" # comodin de una letra por las dudas (si no es un caracter numerico o alfa) if calle>2: break # devolver las dos primeras calles (si hay) y la altura return nombres[0], nombres[1], altura and int(altura[-6:]) or None $BODY$ LANGUAGE 'plpythonu' IMMUTABLE; Para ejecutar la funcin, podemos simplemente llamarla desde la clausula from ya que devuelve un tipo de datos compuesto: select calle1, calle2, altura from analizar_dir('balcarce 50 esquina rivadavia') Lo que nos devolver: calle1 text balcarce calle2 text rivadavia altura integer 50
Tambin se la puede llamar desde una consulta analizando datos de una tabla (ej. campo lugar de novedades): SELECT (d.dir).calle1, (d.dir).calle2, (d.dir).altura FROM (SELECT analizar_dir(lugar) AS dir FROM novedades) d
Temas varios
Error con generadores y plpy.execute
Si se usa un generador (yield) para devolver filas (setof) y al mismo tiempo se usa plpy.execute: CREATE OR REPLACE FUNCTION mi_funcion2() RETURNS SETOF mi_tabla AS $BODY$ plpy.execute("select 1")
yield [None, None, None] $BODY$ LANGUAGE 'plpythonu' VOLATILE; SELECT * FROM mi_funcion2() Se producir un error: ********** Error ********** ERROR: error fetching next item from iterator SQL state: 22000 Utilizar return en lugar de yield
Ejemplo de Agregados
CREATE OR REPLACE FUNCTION py_disp() RETURNS trigger AS $$ plpy.notice("Campo 1 = %s" % TD["new"]["campo1"]) if TD["new"]["campo1"] == "": raise RuntimeError("El campo no puede ser vacio") $$ language plpythonu; create table tabla_prueba (campo1 text) CREATE TRIGGER disp_prueba BEFORE INSERT OR UPDATE OR DELETE ON tabla_prueba FOR EACH ROW EXECUTE PROCEDURE py_disp(); INSERT INTO tabla_prueba VALUES ('')
CREATE OR REPLACE FUNCTION first_agg(text, text) RETURNS text AS $BODY$ if not args[0]: return args[1] else: return args[0] $BODY$ LANGUAGE 'plpythonu' VOLATILE COST 100; CREATE select select UPDATE AGGREGATE first (text) ( sfunc = first_agg, stype = text ); * from part; price, first(pname) , sum(stock), count(pno) from part GROUP BY PRICE; part set price=15 where pno in (2, 4)
Ejemplo de Disparador
CREATE OR REPLACE FUNCTION py_disp() RETURNS trigger AS $$ plpy.notice("Campo 1 = %s" % TD["new"]["campo1"]) if TD["new"]["campo1"] == "": raise RuntimeError("El campo no puede ser vacio") $$ language plpythonu; create table tabla_prueba (campo1 text)
CREATE TRIGGER disp_prueba BEFORE INSERT OR UPDATE OR DELETE ON tabla_prueba FOR EACH ROW EXECUTE PROCEDURE py_disp(); INSERT INTO tabla_prueba VALUES ('hola'); INSERT INTO tabla_prueba VALUES ('');