Puedes escribir funciones definidas por el usuario en C (o en un lenguaje que pueda hacerse compatible con C, como C++). Estas funciones se compilan en objetos cargables dinámicamente (también llamados bibliotecas compartidas) y son cargadas por el servidor a demanda. La funcionalidad de carga dinámica es lo que distingue a las funciones en “lenguaje C” de las funciones “internas”; las convenciones de programación reales son esencialmente las mismas para ambas. (Por lo tanto, la biblioteca estándar de funciones internas es una rica fuente de ejemplos de código para funciones C definidas por el usuario).
Actualmente solo se utiliza una convención de llamada para las funciones C
(“versión 1”). El soporte para esa convención de llamada se
indica escribiendo una llamada a la macro PG_FUNCTION_INFO_V1()
para la función, como se ilustra a continuación.
La primera vez que se llama en una sesión a una función definida por el usuario en un
archivo de objeto cargable particular, el cargador dinámico carga ese archivo de objeto en la memoria para que se
pueda llamar a la función. Por lo tanto, el comando CREATE FUNCTION
para una función C definida por el usuario debe especificar dos elementos de
información para la función: el nombre del archivo de
objeto cargable y el nombre en C (símbolo de enlace) de la función específica a llamar
dentro de ese archivo de objeto. Si no se especifica explícitamente el nombre en C,
se asume que es el mismo que el nombre de la función SQL.
Se utiliza el siguiente algoritmo para localizar el archivo de objeto compartido
basado en el nombre proporcionado en el comando CREATE FUNCTION:
Si el nombre es una ruta absoluta, se carga el archivo dado.
Si el nombre comienza con la cadena $libdir,
esa parte se reemplaza por el nombre del directorio de biblioteca de paquetes de
PostgreSQL, el cual se determina en el momento de la
compilación.
Si el nombre no contiene una parte de directorio, se busca el archivo en la ruta especificada por la variable de configuración dynamic_library_path.
De lo contrario (el archivo no se encontró en la ruta, o contiene una parte de directorio no absoluta), el cargador dinámico intentará tomar el nombre tal como se proporciona, lo que muy probablemente fallará. (No es fiable depender del directorio de trabajo actual).
Si esta secuencia no funciona, se añade al nombre proporcionado la extensión de
nombre de archivo de biblioteca compartida específica de la plataforma (a menudo
.so) y se vuelve a intentar esta secuencia. Si eso también
falla, la carga fallará.
Se recomienda ubicar las bibliotecas compartidas de forma relativa a
$libdir o mediante la ruta de la biblioteca dinámica.
Esto simplifica las actualizaciones de versión si la nueva instalación se encuentra en una
ubicación diferente. El directorio real al que representa
$libdir se puede conocer con el
comando pg_config --pkglibdir.
El ID de usuario bajo el cual se ejecuta el servidor de PostgreSQL debe poder recorrer la ruta hacia el archivo que deseas cargar. Hacer que el archivo o un directorio de nivel superior no sea legible y/o no ejecutable por el usuario postgres es un error común.
En cualquier caso, el nombre de archivo que se proporciona en el
comando CREATE FUNCTION se registra literalmente
en los catálogos del sistema, por lo que si es necesario volver a cargar
el archivo se aplica el mismo procedimiento.
PostgreSQL no compilará una función C
automáticamente. El archivo de objeto debe compilarse antes de que se haga referencia a él
en un comando CREATE FUNCTION. Consulta la Section 36.10.5 para obtener
información adicional.
Para garantizar que un archivo de objeto cargado dinámicamente no se cargue en un
servidor incompatible, PostgreSQL comprueba que el
archivo contenga un “bloque mágico” con el contenido adecuado.
Esto permite al servidor detectar incompatibilidades obvias, como código
compilado para una versión principal diferente de
PostgreSQL. Para incluir un bloque mágico,
escribe esto en uno (y solo uno) de los archivos de origen del módulo, después de haber
incluido el encabezado fmgr.h:
PG_MODULE_MAGIC;
o
PG_MODULE_MAGIC_EXT(parameters);
La variante PG_MODULE_MAGIC_EXT permite la especificación
de información adicional sobre el módulo; actualmente, se puede añadir un nombre y/o una
cadena de versión. (Se podrían permitir más campos en el futuro).
Escribe algo como esto:
PG_MODULE_MAGIC_EXT(
.name = "my_module_name",
.version = "1.2.3"
);
Posteriormente, el nombre y la versión se pueden examinar mediante
la función pg_get_loaded_modules().
El significado de la cadena de versión no está restringido
por PostgreSQL, pero se recomienda el uso de las reglas
de versionado semántico.
Después de que se utiliza por primera vez, un archivo de objeto cargado dinámicamente se retiene en memoria. Las futuras llamadas en la misma sesión a las funciones en ese archivo solo incurrirán en la pequeña sobrecarga de una búsqueda en la tabla de símbolos. Si necesitas forzar la recarga de un archivo de objeto, por ejemplo después de recompilarlo, inicia una nueva sesión.
Opcionalmente, un archivo cargado dinámicamente puede contener una función de inicialización.
Si el archivo incluye una función llamada _PG_init, esa función se llamará
inmediatamente después de cargar el archivo. La función no recibe parámetros y debe devolver void.
Actualmente no hay forma de descargar un archivo cargado dinámicamente.
Para saber cómo escribir funciones en lenguaje C, necesitas saber cómo representa internamente PostgreSQL los tipos de datos base y cómo se pueden pasar hacia y desde las funciones. Internamente, PostgreSQL considera un tipo base como un “bloque de memoria”. Las funciones definidas por el usuario que defines sobre un tipo definen a su vez la forma en que PostgreSQL puede operar con él. Es decir, PostgreSQL solo almacenará y recuperará los datos del disco y utilizará tus funciones definidas por el usuario para ingresar, procesar y generar los datos.
Los tipos base pueden tener uno de tres formatos internos:
paso por valor, longitud fija
paso por referencia, longitud fija
paso por referencia, longitud variable
Los tipos por valor solo pueden tener 1, 2 o 4 bytes de longitud
(también 8 bytes, si sizeof(Datum) es 8 en tu máquina).
Debes tener cuidado al definir tus tipos de manera que tengan el
mismo tamaño (in bytes) en todas las arquitecturas. Por ejemplo, el
tipo long es peligroso porque tiene 4 bytes en algunas
máquinas y 8 bytes en otras, mientras que el tipo int tiene 4 bytes
en la mayoría de las máquinas Unix. Una implementación razonable del
tipo int4 en máquinas Unix podría ser:
/* entero de 4 bytes, pasado por valor */ typedef int int4;
(El código C real de PostgreSQL llama a este tipo int32, porque
es una convención en C que int
significa XXXX bits. Ten en cuenta
por lo tanto también que el tipo C int8 tiene un tamaño de 1 byte. El
tipo SQL int8 se llama int64 en C. Consulta también la
Table 36.2).
Por otro lado, los tipos de longitud fija de cualquier tamaño se pueden pasar por referencia. Por ejemplo, aquí tienes una muestra de la implementación de un tipo de PostgreSQL:
/* estructura de 16 bytes, pasada por referencia */
typedef struct
{
double x, y;
} Point;
Solo se pueden utilizar punteros a dichos tipos al pasarlos hacia y
desde las funciones de PostgreSQL.
Para devolver un valor de este tipo, asigna la cantidad correcta de
memoria con palloc, llena la memoria asignada,
y devuelve un puntero a ella. (Además, si solo quieres devolver el
mismo valor que uno de tus argumentos de entrada que es del mismo tipo de datos,
puedes omitir el palloc adicional y simplemente devolver el
puntero al valor de entrada).
Por último, todos los tipos de longitud variable también deben pasarse
por referencia. Todos los tipos de longitud variable deben comenzar
con un campo de longitud opaco de exactamente 4 bytes, el cual será establecido
por SET_VARSIZE; ¡nunca establezcas este campo directamente! Todos los datos
que se almacenen dentro de ese tipo deben ubicarse en la memoria
inmediatamente después de ese campo de longitud. El
campo de longitud contiene la longitud total de la estructura,
es decir, incluye el tamaño del campo de longitud
en sí.
Otro punto importante es evitar dejar bits sin inicializar dentro de los valores de los tipos de datos; por ejemplo, ten cuidado de rellenar con ceros cualquier byte de alineación que pueda estar presente en las estructuras. Sin esto, el planificador podría ver constantes lógicamente equivalentes de tu tipo de datos como desiguales, lo que llevaría a planes ineficientes (aunque no incorrectos).
Nunca modifiques el contenido de un valor de entrada pasado por referencia. Si lo haces, es probable que corrompas los datos en disco, ya que el puntero que se te proporciona podría apuntar directamente a un búfer de disco. La única excepción a esta regla se explica en la Section 36.12.
Como ejemplo, podemos definir el tipo text de la siguiente
manera:
typedef struct {
int32 length;
char data[FLEXIBLE_ARRAY_MEMBER];
} text;
La notación [FLEXIBLE_ARRAY_MEMBER] significa que la longitud
real de la parte de datos no está especificada por esta declaración.
Al manipular tipos de longitud variable, debemos tener cuidado de asignar
la cantidad correcta de memoria y establecer el campo de longitud correctamente.
Por ejemplo, si quisiéramos almacenar 40 bytes en una estructura text,
podríamos utilizar un fragmento de código como este:
#include "postgres.h" ... char buffer[40]; /* nuestros datos de origen */ ... text *destination = (text *) palloc(VARHDRSZ + 40); SET_VARSIZE(destination, VARHDRSZ + 40); memcpy(destination->data, buffer, 40); ...
VARHDRSZ es lo mismo que sizeof(int32), pero
se considera un buen estilo utilizar la macro VARHDRSZ
para referirse al tamaño del encabezado de un tipo de longitud variable.
Además, el campo de longitud debe establecerse utilizando la
macro SET_VARSIZE, no mediante una asignación simple.
La Table 36.2 muestra los tipos C
correspondientes a muchos de los tipos de datos SQL integrados
de PostgreSQL.
La columna “Definido en” indica el archivo de cabecera que
debe incluirse para obtener la definición del tipo. (La definición real
podría estar en un archivo diferente que sea incluido por el archivo
listado. Se recomienda que los usuarios se ciñan a la interfaz
definida). Ten en cuenta que siempre debes incluir
postgres.h primero en cualquier archivo de origen de código del
servidor, porque declara una serie de cosas que necesitarás de todos
modos, y porque incluir otros encabezados primero puede causar problemas
de portabilidad.
Table 36.2. Tipos C equivalentes para tipos SQL integrados
| Tipo SQL | Tipo C | Definido en |
|---|---|---|
boolean | bool | postgres.h (maybe compiler built-in) |
box | BOX* | utils/geo_decls.h |
bytea | bytea* | postgres.h |
"char" | char | (compiler built-in) |
character | BpChar* | postgres.h |
cid | CommandId | postgres.h |
date | DateADT | utils/date.h |
float4 (real) | float4 | postgres.h |
float8 (double precision) | float8 | postgres.h |
int2 (smallint) | int16 | postgres.h |
int4 (integer) | int32 | postgres.h |
int8 (bigint) | int64 | postgres.h |
interval | Interval* | datatype/timestamp.h |
lseg | LSEG* | utils/geo_decls.h |
name | Name | postgres.h |
numeric | Numeric | utils/numeric.h |
oid | Oid | postgres.h |
oidvector | oidvector* | postgres.h |
path | PATH* | utils/geo_decls.h |
point | POINT* | utils/geo_decls.h |
regproc | RegProcedure | postgres.h |
text | text* | postgres.h |
tid | ItemPointer | storage/itemptr.h |
time | TimeADT | utils/date.h |
time with time zone | TimeTzADT | utils/date.h |
timestamp | Timestamp | datatype/timestamp.h |
timestamp with time zone | TimestampTz | datatype/timestamp.h |
varchar | VarChar* | postgres.h |
xid | TransactionId | postgres.h |
Ahora que hemos repasado todas las estructuras posibles para los tipos base, podemos mostrar algunos ejemplos de funciones reales.
La convención de llamada de la versión 1 se apoya en macros para suprimir la mayor parte de la complejidad de pasar argumentos y resultados. La declaración en C de una función de la versión 1 siempre es:
Datum funcname(PG_FUNCTION_ARGS)
Además, la llamada a la macro:
PG_FUNCTION_INFO_V1(funcname);
debe aparecer en el mismo archivo de código fuente. (Por convención, se escribe
justo antes de la propia función). Esta llamada a la macro no es necesaria para
las funciones en lenguaje internal, ya que
PostgreSQL asume que todas las funciones internas utilizan
la convención de la versión 1. Sin embargo, sí es requerida para funciones cargadas
dinámicamente.
En una función de versión 1, cada argumento real se recupera utilizando una macro
PG_GETARG_ que corresponde al
tipo de datos del argumento. (En funciones no estrictas, es necesario realizar una
comprobación previa de la nulidad del argumento utilizando xxx()PG_ARGISNULL();
ver más abajo). El resultado se devuelve utilizando una macro
PG_RETURN_ para el tipo de retorno.
xxx()PG_GETARG_ toma como argumento
el número del argumento de la función que se desea recuperar, comenzando el conteo en 0.
xxx()PG_RETURN_ toma como argumento el valor real
a devolver.
xxx()
Para invocar otra función de versión 1, puedes utilizar
DirectFunctionCall.
Esto es particularmente útil cuando deseas invocar funciones definidas en la biblioteca
interna estándar, utilizando una interfaz similar a su firma SQL.
n(func, arg1, ..., argn)
Estas funciones de conveniencia y otras similares se pueden encontrar en
fmgr.h. La familia
DirectFunctionCall espera el nombre de una
función C como primer argumento. También existe
nOidFunctionCall, que toma el OID de la
función destino, y algunas otras variantes. Todas estas esperan que los argumentos de la
función se suministren como nDatums, e igualmente devuelven Datum.
Ten en cuenta que no se permite que los argumentos ni el resultado sean NULL cuando se utilizan
estas funciones de conveniencia.
Por ejemplo, para invocar la función starts_with(text, text) desde C,
puedes buscar en el catálogo y encontrar que su implementación en C es la función
Datum text_starts_with(PG_FUNCTION_ARGS). Normalmente utilizarías
DirectFunctionCall2(text_starts_with, ...) para invocar dicha función.
Sin embargo, starts_with(text, text) requiere información de ordenamiento (collation),
por lo que fallará con “could not determine which collation to use for string comparison”
si se invoca de esa manera. En su lugar, debes utilizar
DirectFunctionCall2Coll(text_starts_with, ...) y proporcionar el ordenamiento
deseado, el cual típicamente se pasa directamente desde PG_GET_COLLATION(),
como se muestra en el ejemplo de abajo.
fmgr.h también suministra macros que facilitan las conversiones entre
tipos C y Datum. Por ejemplo, para convertir un Datum en
text*, puedes utilizar DatumGetTextPP(X). Mientras que algunos
tipos tienen macros llamadas como TypeGetDatum(X) para la conversión inversa,
text* no las tiene; basta con utilizar la macro genérica
PointerGetDatum(X) para ello. Si tu extensión define tipos adicionales,
usualmente también es conveniente definir macros similares para tus tipos.
Aquí tienes algunos ejemplos utilizando la convención de llamada de la versión 1:
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include "varatt.h"
PG_MODULE_MAGIC;
/* por valor */
PG_FUNCTION_INFO_V1(add_one);
Datum
add_one(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}
/* por referencia, longitud fija */
PG_FUNCTION_INFO_V1(add_one_float8);
Datum
add_one_float8(PG_FUNCTION_ARGS)
{
/* Las macros para FLOAT8 ocultan su naturaleza de paso por referencia. */
float8 arg = PG_GETARG_FLOAT8(0);
PG_RETURN_FLOAT8(arg + 1.0);
}
PG_FUNCTION_INFO_V1(makepoint);
Datum
makepoint(PG_FUNCTION_ARGS)
{
/* Aquí, la naturaleza de paso por referencia de Point no está oculta. */
Point *pointx = PG_GETARG_POINT_P(0);
Point *pointy = PG_GETARG_POINT_P(1);
Point *new_point = (Point *) palloc(sizeof(Point));
new_point->x = pointx->x;
new_point->y = pointy->y;
PG_RETURN_POINT_P(new_point);
}
/* por referencia, longitud variable */
PG_FUNCTION_INFO_V1(copytext);
Datum
copytext(PG_FUNCTION_ARGS)
{
text *t = PG_GETARG_TEXT_PP(0);
/*
* VARSIZE_ANY_EXHDR es el tamaño de la estructura en bytes, menos el
* VARHDRSZ o VARHDRSZ_SHORT de su encabezado. Construye la copia con un
* encabezado de longitud completa.
*/
text *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
/*
* VARDATA es un puntero a la región de datos de la nueva estructura. El origen
* podría ser un dato corto, por lo que recupera sus datos mediante VARDATA_ANY.
*/
memcpy(VARDATA(new_t), /* destino */
VARDATA_ANY(t), /* origen */
VARSIZE_ANY_EXHDR(t)); /* cuántos bytes */
PG_RETURN_TEXT_P(new_t);
}
PG_FUNCTION_INFO_V1(concat_text);
Datum
concat_text(PG_FUNCTION_ARGS)
{
text *arg1 = PG_GETARG_TEXT_PP(0);
text *arg2 = PG_GETARG_TEXT_PP(1);
int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
text *new_text = (text *) palloc(new_text_size);
SET_VARSIZE(new_text, new_text_size);
memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
PG_RETURN_TEXT_P(new_text);
}
/* Un wrapper alrededor de starts_with(text, text) */
PG_FUNCTION_INFO_V1(t_starts_with);
Datum
t_starts_with(PG_FUNCTION_ARGS)
{
text *t1 = PG_GETARG_TEXT_PP(0);
text *t2 = PG_GETARG_TEXT_PP(1);
Oid collid = PG_GET_COLLATION();
bool result;
result = DatumGetBool(DirectFunctionCall2Coll(text_starts_with,
collid,
PointerGetDatum(t1),
PointerGetDatum(t2)));
PG_RETURN_BOOL(result);
}
Suponiendo que el código anterior se ha preparado en el archivo
funcs.c y se ha compilado en un objeto compartido,
podríamos definir las funciones en PostgreSQL
con comandos como este:
CREATE FUNCTION add_one(integer) RETURNS integer
AS 'DIRECTORY/funcs', 'add_one'
LANGUAGE C STRICT;
-- nota la sobrecarga del nombre de función SQL "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
AS 'DIRECTORY/funcs', 'add_one_float8'
LANGUAGE C STRICT;
CREATE FUNCTION makepoint(point, point) RETURNS point
AS 'DIRECTORY/funcs', 'makepoint'
LANGUAGE C STRICT;
CREATE FUNCTION copytext(text) RETURNS text
AS 'DIRECTORY/funcs', 'copytext'
LANGUAGE C STRICT;
CREATE FUNCTION concat_text(text, text) RETURNS text
AS 'DIRECTORY/funcs', 'concat_text'
LANGUAGE C STRICT;
CREATE FUNCTION t_starts_with(text, text) RETURNS boolean
AS 'DIRECTORY/funcs', 't_starts_with'
LANGUAGE C STRICT;
Aquí, DIRECTORY representa el directorio del
archivo de biblioteca compartida (por ejemplo, el directorio del tutorial de
PostgreSQL, que contiene el código de los ejemplos
utilizados en esta sección). (Un estilo mejor sería utilizar simplemente
'funcs' en la cláusula AS, después de haber
añadido DIRECTORY a la ruta de búsqueda. En cualquier
caso, podemos omitir la extensión específica del sistema para una biblioteca
compartida, comúnmente .so).
Observa que hemos especificado las funciones como “strict”, lo que
significa que el sistema debe asumir automáticamente un resultado nulo si cualquier
valor de entrada es nulo. Al hacer esto, nos evitamos tener que comprobar entradas
nulas en el código de la función. Sin esto, tendríamos que comprobar los valores nulos
explícitamente, utilizando PG_ARGISNULL().
La macro PG_ARGISNULL( permite
a una función comprobar si cada entrada es nula. (Por supuesto, hacer esto solo es
necesario en funciones que no se declaran como “strict”). Al igual que
con las macros n)PG_GETARG_,
los argumentos de entrada se cuentan comenzando desde cero. Ten en cuenta que debes
abstenerte de ejecutar xxx()PG_GETARG_
hasta haber verificado que el argumento no es nulo. Para devolver un resultado nulo,
ejecuta xxx()PG_RETURN_NULL(); esto funciona tanto en funciones estrictas
como no estrictas.
A primera vista, las convenciones de codificación de la versión 1 podrían parecer
simplemente un oscurantismo sin sentido, en comparación con el uso de las convenciones
de llamada normales de C. Sin embargo, nos permiten manejar
argumentos/valores de retorno que admiten valores NULL, y valores
“toasted” (comprimidos o fuera de línea).
Otras opciones proporcionadas por la interfaz de la versión 1 son dos variantes de las
macros PG_GETARG_. La primera
de ellas, xxx()PG_GETARG_, garantiza
devolver una copia del argumento especificado en la que sea seguro escribir. (Las macros
normales a veces devolverán un puntero a un valor que está almacenado físicamente en una
tabla, en el cual no se debe escribir. El uso de las macros
xxx_COPY()PG_GETARG_ garantiza un resultado
escribible). La segunda variante consiste en las macros
xxx_COPY()PG_GETARG_ que toman tres
argumentos. El primero es el número del argumento de la función (como se indica arriba).
El segundo y el tercero son el desplazamiento y la longitud del segmento a devolver.
Los desplazamientos se cuentan desde cero, y una longitud negativa solicita que se devuelva
el resto del valor. Estas macros proporcionan un acceso más eficiente a partes de valores
grandes en caso de que tengan el tipo de almacenamiento “external”. (El tipo
de almacenamiento de una columna se puede especificar utilizando
xxx_SLICE()ALTER TABLE .
tablename ALTER COLUMN colname SET STORAGE storagetypestoragetype es uno de los siguientes: plain,
external, extended o main).
Finalmente, las convenciones de llamada a funciones de la versión 1 hacen posible
devolver conjuntos de resultados (Section 36.10.9) e implementar
funciones de disparador (Chapter 37) y controladores de llamadas de
lenguajes procedimentales (Chapter 57). Para más detalles, consulta
src/backend/utils/fmgr/README en la distribución del código fuente.
Antes de pasar a temas más avanzados, deberíamos discutir algunas reglas de codificación para las funciones en lenguaje C de PostgreSQL. Si bien podría ser posible cargar funciones escritas en lenguajes distintos de C en PostgreSQL, esto suele ser difícil (cuando es posible en absoluto) porque otros lenguajes, como C++, FORTRAN o Pascal a menudo no siguen la misma convención de llamada que C. Es decir, otros lenguajes no pasan los argumentos y los valores de retorno entre funciones de la misma manera. Por esta razón, asumiremos que tus funciones en lenguaje C están escritas realmente en C.
Las reglas básicas para escribir y compilar funciones en C son las siguientes:
Utiliza pg_config
--includedir-server
para saber dónde están instalados los archivos de cabecera del servidor
PostgreSQL en tu sistema (o en el sistema en el que
se ejecutarán tus usuarios).
Compilar y enlazar tu código para que pueda ser cargado dinámicamente en PostgreSQL siempre requiere flags especiales. Consulta Section 36.10.5 para obtener una explicación detallada de cómo hacerlo para tu sistema operativo en particular.
Recuerda definir un “magic block” (bloque mágico) para tu biblioteca compartida, como se describe en Section 36.10.1.
Al asignar memoria, utiliza las funciones de PostgreSQL
palloc y pfree
en lugar de las funciones correspondientes de la biblioteca estándar de C
malloc y free.
La memoria asignada por palloc se liberará
automáticamente al final de cada transacción, evitando fugas de memoria.
Pon siempre a cero los bytes de tus estructuras utilizando memset
(o asígnalas con palloc0 en primer lugar).
Aunque asignes un valor a cada campo de tu estructura, podría haber un relleno de
alineación (huecos en la estructura) que contenga valores basura. Sin esto, es difícil
soportar índices hash o uniones hash, ya que debes seleccionar solo los bits
significativos de tu estructura de datos para calcular un hash. El planificador
también depende a veces de la comparación de constantes mediante igualdad de bits,
por lo que puedes obtener resultados de planificación no deseados si los valores
lógicamente equivalentes no son idénticos a nivel de bits.
La mayoría de los tipos internos de PostgreSQL
se declaran en postgres.h, mientras que las interfaces
del gestor de funciones (PG_FUNCTION_ARGS, etc.) están en
fmgr.h, por lo que necesitarás incluir al menos estos
dos archivos. Por razones de portabilidad, es mejor incluir
postgres.h primero, antes de cualquier
otro archivo de cabecera del sistema o del usuario. Incluir
postgres.h también incluirá
elog.h y palloc.h por ti.
Los nombres de símbolos definidos dentro de los archivos de objeto no deben entrar en conflicto entre sí ni con los símbolos definidos en el ejecutable del servidor PostgreSQL. Tendrás que renombrar tus funciones o variables si recibes mensajes de error al respecto.
Antes de que puedas usar tus funciones de extensión de PostgreSQL escritas en C, deben compilarse y enlazarse de una manera especial para producir un archivo que el servidor pueda cargar dinámicamente. Para ser precisos, se debe crear una biblioteca compartida (shared library).
Para obtener información más allá de lo que se detalla en esta sección,
debes leer la documentación de tu sistema operativo, en particular las páginas del
manual del compilador de C, cc, y del editor de enlaces, ld.
Además, el código fuente de PostgreSQL contiene varios ejemplos
prácticos en el directorio contrib. Sin embargo, si te basas en
estos ejemplos, harás que tus módulos dependan de la disponibilidad del código
fuente de PostgreSQL.
La creación de bibliotecas compartidas es generalmente análoga al enlace de ejecutables: primero los archivos fuente se compilan en archivos objeto, luego los archivos objeto se enlazan entre sí. Los archivos objeto deben crearse como código independiente de la posición (PIC), lo que conceptualmente significa que pueden colocarse en una ubicación arbitraria en la memoria cuando son cargados por el ejecutable. (Los archivos objeto destinados a ejecutables no suelen compilarse de esa manera). El comando para enlazar una biblioteca compartida contiene banderas (flags) especiales para distinguirlo del enlace de un ejecutable (al menos en teoría; en algunos sistemas la práctica es mucho más compleja).
En los siguientes ejemplos asumimos que tu código fuente está en un archivo
foo.c y crearemos una biblioteca compartida
foo.so. El archivo objeto intermedio se llamará
foo.o a menos que se indique lo contrario. Una biblioteca
compartida puede contener más de un archivo objeto, pero aquí solo usamos uno.
La bandera del compilador para crear PIC es
-fPIC. Para crear bibliotecas compartidas, la bandera
del compilador es -shared.
cc -fPIC -c foo.c cc -shared -o foo.so foo.o
Esto es aplicable a partir de la versión 13.0 de
FreeBSD, las versiones anteriores utilizaban
el compilador gcc.
La bandera del compilador para crear PIC es
-fPIC.
La bandera del compilador para crear una biblioteca compartida es
-shared. Un ejemplo completo se ve así:
cc -fPIC -c foo.c cc -shared -o foo.so foo.o
Aquí tienes un ejemplo. Asume que las herramientas de desarrollo están instaladas.
cc -c foo.c cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o
La bandera del compilador para crear PIC es
-fPIC. Para sistemas ELF, se utiliza el
compilador con la bandera -shared para enlazar
bibliotecas compartidas. En los sistemas más antiguos que no son ELF, se utiliza
ld -Bshareable.
gcc -fPIC -c foo.c gcc -shared -o foo.so foo.o
La bandera del compilador para crear PIC es
-fPIC. Se utiliza ld -Bshareable
para enlazar bibliotecas compartidas.
gcc -fPIC -c foo.c ld -Bshareable -o foo.so foo.o
La bandera del compilador para crear PIC es
-KPIC con el compilador de Sun y
-fPIC con GCC. Para
enlazar bibliotecas compartidas, la opción del compilador es
-G con cualquiera de los compiladores o, alternativamente,
-shared con GCC.
cc -KPIC -c foo.c cc -G -o foo.so foo.o
o
gcc -fPIC -c foo.c gcc -G -o foo.so foo.o
Si esto es demasiado complicado para ti, deberías considerar el uso de GNU Libtool, el cual oculta las diferencias de plataforma detrás de una interfaz uniforme.
El archivo de biblioteca compartida resultante se puede cargar luego en
PostgreSQL. Al especificar el nombre del archivo
en el comando CREATE FUNCTION, se debe indicar el nombre de
la biblioteca compartida, no el del archivo objeto intermedio.
Ten en cuenta que la extensión de biblioteca compartida estándar del sistema
(generalmente .so o .sl) se puede omitir del
comando CREATE FUNCTION, y normalmente debería
omitirse para una mejor portabilidad.
Consulta la Section 36.10.1 para saber dónde espera el servidor encontrar los archivos de biblioteca compartida.
Esta sección contiene orientación para los autores de extensiones y otros plugins del servidor sobre la estabilidad de la API y ABI en el servidor PostgreSQL.
El servidor PostgreSQL contiene varias API bien delimitadas para plugins del servidor, como el gestor de funciones (fmgr, descrito en este capítulo), SPI (Chapter 45) y varios hooks diseñados específicamente para extensiones. Estas interfaces se gestionan cuidadosamente para lograr estabilidad y compatibilidad a largo plazo. Sin embargo, todo el conjunto de funciones y variables globales en el servidor constituye efectivamente la API públicamente utilizable, y la mayor parte no fue diseñada con la extensibilidad y la estabilidad a largo plazo en mente.
Por lo tanto, aunque aprovechar estas interfaces es válido, cuanto más te desvíes del camino habitual, más probable será que encuentres problemas de compatibilidad de API o ABI en algún momento. Se anima a los autores de extensiones a proporcionar comentarios sobre sus requisitos, de modo que con el tiempo, a medida que surjan nuevos patrones de uso, ciertas interfaces puedan considerarse más estables o se puedan añadir interfaces nuevas y mejor diseñadas.
La API, o interfaz de programación de aplicaciones, es la interfaz utilizada en tiempo de compilación.
No hay ninguna promesa de compatibilidad de API entre versiones
mayores de PostgreSQL. Por lo tanto, el código de las extensiones
podría requerir cambios en el código fuente para funcionar con múltiples versiones mayores.
Normalmente, esto se puede gestionar con condiciones del preprocesador
como #if PG_VERSION_NUM >= 160000.
Las extensiones sofisticadas que utilizan interfaces más allá de las bien delimitadas
suelen requerir algunos de estos cambios para cada versión mayor del servidor.
PostgreSQL hace un esfuerzo para evitar rupturas de la API del servidor en las versiones menores. In general, el código de extensión que se compila y funciona con una versión menor también debería compilarse y funcionar con cualquier otra versión menor de la misma versión mayor, pasada o futura.
Cuando se requiere un cambio, se gestionará cuidadosamente, teniendo en cuenta los requisitos de las extensiones. Dichos cambios se comunicarán en las notas de lanzamiento (Appendix E).
La ABI, o interfaz binaria de aplicación, es la interfaz utilizada en tiempo de ejecución.
Los servidores de diferentes versiones mayores tienen ABI intencionadamente
incompatibles. Por lo tanto, las extensiones que utilizan las API del servidor
deben volver a compilarse para cada versión mayor. La inclusión de
PG_MODULE_MAGIC (consulta Section 36.10.1)
garantiza que el código compilado para una versión mayor sea rechazado por otras
versiones mayores.
PostgreSQL hace un esfuerzo para evitar rupturas de la ABI del servidor en las versiones menores. En general, una extensión compilada contra cualquier versión menor debería funcionar con cualquier otra versión menor de la misma versión mayor, pasada o futura.
Cuando se requiere un cambio, PostgreSQL elegirá el cambio menos invasivo posible, por ejemplo, insertando un nuevo campo en el espacio de relleno o añadiéndolo al final de una estructura. Este tipo de cambios no deberían afectar a las extensiones a menos que utilicen patrones de código muy inusuales.
Sin embargo, en casos raros, incluso tales cambios no invasivos pueden ser inviables o imposibles. En tal evento, el cambio se gestionará cuidadosamente, teniendo en cuenta los requisitos de las extensiones. Dichos cambios también se documentarán en las notas de lanzamiento (Appendix E).
Ten en cuenta, sin embargo, que muchas partes del servidor no están diseñadas ni se mantienen como API de consumo público (y que, en la mayoría de los casos, el límite real tampoco está bien definido). Si surgen necesidades urgentes, los cambios en esas partes se realizarán de forma natural con menos consideración por el código de las extensiones que los cambios en interfaces bien definidas y ampliamente utilizadas.
Además, a falta de una detección automatizada de tales cambios, esto no es una garantía, pero históricamente tales cambios que rompen la compatibilidad han sido extremadamente raros.
Los tipos compuestos no tienen una distribución fija como las estructuras de C. Las instancias de un tipo compuesto pueden contener campos nulos. Además, los tipos compuestos que forman parte de una jerarquía de herencia pueden tener campos diferentes a los de otros miembros de la misma jerarquía. Por lo tanto, PostgreSQL proporciona una interfaz de función para acceder a los campos de los tipos compuestos desde C.
Supongamos que queremos escribir una función para responder a la consulta:
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
Utilizando las convenciones de llamada de la versión 1, podemos definir
c_overpaid como:
#include "postgres.h"
#include "executor/executor.h" /* para GetAttributeByName() */
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(c_overpaid);
Datum
c_overpaid(PG_FUNCTION_ARGS)
{
HeapTupleHeader t = PG_GETARG_HEAPTUPLEHEADER(0);
int32 limit = PG_GETARG_INT32(1);
bool isnull;
Datum salary;
salary = GetAttributeByName(t, "salary", &isnull);
if (isnull)
PG_RETURN_BOOL(false);
/* Alternativamente, podríamos preferir hacer PG_RETURN_NULL() para salarios nulos. */
PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}
GetAttributeByName es la función del sistema de
PostgreSQL que devuelve atributos de la fila especificada.
Tiene tres argumentos: el argumento de tipo HeapTupleHeader pasado a la
función, el nombre del atributo deseado y un parámetro de retorno que indica si el
atributo es nulo. GetAttributeByName devuelve un valor de tipo
Datum que puedes convertir al tipo de datos adecuado utilizando la función
DatumGet apropiada. Ten en cuenta
que el valor de retorno carece de sentido si la bandera de nulo está activa; comprueba
siempre la bandera de nulo antes de intentar hacer cualquier cosa con el resultado.
XXX()
También existe GetAttributeByNum, que selecciona el atributo objetivo
por número de columna en lugar de por nombre.
El siguiente comando declara la función c_overpaid en SQL:
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY/funcs', 'c_overpaid'
LANGUAGE C STRICT;
Observa que hemos utilizado STRICT para no tener que comprobar si los
argumentos de entrada eran NULL.
Para devolver una fila o un valor de tipo compuesto desde una función en el lenguaje C, puedes utilizar una API especial que proporciona macros y funciones para ocultar la mayor parte de la complejidad de la construcción de tipos de datos compuestos. Para utilizar esta API, el archivo fuente debe incluir:
#include "funcapi.h"
Hay dos formas de construir un valor de datos compuesto (en adelante,
una “tupla”): puedes construirlo a partir de un array de valores Datum,
o a partir de un array de cadenas de C que se pueden pasar a las funciones de conversión
de entrada de los tipos de datos de las columnas de la tupla. En cualquier caso,
primero necesitas obtener o construir un descriptor TupleDesc
para la estructura de la tupla. Cuando trabajas con Datums, pasas el
TupleDesc a BlessTupleDesc, y luego llamas a
heap_form_tuple para cada fila. Cuando trabajas con cadenas de C,
pasas el TupleDesc a TupleDescGetAttInMetadata,
y luego llamas a BuildTupleFromCStrings para cada fila. En el caso de
una función que devuelve un conjunto de tuplas, los pasos de configuración se pueden realizar
todos una sola vez durante la primera llamada a la función.
Hay varias funciones auxiliares disponibles para configurar el TupleDesc
necesario. La forma recomendada de hacer esto en la mayoría de las funciones que devuelven
valores compuestos es llamar a:
TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
Oid *resultTypeId,
TupleDesc *resultTupleDesc)
pasando la misma estructura fcinfo que se pasó a la propia función que realiza
la llamada. (Esto, por supuesto, requiere que utilices las convenciones de llamada de la versión 1).
Se puede especificar resultTypeId como NULL o como la dirección
de una variable local para recibir el OID del tipo de resultado de la función.
resultTupleDesc debe ser la dirección de una variable local de tipo
TupleDesc. Comprueba que el resultado sea TYPEFUNC_COMPOSITE;
si es así, resultTupleDesc se habrá rellenado con el TupleDesc
necesario. (Si no lo es, puedes informar de un error del estilo de “function returning record
called in context that cannot accept type record”).
get_call_result_type puede resolver el tipo real del resultado de una
función polimórfica; por lo tanto, es útil en funciones que devuelven resultados polimórficos
escalares, no solo en funciones que devuelven tipos compuestos. La salida
resultTypeId es principalmente útil para funciones que devuelven escalares
polimórficos.
get_call_result_type tiene una función hermana,
get_expr_result_type, que se puede utilizar para resolver el tipo de salida
esperado para una llamada a una función representada por un árbol de expresión. Esto se puede usar
cuando se intenta determinar el tipo de resultado desde fuera de la propia función. También existe
get_func_result_type, que se puede usar cuando solo está disponible el OID
de la función. Sin embargo, estas funciones no son capaces de lidiar con funciones declaradas
para devolver record, y get_func_result_type no puede
resolver tipos polimórficos, por lo que deberías utilizar preferentemente
get_call_result_type.
Las funciones más antiguas, ahora obsoletas, para obtener objetos
TupleDesc son:
TupleDesc RelationNameGetTupleDesc(const char *relname)
para obtener un TupleDesc para el tipo de fila de una relación nombrada,
y:
TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)
para obtener un TupleDesc basado en un OID de tipo. Esto se puede usar
para obtener un TupleDesc para un tipo base o compuesto. Sin embargo,
no funcionará para una función que devuelva record, y no puede resolver
tipos polimórficos.
Una vez que tengas un TupleDesc, llama a:
TupleDesc BlessTupleDesc(TupleDesc tupdesc)
si planeas trabajar con Datums, o:
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
si planeas trabajar con cadenas de C. Si estás escribiendo una función que devuelve un conjunto
(set), puedes guardar los resultados de estas funciones en la estructura
FuncCallContext; utiliza el campo
tuple_desc o attinmeta respectivamente.
Cuando trabajes con Datums, utiliza:
HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
para construir una HeapTuple a partir de los datos del usuario en forma de Datum.
Cuando trabajes con cadenas de C, utiliza:
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
para construir una HeapTuple a partir de los datos del usuario en forma
de cadena de C. values es un array de cadenas de C, una por cada atributo
de la fila devuelta. Cada cadena de C debe tener el formato esperado por la función de entrada
del tipo de datos del atributo. Para devolver un valor nulo en uno de los atributos, el puntero
correspondiente en el array values debe establecerse en NULL.
Esta función deberá llamarse de nuevo por cada fila que devuelvas.
Una vez que hayas construido una tupla para devolverla desde tu función, esta debe convertirse
en un Datum. Utiliza:
HeapTupleGetDatum(HeapTuple tuple)
para convertir una HeapTuple en un Datum válido. Este Datum
se puede devolver directamente si tienes la intención de devolver solo una única fila, o se
puede usar como el valor de retorno actual en una función que devuelve un conjunto.
En la siguiente sección se muestra un ejemplo.
Las funciones en lenguaje C tienen dos opciones para devolver conjuntos (múltiples filas). En un método, llamado modo ValuePerCall, una función que devuelve conjuntos es llamada repetidamente (pasando los mismos argumentos cada vez) y devuelve una nueva fila en cada llamada, hasta que no le quedan más filas que devolver y lo indica devolviendo NULL. Por lo tanto, la función que devuelve conjuntos (SRF, por sus siglas en inglés) debe guardar suficiente estado entre llamadas para recordar lo que estaba haciendo y devolver el siguiente elemento correcto en cada llamada. En el otro método, llamado modo Materialize, una SRF llena y devuelve un objeto tuplestore que contiene todo su resultado; en este caso, solo se produce una llamada para todo el resultado y no se necesita un estado entre llamadas.
Al utilizar el modo ValuePerCall, es importante recordar que no se garantiza que la consulta se
ejecute hasta el final; es decir, debido a opciones como LIMIT, el ejecutor
podría dejar de realizar llamadas a la función que devuelve conjuntos antes de que se hayan
obtenido todas las filas. Esto significa que no es seguro realizar actividades de limpieza en
la última llamada, porque podría no llegar a producirse nunca. Se recomienda utilizar el modo
Materialize para funciones que necesitan acceder a recursos externos, como descriptores de archivos.
El resto de esta sección documenta un conjunto de macros auxiliares que se utilizan comúnmente
(aunque no es obligatorio usarlas) para las SRF que utilizan el modo ValuePerCall. Se pueden
encontrar detalles adicionales sobre el modo Materialize en
src/backend/utils/fmgr/README. Además, los módulos de
contrib en la distribución del código fuente de
PostgreSQL contienen muchos ejemplos de SRF que utilizan tanto el
modo ValuePerCall como el modo Materialize.
Para utilizar las macros de soporte para ValuePerCall descritas aquí, incluye
funcapi.h. Estas macros funcionan con una estructura
FuncCallContext que contiene el estado que debe guardarse entre llamadas.
Dentro de la SRF que realiza la llamada, se utiliza fcinfo->flinfo->fn_extra
para mantener un puntero a FuncCallContext entre llamadas.
Las macros rellenan automáticamente ese campo en el primer uso, y esperan encontrar el mismo
puntero allí en los usos posteriores.
typedef struct FuncCallContext
{
/*
* Número de veces que hemos sido llamados anteriormente
*
* SRF_FIRSTCALL_INIT() inicializa call_cntr a 0 por ti, y
* se incrementa automáticamente cada vez que se llama a SRF_RETURN_NEXT().
*/
uint64 call_cntr;
/*
* Número máximo de llamadas OPCIONAL
*
* max_calls está aquí solo por conveniencia y su configuración es opcional.
* Si no se establece, debes proporcionar medios alternativos para saber cuándo
* ha terminado la función.
*/
uint64 max_calls;
/*
* Puntero OPCIONAL a información de contexto variada proporcionada por el usuario
*
* user_fctx sirve para apuntar a tus propios datos para retener información
* de contexto arbitraria entre las llamadas a tu función.
*/
void *user_fctx;
/*
* Puntero OPCIONAL a la estructura que contiene los metadatos de entrada del tipo de atributo
*
* attinmeta se utiliza al devolver tuplas (es decir, tipos de datos compuestos)
* y no se usa al devolver tipos de datos base. Solo se necesita si tienes la intención
* de usar BuildTupleFromCStrings() para crear la tupla de retorno.
*/
AttInMetadata *attinmeta;
/*
* contexto de memoria utilizado para estructuras que deben sobrevivir a múltiples llamadas
*
* SRF_FIRSTCALL_INIT() establece multi_call_memory_ctx por ti, y SRF_RETURN_DONE()
* lo utiliza para la limpieza. Es el contexto de memoria más apropiado para cualquier
* memoria que deba reutilizarse a lo largo de múltiples llamadas de la SRF.
*/
MemoryContext multi_call_memory_ctx;
/*
* Puntero OPCIONAL a la estructura que contiene la descripción de la tupla
*
* tuple_desc se utiliza al devolver tuplas (es decir, tipos de datos compuestos)
* y solo se necesita si vas a construir las tuplas con heap_form_tuple() en lugar de
* con BuildTupleFromCStrings(). Ten en cuenta que el puntero TupleDesc almacenado aquí
* normalmente debería haber pasado primero por BlessTupleDesc().
*/
TupleDesc tuple_desc;
} FuncCallContext;
Las macros que debe utilizar una SRF que use esta infraestructura son:
SRF_IS_FIRSTCALL()
Utiliza esto para determinar si tu función está siendo llamada por primera vez o una vez posterior. En la primera llamada (únicamente), llama a:
SRF_FIRSTCALL_INIT()
para inicializar el FuncCallContext. En cada llamada a la función,
incluida la primera, llama a:
SRF_PERCALL_SETUP()
para prepararte para utilizar el FuncCallContext.
Si tu función tiene datos que devolver en la llamada actual, utiliza:
SRF_RETURN_NEXT(funcctx, result)
para devolverlos a quien realiza la llamada. (result debe ser de tipo
Datum, ya sea un único valor o una tupla preparada como se describió anteriormente).
Finalmente, cuando tu función haya terminado de devolver datos, utiliza:
SRF_RETURN_DONE(funcctx)
para limpiar y finalizar la SRF.
El contexto de memoria que está activo cuando se llama a la SRF es un contexto
transitorio que se limpiará entre llamadas. Esto significa que no necesitas llamar a
pfree para cada cosa que hayas asignado utilizando palloc;
desaparecerá de todos modos. Sin embargo, si quieres asignar estructuras de datos que sobrevivan
entre llamadas, debes colocarlas en otro lugar. El contexto de memoria referenciado por
multi_call_memory_ctx es una ubicación adecuada para cualquier dato que
necesite sobrevivir hasta que la SRF termine de ejecutarse. En la mayoría de los
casos, esto significa que deberías cambiar a multi_call_memory_ctx
mientras realizas la configuración de la primera llamada.
Utiliza funcctx->user_fctx para mantener un puntero a cualquier estructura
de datos de este tipo que deba persistir entre llamadas. (Los datos que asignes en
multi_call_memory_ctx también desaparecerán automáticamente cuando termine
la consulta, por lo que tampoco es necesario liberar esos datos manualmente).
Aunque los argumentos reales de la función no cambian entre llamadas, si realizas el detoast
de los valores de los argumentos (lo cual normalmente se hace de forma transparente mediante la
macro PG_GETARG_) en el contexto transitorio,
las copias resultantes se liberarán en cada ciclo. En consecuencia, si mantienes referencias a dichos
valores en tu xxxuser_fctx, debes copiarlos a
multi_call_memory_ctx después del detoasting, o asegurarte de realizar
el detoasting de los valores únicamente en ese contexto.
Un ejemplo completo en pseudocódigo se vería de la siguiente manera:
Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
Datum result;
further declarations as needed
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* El código de configuración única va aquí: */
user code
if returning composite
build TupleDesc, and perhaps AttInMetadata
endif returning composite
user code
MemoryContextSwitchTo(oldcontext);
}
/* El código de configuración para cada llamada va aquí: */
user code
funcctx = SRF_PERCALL_SETUP();
user code
/* esta es solo una forma en la que podríamos comprobar si hemos terminado: */
if (funcctx->call_cntr < funcctx->max_calls)
{
/* Aquí queremos devolver otro elemento: */
user code
obtain result Datum
SRF_RETURN_NEXT(funcctx, result);
}
else
{
/* Aquí hemos terminado de devolver elementos, así que solo informamos de ello. */
/* (Resiste la tentación de poner código de limpieza aquí). */
SRF_RETURN_DONE(funcctx);
}
}
A complete example of a simple SRF returning a composite type looks like:
PG_FUNCTION_INFO_V1(retcomposite);
Datum
retcomposite(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
int call_cntr;
int max_calls;
TupleDesc tupdesc;
AttInMetadata *attinmeta;
/* tareas realizadas solo en la primera llamada de la función */
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
/* crea un contexto de función para persistencia entre llamadas */
funcctx = SRF_FIRSTCALL_INIT();
/* cambia al contexto de memoria adecuado para múltiples llamadas de función */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* número total de tuplas a devolver */
funcctx->max_calls = PG_GETARG_INT32(0);
/* Construye un descriptor de tupla para nuestro tipo de resultado */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));
/*
* genera los metadatos de atributo necesarios más adelante para producir tuplas
* a partir de cadenas de C sin procesar
*/
attinmeta = TupleDescGetAttInMetadata(tupdesc);
funcctx->attinmeta = attinmeta;
MemoryContextSwitchTo(oldcontext);
}
/* tareas realizadas en cada llamada de la función */
funcctx = SRF_PERCALL_SETUP();
call_cntr = funcctx->call_cntr;
max_calls = funcctx->max_calls;
attinmeta = funcctx->attinmeta;
if (call_cntr < max_calls) /* hacer cuando queda más por enviar */
{
char **values;
HeapTuple tuple;
Datum result;
/*
* Prepara un array de valores para construir la tupla devuelta.
* Este debe ser un array de cadenas de C que serán
* procesadas más tarde por las funciones de entrada de tipo.
*/
values = (char **) palloc(3 * sizeof(char *));
values[0] = (char *) palloc(16 * sizeof(char));
values[1] = (char *) palloc(16 * sizeof(char));
values[2] = (char *) palloc(16 * sizeof(char));
snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1));
snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1));
snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1));
/* construye una tupla */
tuple = BuildTupleFromCStrings(attinmeta, values);
/* convierte la tupla en un datum */
result = HeapTupleGetDatum(tuple);
/* limpia (esto no es realmente necesario) */
pfree(values[0]);
pfree(values[1]);
pfree(values[2]);
pfree(values);
SRF_RETURN_NEXT(funcctx, result);
}
else /* hacer cuando no quede más */
{
SRF_RETURN_DONE(funcctx);
}
}
Una forma de declarar esta función en SQL es:
CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);
CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
RETURNS SETOF __retcomposite
AS 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
Una forma diferente es usar parámetros OUT:
CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
OUT f1 integer, OUT f2 integer, OUT f3 integer)
RETURNS SETOF record
AS 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
Ten en cuenta que en este método el tipo de salida de la función es formalmente
un tipo record anónimo.
Se puede declarar que las funciones en lenguaje C aceptan y devuelven los tipos polimórficos
descritos en Section 36.2.5. Cuando los argumentos o los tipos de
retorno de una función se definen como tipos polimórficos, el autor de la función no puede saber
de antemano con qué tipo de datos será llamada ni cuál debe devolver. Se proporcionan dos rutinas
en fmgr.h para permitir que una función de C de versión 1 descubra los tipos
de datos reales de sus argumentos y el tipo que se espera que devuelva. Las rutinas se denominan
get_fn_expr_rettype(FmgrInfo *flinfo) y
get_fn_expr_argtype(FmgrInfo *flinfo, int argnum).
Devuelven el OID del tipo de resultado o argumento, o bien InvalidOid si la
información no está disponible. Normalmente se accede a la estructura flinfo
como fcinfo->flinfo. El parámetro argnum comienza desde cero.
get_call_result_type también se puede usar como alternativa a
get_fn_expr_rettype. También existe get_fn_expr_variadic,
que se puede utilizar para saber si los argumentos variádicos se han fusionado en un array.
Esto es útil principalmente para funciones VARIADIC "any", ya que dicha fusión
siempre habrá ocurrido para las funciones variádicas que toman tipos de array ordinarios.
Por ejemplo, supongamos que queremos escribir una función que acepte un único elemento de cualquier tipo y devuelva un array unidimensional de ese tipo:
PG_FUNCTION_INFO_V1(make_array);
Datum
make_array(PG_FUNCTION_ARGS)
{
ArrayType *result;
Oid element_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
Datum element;
bool isnull;
int16 typlen;
bool typbyval;
char typalign;
int ndims;
int dims[MAXDIM];
int lbs[MAXDIM];
if (!OidIsValid(element_type))
elog(ERROR, "could not determine data type of input");
/* obtiene el elemento proporcionado, teniendo cuidado en caso de que sea NULL */
isnull = PG_ARGISNULL(0);
if (isnull)
element = (Datum) 0;
else
element = PG_GETARG_DATUM(0);
/* tenemos una dimensión */
ndims = 1;
/* y un elemento */
dims[0] = 1;
/* y el límite inferior es 1 */
lbs[0] = 1;
/* obtiene la información necesaria sobre el tipo de elemento */
get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
/* ahora construye el array */
result = construct_md_array(&element, &isnull, ndims, dims, lbs,
element_type, typlen, typbyval, typalign);
PG_RETURN_ARRAYTYPE_P(result);
}
El siguiente comando declara la función make_array en SQL:
CREATE FUNCTION make_array(anyelement) RETURNS anyarray
AS 'DIRECTORY/funcs', 'make_array'
LANGUAGE C IMMUTABLE;
Existe una variante de polimorfismo que solo está disponible para las funciones en lenguaje C:
se pueden declarar para tomar parámetros de tipo "any". (Ten en cuenta que
este nombre de tipo debe estar entre comillas dobles, ya que también es una palabra reservada en SQL).
Esto funciona igual que anyelement, excepto que no obliga a que los diferentes
argumentos "any" sean del mismo tipo, ni ayudan a determinar el tipo de resultado
de la función. Una función en lenguaje C también puede declarar que su último parámetro sea
VARIADIC "any". Esto coincidirá con uno o más argumentos reales de cualquier tipo
(no necesariamente del mismo tipo). Estos argumentos no se agruparán en un array
como ocurre con las funciones variádicas normales; simplemente se pasarán a la función por separado.
Se debe utilizar la macro PG_NARGS() y los métodos descritos anteriormente para
determinar el número de argumentos reales y sus tipos al utilizar esta característica. Además,
los usuarios de dicha función podrían desear utilizar la palabra clave VARIADIC
en su llamada a la función, con la expectativa de que la función trate los elementos del array como
argumentos separados. La propia función debe implementar ese comportamiento si así se desea, después
de usar get_fn_expr_variadic para detectar que el argumento real estaba marcado
con VARIADIC.
Los complementos (add-ins) pueden reservar memoria compartida al iniciar el servidor. Para hacerlo,
la biblioteca compartida del complemento debe cargarse previamente especificándola en
shared_preload_libraries.
La biblioteca compartida también debe registrar un shmem_request_hook en su
función _PG_init. Este shmem_request_hook puede reservar
memoria compartida llamando a:
void RequestAddinShmemSpace(Size size)
Cada backend debe obtener un puntero a la memoria compartida reservada llamando a:
void *ShmemInitStruct(const char *name, Size size, bool *foundPtr)
Si esta función establece foundPtr en false, el invocador
debe proceder a inicializar el contenido de la memoria compartida reservada. Si
foundPtr se establece en true, la memoria compartida ya fue
inicializada por otro backend, y el invocador no necesita inicializarla más.
Para evitar condiciones de carrera, cada backend debe utilizar el LWLock
AddinShmemInitLock al inicializar su asignación de memoria compartida,
como se muestra aquí:
static mystruct *ptr = NULL;
bool found;
LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
... inicializar el contenido de la memoria compartida ...
ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);
shmem_startup_hook proporciona un lugar conveniente para el código de
inicialización, pero no es estrictamente obligatorio que todo ese código se coloque en este hook.
En Windows (y en cualquier otro lugar donde se defina EXEC_BACKEND), cada backend
ejecuta el shmem_startup_hook registrado poco después de acoplarse a la memoria
compartida, por lo que los complementos aún deben adquirir AddinShmemInitLock
dentro de este hook, como se muestra en el ejemplo anterior. En otras plataformas, solo el proceso
postmaster ejecuta el shmem_startup_hook, y cada backend hereda automáticamente
los punteros a la memoria compartida.
Puedes encontrar un ejemplo de shmem_request_hook y
shmem_startup_hook en
contrib/pg_stat_statements/pg_stat_statements.c en el árbol de fuentes de
PostgreSQL.
Existe otro método más flexible para reservar memoria compartida que se puede realizar después
del inicio del servidor y fuera de un shmem_request_hook. Para hacerlo, cada
backend que vaya a utilizar la memoria compartida debe obtener un puntero a ella llamando a:
void *GetNamedDSMSegment(const char *name, size_t size,
void (*init_callback) (void *ptr),
bool *found)
Si aún no existe un segmento de memoria compartida dinámica con el nombre indicado, esta función
lo asignará y lo inicializará con la función de devolución de llamada (callback)
init_callback proporcionada. Si el segmento ya ha sido asignado e inicializado
por otro backend, esta función simplemente acopla el segmento de memoria compartida dinámica
existente al backend actual.
A diferencia de la memoria compartida reservada al iniciar el servidor, no es necesario adquirir
AddinShmemInitLock ni tomar otras medidas para evitar condiciones de carrera
al reservar memoria compartida con GetNamedDSMSegment. Esta función garantiza
que solo un backend asigne e inicialice el segmento y que todos los demás backends reciban un puntero
al segmento completamente asignado e inicializado.
Puedes encontrar un ejemplo de uso completo de GetNamedDSMSegment en
src/test/modules/test_dsm_registry/test_dsm_registry.c
en el árbol de fuentes de PostgreSQL.
Los complementos pueden reservar LWLocks al iniciar el servidor. Al igual que con la memoria
compartida reservada al iniciar el servidor, la biblioteca compartida del complemento debe cargarse
previamente especificándola en
shared_preload_libraries,
y la biblioteca compartida debe registrar un shmem_request_hook en su
función _PG_init. Este shmem_request_hook puede reservar
LWLocks llamando a:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
Esto garantiza que un array de num_lwlocks LWLocks esté disponible bajo el nombre
tranche_name. Se puede obtener un puntero a este array llamando a:
LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
Existe otro método más flexible para obtener LWLocks que se puede realizar después del inicio del
servidor y fuera de un shmem_request_hook. Para hacerlo, primero asigna un
tranche_id llamando a:
int LWLockNewTrancheId(void)
A continuación, inicializa cada LWLock, pasando el nuevo tranche_id como argumento:
void LWLockInitialize(LWLock *lock, int tranche_id)
De manera similar a la memoria compartida, cada backend debe garantizar que solo un proceso asigne
un nuevo tranche_id e inicialice cada nuevo LWLock. Una forma de hacerlo es llamar
a estas funciones únicamente en tu código de inicialización de memoria compartida con el
AddinShmemInitLock adquirido en modo exclusivo. Si utilizas
GetNamedDSMSegment, llamar a estas funciones en la función de devolución de
llamada init_callback es suficiente para evitar condiciones de carrera.
Finalmente, cada backend que utilice el tranche_id debe asociarlo con un
tranche_name llamando a:
void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
Puedes encontrar un ejemplo de uso completo de LWLockNewTrancheId,
LWLockInitialize y LWLockRegisterTranche en
contrib/pg_prewarm/autoprewarm.c en el árbol de fuentes de
PostgreSQL.
Los complementos pueden definir eventos de espera personalizados bajo el tipo de evento de espera
Extension llamando a:
uint32 WaitEventExtensionNew(const char *wait_event_name)
El evento de espera se asocia a una cadena personalizada visible para el usuario.
Puedes encontrar un ejemplo en src/test/modules/worker_spi en el árbol de
fuentes de PostgreSQL.
Los eventos de espera personalizados se pueden visualizar en
pg_stat_activity:
=# SELECT wait_event_type, wait_event FROM pg_stat_activity
WHERE backend_type ~ 'worker_spi';
wait_event_type | wait_event
-----------------+---------------
Extension | WorkerSpiMain
(1 row)
Un punto de inyección con un name (nombre) dado se declara utilizando la macro:
INJECTION_POINT(name, arg);
Ya existen algunos puntos de inyección declarados en puntos estratégicos dentro del código del
servidor. Después de añadir un nuevo punto de inyección, es necesario compilar el código para que ese
punto de inyección esté disponible en el binario. Los complementos escritos en lenguaje C pueden
declarar puntos de inyección en su propio código utilizando la misma macro. Los nombres de los puntos
de inyección deben utilizar caracteres en minúscula, con los términos separados por guiones.
arg es un valor de argumento opcional que se proporciona a la devolución de
llamada (callback) en tiempo de ejecución.
La ejecución de un punto de inyección puede requerir la asignación de una pequeña cantidad de memoria, lo cual puede fallar. Si necesitas tener un punto de inyección en una sección crítica donde no se permiten asignaciones dinámicas, puedes utilizar un enfoque de dos pasos con las siguientes macros:
INJECTION_POINT_LOAD(name); INJECTION_POINT_CACHED(name, arg);
Antes de entrar en la sección crítica, llama a INJECTION_POINT_LOAD. Esta macro
comprueba el estado de la memoria compartida y carga la devolución de llamada en la memoria privada
del backend si está activa. Dentro de la sección crítica, utiliza
INJECTION_POINT_CACHED para ejecutar la devolución de llamada.
Los complementos (add-ins) pueden acoplar devoluciones de llamada (callbacks) a un punto de inyección ya declarado llamando a:
extern void InjectionPointAttach(const char *name,
const char *library,
const char *function,
const void *private_data,
int private_data_size);
name es el nombre del punto de inyección, el cual cuando
sea alcanzado durante la ejecución ejecutará la function
cargada desde library. private_data
es un área privada de datos de tamaño private_data_size
que se pasa como argumento al callback cuando se ejecuta.
Aquí hay un ejemplo de devolución de llamada para
InjectionPointCallback:
static void
custom_injection_callback(const char *name,
const void *private_data,
void *arg)
{
uint32 wait_event_info = WaitEventInjectionPointNew(name);
pgstat_report_wait_start(wait_event_info);
elog(NOTICE, "%s: executed custom callback", name);
pgstat_report_wait_end();
}
Esta función de devolución de llamada imprime un mensaje en el registro de errores del servidor
con severidad NOTICE, pero las devoluciones de llamada pueden implementar una
lógica más compleja.
Una forma alternativa de definir la acción a tomar cuando se alcanza un punto de inyección
es añadir el código de prueba junto con el código fuente normal. Esto puede ser útil si la
acción, por ejemplo, depende de variables locales que no son accesibles para los módulos
cargados. La macro IS_INJECTION_POINT_ATTACHED se puede utilizar para
comprobar si un punto de inyección está acoplado, por ejemplo:
#ifdef USE_INJECTION_POINTS
if (IS_INJECTION_POINT_ATTACHED("before-foobar"))
{
/* cambia una variable local si el punto de inyección está acoplado */
local_var = 123;
/* ejecuta también la devolución de llamada */
INJECTION_POINT_CACHED("before-foobar", NULL);
}
#endif
Ten en cuenta que la devolución de llamada acoplada al punto de inyección no será
ejecutada por la macro IS_INJECTION_POINT_ATTACHED. Si deseas ejecutar
la devolución de llamada, también debes llamar a INJECTION_POINT_CACHED
como en el ejemplo anterior.
Opcionalmente, es posible desacoplar un punto de inyección llamando a:
extern bool InjectionPointDetach(const char *name);
En caso de éxito, se devuelve true, de lo contrario
se devuelve false.
Una devolución de llamada acoplada a un punto de inyección está disponible en todos los
backends, incluidos los backends iniciados después de llamar a
InjectionPointAttach. Permanece acoplada mientras el servidor está en
ejecución o hasta que el punto de inyección se desacopla utilizando
InjectionPointDetach.
Puedes encontrar un ejemplo en src/test/modules/injection_points en
el árbol de fuentes de PostgreSQL.
Habilitar los puntos de inyección requiere --enable-injection-points con
configure o bien -Dinjection_points=true con
Meson.
Es posible que los complementos escritos en lenguaje C utilicen tipos personalizados de estadísticas acumulativas registrados en el sistema de estadísticas acumulativas.
Primero, define una estructura PgStat_KindInfo que incluya toda
la información relacionada con el tipo personalizado registrado. Por ejemplo:
static const PgStat_KindInfo custom_stats = {
.name = "custom_stats",
.fixed_amount = false,
.shared_size = sizeof(PgStatShared_Custom),
.shared_data_off = offsetof(PgStatShared_Custom, stats),
.shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats),
.pending_size = sizeof(PgStat_StatCustomEntry),
}
Luego, cada backend que necesite utilizar este tipo personalizado debe registrarlo
con pgstat_register_kind y un ID único utilizado para almacenar las
entradas relacionadas con este tipo de estadísticas:
extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind,
const PgStat_KindInfo *kind_info);
Mientras desarrollas una nueva extensión, utiliza PGSTAT_KIND_EXPERIMENTAL
para el parámetro kind. Cuando estés listo para lanzar la extensión
a los usuarios, reserva un ID de tipo en la página de
Custom Cumulative Statistics.
Los detalles de la API para PgStat_KindInfo se pueden encontrar en el
archivo src/include/utils/pgstat_internal.h.
El tipo de estadísticas registrado se asocia con un nombre y un ID único compartido a través
del servidor en la memoria compartida. Cada backend que utiliza un tipo personalizado de
estadísticas mantiene una caché local que almacena la información de cada
PgStat_KindInfo personalizada.
Coloca el módulo de extensión que implementa el tipo de estadísticas acumulativas personalizadas en shared_preload_libraries para que se cargue al inicio de la ejecución de PostgreSQL.
Puedes encontrar un ejemplo que describe cómo registrar y utilizar estadísticas personalizadas en
src/test/modules/injection_points.
Aunque el motor de PostgreSQL está escrito en C, es posible escribir extensiones en C++ si se siguen estas directrices:
Todas las funciones a las que acceda el motor deben presentar una interfaz C al motor;
estas funciones C pueden llamar luego a funciones C++. Por ejemplo, se requiere
el enlace extern C para las funciones a las que accede el motor.
Esto también es necesario para cualquier función que se pase como puntero entre el motor
y el código C++.
Libera la memoria utilizando el método de liberación adecuado. Por ejemplo, la mayor
parte de la memoria del motor se asigna mediante palloc(), por lo que
debes utilizar pfree() para liberarla. El uso de delete
de C++ en tales casos fallará.
Evita que las excepciones se propaguen al código C (utiliza un bloque catch-all en el nivel
superior de todas las funciones extern C). Esto es necesario incluso si el
código C++ no lanza explícitamente ninguna excepción, porque eventos como quedarse sin memoria
aún pueden lanzar excepciones. Cualquier excepción debe ser capturada y los errores adecuados
deben devolverse a la interfaz C. Si es posible, compila C++ con -fno-exceptions
para eliminar las excepciones por completo; en tales casos, debes comprobar los fallos en tu
código C++, por ejemplo, comprobando si new() devuelve NULL.
Si llamas a funciones del motor desde código C++, asegúrate de que la pila de llamadas de C++
contenga únicamente estructuras de datos básicas planas (POD, por sus siglas
en inglés). Esto es necesario porque los errores del motor generan un longjmp()
distante que no desenrolla adecuadamente una pila de llamadas de C++ con objetos que no sean POD.
En resumen, lo mejor es colocar el código C++ detrás de un muro de funciones extern C
que sirvan de interfaz con el motor, y evitar fugas de excepciones, memoria y pila de llamadas.