Pl/Python: Python dentro de PostgreSQL

Anuncio
WikiPrint - from Polar Technologies
Índice
Funciones ("Procedimientos almacenados")
1. Ejemplo simple
2. Recibir tipos compuestos
3. Devolver tipos compuestos
4. Devolver múltiples 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 Prácticos
1. Analizador de direcciones
Temas varios
1. Error con generadores y plpy.execute
2. Ejemplo de Agregados
3. Ejemplo de Disparador
Pl/Python: Python dentro de PostgreSQL
El lenguaje procedural plpython permite escribir funciones python para la base de datos relacional PostgreSQL.
Python es un lenguaje simple, moderno y flexible, fácil de aprender y usar, que posibilita el diseño rápido de todo tipo de aplicaciones multiplataforma,
ya sea en sistemas de gestión comercial como juegos o aplicaciones científicas
Para información sobre como instalarlo, ver PreguntasFrecuentes
Como se puede acceder a todas las funciones de python, no debe usarse para usuarios no confiados, por ello la u en plpythonu (u=untrusted).
Funciones ("Procedimientos almacenados")
El cuerpo de una funcion plpythonu es simplemente un script de Python. Cuando la función es llamada, sus argumentos son pasados como elementos
de una lista args; los argumentos por nombre son pasados como variables ordinarias. El resultado es devuelto de la manera usual, con un return o
un yield (en el caso que devuelvan un conjunto de resultados)
Los valores NULL de PostgreSQL equivalen a None en Python.
Está disponible el diccionario SD para almacenar datos entre cada llamada a función, y el diccionario globar GD para usar desde todas las funciones.
Nota: PostgreSQL 8.1 no soporta argumentos por nombre, recibir valores compuestos, devolver listas/tuplas o usar generadores.
Ejemplo simple
Calcular el valor máximo 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 función:
SELECT pymax(2, 3);
-- devuelve 3
1
WikiPrint - from Polar Technologies
Recibir tipos compuestos
Las funciones plpython pueden recibir tipos compuestos (ej.registros de tablas) como diccionarios:
CREATE TABLE empleado (
nombre TEXT,
salario INTEGER,
edad INTEGER
);
CREATE FUNCTION sueldo_alto (e empleado)
RETURNS boolean
AS $$
if e["salario"] > 200000:
return True
if (e["edad"] < 30) and (e["salario"] > 100000):
return True
return False
$$ LANGUAGE plpythonu;
Devolver tipos compuestos
Los tipos compuestos pueden ser devueltos como secuencias (tuplas o listas), diccionarios u objetos. En este ejemplo se devuelve un tipo compuesto
representando una persona:
CREATE TYPE persona AS (
nombre
TEXT,
apellido TEXT
);
CREATE FUNCTION crear_persona (nombre TEXT, apellido TEXT)
RETURNS persona
AS $$
return [ nombre, apellido ]
# o como tupla: return ( nombre, apellido )
# o como diccionario: return { "nombre": nombre, "apellido": apellido }
$$ LANGUAGE plpythonu;
CREATE FUNCTION crear_persona (nombre TEXT, persona TEXT)
RETURNS persona
AS $$
class Persona:
def __init__ (self, n, a):
self.nombre = n
self.apellido = a
return Persona(nombre, apellido)
$$ LANGUAGE plpythonu;
Devolver múltiples tipos escalares o compuestos (set-of)
Se puede devolver múltiples valores (usando listas/tuplas, iteradores o generadores). En este ejemplo se devuelven varios saludos:
CREATE TYPE saludo AS (
mensaje TEXT, -- hola
a_quien TEXT -- mundo
);
2
WikiPrint - from Polar Technologies
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 función 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 estarán 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.
Acceso a la base de datos
Automaticamente se importa un módulo llamado plpy.
Generar mensajes y lanzar errores
Este módulo incluye funciones de plpy.debug(msg), plpy.log(msg), plpy.info(msg), plpy.notice(msg), plpy.warning(msg),
plpy.error(msg), y plpy.fatal(msg)
plpy.error y plpy.fatal en realidad disparan una excepción python, si no se controla, se propaga y causa que la transacción se aborte.
Equivalente a llamar raise plpy.ERROR(msg) y raise plpy.FATAL(msg), respectivamente
Las otras funciones solo generan mensajes en los distintos niveles de prioridad.
Preparar y ejecutar consultas
Adicionalmente, el módulo plpy provee dos funciones: execute y prepare.
Llamar a plpy.execute(query, limit) con una consulta (query: string) y un límite de registros opcional (limit), permite ejecutar la consulta y
devuelve los resultados en un objeto que emula una lista de diccionarios, pudiendo acceder por número de fila y nombre de columna. Tiene tres
métodos adicionales: nrows que devuelve el número de filas, y status.
Ejemplo:
rv = plpy.execute("SELECT * FROM mi_tabla", 5)
for fila in rv:
print fila['columna']
3
WikiPrint - from Polar Technologies
La función plpy.prepare(query,[parameter_types]), prepara el plan de ejecución para una consulta, se le pasa la consulta como string y la
lista de tipos de parámetros:
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 parámetros ($1 y $2).
Despues de preparar la sentencia, usar la función plpy.execute para ejecutarla:
rv = plpy.execute(plan, [ "Mariano", True ], 5)
Se pasa el plan como primer argumento, los parámetros como segundo (en este caso, busca nombre="Mariano" y si esta casado). El límite (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 función...
$$ LANGUAGE plpythonu;
Ejemplos Prácticos
Analizador de direcciones
En este ejemplo completo, se utiliza una función para analizar y desglozar una dirección 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 función:
CREATE TYPE direccion AS (
calle1 TEXT,
calle2 TEXT,
altura INTEGER
);
Luego creamos la funcion analizar_dir con el código 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)
4
WikiPrint - from Polar Technologies
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 función, 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
calle2 text
altura integer
balcarce
rivadavia
50
También 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")
5
WikiPrint - from Polar Technologies
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)
6
WikiPrint - from Polar Technologies
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 ('');
7
Descargar