diff --git a/.docs/extensions.es.md b/.docs/extensions.es.md index 744a8ad..3ee6552 100644 --- a/.docs/extensions.es.md +++ b/.docs/extensions.es.md @@ -4,104 +4,97 @@ Cada extensión tiene dos partes: -1. **Cómo se instala**, en el [`Dockerfile`](../Dockerfile). -2. **Cómo se prueba**, en `extensions//test.sql`. +1. **Cómo se instala**: una línea en [`extensions/pgdg.txt`](../extensions/pgdg.txt) o unas pocas líneas en el [`Dockerfile`](../Dockerfile). +2. **Cómo se prueba**: `extensions//test.sql`. -No hay más código que tocar. +No hay más código que tocar. La lista completa de lo que se incluye está en [Extensiones incluidas](./included-extensions.es.md). ## Añadir una extensión -### Opción A: está empaquetada en PGDG (preferida) +Elige la primera opción que funcione, en este orden. -Busca `postgresql--` en [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/). Tiene que existir para todas las versiones soportadas. +### Opción A: está empaquetada en PGDG (preferida) -Declara su versión arriba del todo en el Dockerfile. El comentario `# renovate:` le indica a Renovate el índice de PGDG, para que abra un PR cuando se empaquete una versión nueva: +Busca `postgresql--` en [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/) y mira qué versiones tiene: -```dockerfile -# renovate: datasource=deb depName=postgresql-18-mi-extension registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 -ARG MI_EXTENSION_VERSION=1.2.3 +```sh +docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-' ``` -`suite` es la versión de Debian de la imagen base (hoy `trixie`) seguida de `-pgdg`. Puedes comprobarla con `docker run --rm postgres:18 cat /etc/os-release`. - -Después, añádela fijada al `apt-get install` de la etapa final, y a los labels: +Si existe la misma versión upstream para todas las versiones mayores soportadas, añade una línea a `extensions/pgdg.txt`: -```dockerfile -ARG MI_EXTENSION_VERSION -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ - "postgresql-${PG_MAJOR}-mi-extension=${MI_EXTENSION_VERSION}*" \ - && rm -rf /var/lib/apt/lists/* -LABEL ... \ - io.github.ruxwez.postgres.mi-extension.version="${MI_EXTENSION_VERSION}" +``` +mi-paquete 1.2.3 ``` -El `*` final cubre la revisión de Debian (`1.2.3-1.pgdg13+1`). Usa `+*` cuando la versión del paquete lleve `+dfsg` o algo parecido, como pasa con PostGIS. Puedes ver las versiones disponibles con: +Si la versión cambia según la versión mayor, o el paquete no existe para alguna, añade una línea por versión mayor: -```sh -docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-mi-extension' +``` +mi-paquete/17 2.0.1 +mi-paquete/18 2.1.0 ``` -PGDG suele mantener varias versiones anteriores en su índice, así que la versión fijada sigue funcionando un tiempo después de que salga una nueva. Cuando una versión desaparece del índice, el build falla con `Version '…' was not found` hasta que se hace merge del PR de Renovate. - -La imagen oficial base define `PG_MAJOR`. +La versión es la upstream, sin la revisión de Debian: `1.2.3` cubre `1.2.3-1.pgdg13+1`. Renovate actualiza estas líneas y las agrupa en un PR semanal. -### Opción B: compilarla desde el código fuente +### Opción B: los autores publican paquetes precompilados -Declara su versión arriba del todo en el Dockerfile, junto a las demás: +Si los autores adjuntan a sus releases un `.deb` (o un `.zip` con uno) por cada versión mayor de PostgreSQL y arquitectura, añádelo a la etapa `prebuilt`. `fetch-deb` lo descarga y se queda solo con sus archivos de PostgreSQL: ```dockerfile -# renovate: datasource=github-tags depName=owner/mi-extension +# arriba, con las demás versiones +# renovate: datasource=github-releases depName=owner/mi-extension ARG MI_EXTENSION_VERSION=1.2.3 -``` - -El comentario `# renovate:` justo encima del `ARG` permite que Renovate abra un PR cada vez que salga un tag nuevo. Mantén el mismo formato. -Añade una etapa encima de `build-base`. Tiene que instalar en `/out` (con `DESTDIR`): - -```dockerfile -FROM build-base AS mi-extension +# en la etapa prebuilt ARG MI_EXTENSION_VERSION -RUN git clone --depth 1 --branch "v${MI_EXTENSION_VERSION}" https://github.com/owner/mi-extension.git /src \ - && make -C /src \ - && make -C /src install DESTDIR=/out +RUN fetch-deb "https://github.com/owner/mi-extension/releases/download/v${MI_EXTENSION_VERSION}/mi-extension-pg${PG_MAJOR}-${TARGETARCH}.deb" ``` -En la etapa final, cópiala y añade su versión a los labels: +### Opción C: compilarla desde el código fuente + +Añade una línea a la etapa `source`. `pgxs-build` descarga un tag (o un commit) y hace `make install` en `/out`: ```dockerfile +# arriba, con las demás versiones +# renovate: datasource=github-tags depName=owner/mi-extension +ARG MI_EXTENSION_VERSION=1.2.3 + +# en la etapa source ARG MI_EXTENSION_VERSION -COPY --from=mi-extension /out/ / -LABEL ... \ - io.github.ruxwez.postgres.mi-extension.version="${MI_EXTENSION_VERSION}" +RUN pgxs-build https://github.com/owner/mi-extension.git "v${MI_EXTENSION_VERSION}" ``` -Si el build necesita más paquetes, añádelos a `build-base`. No llegan a la imagen final. +Argumentos opcionales: un subdirectorio y después argumentos extra de `make` (`pgxs-build URL TAG subdir USE_PGXS=1`). Si la extensión no soporta las versiones mayores antiguas, pon delante `MIN_PG_MAJOR=16` para saltárselas. Si el build necesita paquetes `-dev`, añádelos al `apt-get install` de la etapa `source`; no llegan a la imagen final. Las librerías de runtime van en el `apt-get install` de la etapa final. -### Después, en ambos casos +El comentario `# renovate:` justo encima del `ARG` permite que Renovate abra un PR cada vez que salga una versión nueva. Mantén el mismo formato. -Crea `extensions//test.sql`. Tiene que **usar** la extensión, no solo crearla, y lanzar un error si el resultado es incorrecto: +## Escribir el test + +Crea `extensions//test.sql`. Tiene que **usar** la extensión, no solo crearla, y fallar si el resultado es incorrecto. `assert_true(condición, mensaje)` está disponible en todas las bases de datos de test: ```sql CREATE EXTENSION mi_extension; -DO $$ -BEGIN - IF mi_funcion(1) <> 2 THEN - RAISE EXCEPTION 'mi_extension: resultado inesperado'; - END IF; -END $$; +SELECT assert_true(mi_funcion(1) = 2, 'mi_extension: mi_funcion()'); ``` -`tests/run.sh` lo detecta automáticamente. Por último, añade la extensión a la tabla del README. +`tests/run.sh` lo detecta automáticamente. Cada test se ejecuta en una base de datos nueva, todos en el mismo servidor. Estos archivos opcionales en el mismo directorio cambian ese comportamiento: + +| Archivo | Contenido | Efecto | +|---|---|---| +| `server.conf` | Líneas de `postgresql.conf`, p. ej. `shared_preload_libraries = 'mi_extension'` | Se aplican al servidor de test. Los valores de `shared_preload_libraries` de todas las extensiones se combinan. | +| `pg-versions` | Versiones mayores soportadas, p. ej. `17 18` | En las demás versiones el test sale como `SKIP` en vez de fallar. | +| `database` | Un nombre de base de datos, p. ej. `postgres` | El test se ejecuta ahí en vez de en una base de datos nueva (lo necesitan pg_cron y las extensiones que dependen de ella). | +| `isolated` | Por qué la extensión no puede compartir servidor | El test se ejecuta en un servidor propio, solo con su `server.conf`. | + +Por último, añade la extensión a [Extensiones incluidas](./included-extensions.es.md) y a la tabla de categorías del README. ## Actualizar una extensión -Cambia su `ARG *_VERSION` arriba del todo en el Dockerfile. Renovate abre estos PRs por ti, tanto para los paquetes apt como para las extensiones que se compilan. +Cambia su versión en `extensions/pgdg.txt` o en su `ARG` arriba del todo en el Dockerfile. Renovate abre estos PRs por ti. Una vez hecho el merge, el cambio solo llega a `18` y `latest`. Para llevarlo a `18-v1`, saca una release (en [Releases](./releases.es.md) se explica qué número subir). ## Añadir o quitar una versión de PostgreSQL -Edita la lista `major` en [`docker-bake.hcl`](../docker-bake.hcl). Para mover `:latest` a una versión nueva, cambia también `LATEST`. El CI construye y prueba todas las versiones de esa lista, así que si alguna extensión no está disponible para una versión, el build falla. +Edita la lista `major` en [`docker-bake.hcl`](../docker-bake.hcl). Para mover `:latest` a una versión nueva, cambia también `LATEST`. El CI construye y prueba todas las versiones de esa lista. Si alguna extensión no está disponible para una versión nueva, ponle un archivo `pg-versions` (con las líneas por versión mayor o el `MIN_PG_MAJOR` correspondientes), o espera a que lo esté. diff --git a/.docs/extensions.md b/.docs/extensions.md index d5fea3e..b5872fd 100644 --- a/.docs/extensions.md +++ b/.docs/extensions.md @@ -4,104 +4,97 @@ Every extension has two parts: -1. **How it's installed**, in the [`Dockerfile`](../Dockerfile). -2. **How it's tested**, in `extensions//test.sql`. +1. **How it's installed**: a line in [`extensions/pgdg.txt`](../extensions/pgdg.txt), or a few lines in the [`Dockerfile`](../Dockerfile). +2. **How it's tested**: `extensions//test.sql`. -There is no other code to touch. +There is no other code to touch. The full list of what's included is in [Included extensions](./included-extensions.md). ## Adding an extension -### Option A: it's packaged in PGDG (preferred) +Pick the first option that works, in this order. -Look for `postgresql--` on [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/). It has to exist for every supported version. +### Option A: it's packaged in PGDG (preferred) -Declare its version at the top of the Dockerfile. The `# renovate:` comment points Renovate at the PGDG index, so it opens a PR when a new version is packaged: +Look for `postgresql--` on [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/), and list the versions it has: -```dockerfile -# renovate: datasource=deb depName=postgresql-18-my-extension registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 -ARG MY_EXTENSION_VERSION=1.2.3 +```sh +docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-' ``` -`suite` is the Debian release of the base image (`trixie` today) followed by `-pgdg`. Check it with `docker run --rm postgres:18 cat /etc/os-release`. - -Then add it to the `apt-get install` in the final stage, pinned, and to the labels: +If the same upstream version exists for every supported major, add one line to `extensions/pgdg.txt`: -```dockerfile -ARG MY_EXTENSION_VERSION -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ - "postgresql-${PG_MAJOR}-my-extension=${MY_EXTENSION_VERSION}*" \ - && rm -rf /var/lib/apt/lists/* -LABEL ... \ - io.github.ruxwez.postgres.my-extension.version="${MY_EXTENSION_VERSION}" +``` +my-package 1.2.3 ``` -The trailing `*` matches the Debian revision (`1.2.3-1.pgdg13+1`). Use `+*` when the package version contains `+dfsg` or similar, as PostGIS does. You can list the available versions with: +If the version differs per major, or the package doesn't exist for some of them, add one line per major instead: -```sh -docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-my-extension' +``` +my-package/17 2.0.1 +my-package/18 2.1.0 ``` -PGDG usually keeps a few previous versions in its index, so the pin keeps working for a while after a new release. Once a version is dropped from the index, the build fails with `Version '…' was not found` until the Renovate PR is merged. - -`PG_MAJOR` is set by the official base image. +The version is the upstream one, without the Debian revision: `1.2.3` matches `1.2.3-1.pgdg13+1`. Renovate updates these lines and groups them into one weekly PR. -### Option B: build it from source +### Option B: the authors publish prebuilt packages -Declare its version at the top of the Dockerfile, next to the others: +If the authors attach a `.deb` (or a `.zip` with one) per PostgreSQL major and architecture to their releases, add it to the `prebuilt` stage. `fetch-deb` downloads it and keeps only its PostgreSQL files: ```dockerfile -# renovate: datasource=github-tags depName=owner/my-extension +# at the top, with the other versions +# renovate: datasource=github-releases depName=owner/my-extension ARG MY_EXTENSION_VERSION=1.2.3 -``` - -The `# renovate:` comment right above the `ARG` lets Renovate open a PR whenever a new tag is released. Keep the same format. -Add a stage on top of `build-base`. It must install into `/out` (via `DESTDIR`): - -```dockerfile -FROM build-base AS my-extension +# in the prebuilt stage ARG MY_EXTENSION_VERSION -RUN git clone --depth 1 --branch "v${MY_EXTENSION_VERSION}" https://github.com/owner/my-extension.git /src \ - && make -C /src \ - && make -C /src install DESTDIR=/out +RUN fetch-deb "https://github.com/owner/my-extension/releases/download/v${MY_EXTENSION_VERSION}/my-extension-pg${PG_MAJOR}-${TARGETARCH}.deb" ``` -In the final stage, copy it over and add its version to the labels: +### Option C: build it from source + +Add a line to the `source` stage. `pgxs-build` fetches a tag (or a commit) and runs `make install` into `/out`: ```dockerfile +# at the top, with the other versions +# renovate: datasource=github-tags depName=owner/my-extension +ARG MY_EXTENSION_VERSION=1.2.3 + +# in the source stage ARG MY_EXTENSION_VERSION -COPY --from=my-extension /out/ / -LABEL ... \ - io.github.ruxwez.postgres.my-extension.version="${MY_EXTENSION_VERSION}" +RUN pgxs-build https://github.com/owner/my-extension.git "v${MY_EXTENSION_VERSION}" ``` -If the build needs extra packages, add them to `build-base`. They won't reach the final image. +Optional arguments: a subdirectory, then extra `make` arguments (`pgxs-build URL TAG subdir USE_PGXS=1`). Prefix with `MIN_PG_MAJOR=16` to skip older majors that the extension doesn't support. If the build needs `-dev` packages, add them to the `apt-get install` of the `source` stage; they don't reach the final image. Runtime libraries go in the final stage's `apt-get install`. -### Then, in both cases +The `# renovate:` comment right above the `ARG` lets Renovate open a PR whenever a new version is released. Keep the same format. -Create `extensions//test.sql`. It should **use** the extension, not only create it, and raise an error if the result is wrong: +## Writing the test + +Create `extensions//test.sql`. It should **use** the extension, not only create it, and fail if the result is wrong. `assert_true(condition, message)` is available in every test database: ```sql CREATE EXTENSION my_extension; -DO $$ -BEGIN - IF my_function(1) <> 2 THEN - RAISE EXCEPTION 'my_extension: unexpected result'; - END IF; -END $$; +SELECT assert_true(my_function(1) = 2, 'my_extension: my_function()'); ``` -`tests/run.sh` picks it up automatically. Finally, add the extension to the table in the README. +`tests/run.sh` picks it up automatically. Each test runs in a fresh database, all of them on one server. These optional files in the same directory change that: + +| File | Content | Effect | +|---|---|---| +| `server.conf` | `postgresql.conf` lines, e.g. `shared_preload_libraries = 'my_extension'` | Applied to the test server. `shared_preload_libraries` values from every extension are merged. | +| `pg-versions` | Supported majors, e.g. `17 18` | On other majors the test is reported as `SKIP` instead of failing. | +| `database` | A database name, e.g. `postgres` | The test runs there instead of in a fresh database (pg_cron and the extensions that depend on it need this). | +| `isolated` | Why the extension can't share a server | The test runs on a server of its own, with only its own `server.conf`. | + +Finally, add the extension to [Included extensions](./included-extensions.md) and to the category table in the README. ## Upgrading an extension -Change its `ARG *_VERSION` at the top of the Dockerfile. Renovate opens these PRs for you, for both apt packages and extensions built from source. +Change its version in `extensions/pgdg.txt` or in its `ARG` at the top of the Dockerfile. Renovate opens these PRs for you. Once merged, the change only reaches `18` and `latest`. To ship it to `18-v1`, cut a release (see [Releases](./releases.md) to pick the version bump). ## Adding or dropping a PostgreSQL version -Edit the `major` list in [`docker-bake.hcl`](../docker-bake.hcl). To move `:latest` to a new major, change `LATEST` too. CI builds and tests every version in that list, so a version where some extension isn't available fails the build. +Edit the `major` list in [`docker-bake.hcl`](../docker-bake.hcl). To move `:latest` to a new major, change `LATEST` too. CI builds and tests every version in that list. If an extension isn't available for a new major, either give it a `pg-versions` file (and the matching per-major lines or `MIN_PG_MAJOR`), or wait until it is. diff --git a/.docs/included-extensions.es.md b/.docs/included-extensions.es.md new file mode 100644 index 0000000..7fd3a0e --- /dev/null +++ b/.docs/included-extensions.es.md @@ -0,0 +1,197 @@ +# Extensiones incluidas + +> [Read in English](./included-extensions.md) + +Todas las imágenes incluyen las extensiones de abajo, más todo [contrib](https://www.postgresql.org/docs/current/contrib.html) (`pg_stat_statements`, `pg_trgm`, `pgcrypto`, `hstore`, …), que viene con PostgreSQL. + +La versión exacta de cada una, para cada versión mayor de PostgreSQL, está en las [release notes](https://github.com/ruxwez/postgres/releases). Dentro de una imagen: + +```sh +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt +``` + +## Cómo usarlas + +No hay nada activado por defecto, así que la imagen se comporta exactamente igual que la oficial. Activa lo que necesites con `CREATE EXTENSION ` en cada base de datos. + +Las marcadas como **preload** también hay que añadirlas a `shared_preload_libraries`, lo que requiere reiniciar: + +```yaml +services: + db: + image: ruxwez/postgres:18-v1 + command: postgres -c shared_preload_libraries=pg_cron,timescaledb -c cron.database_name=postgres +``` + +Algunas advertencias: + +- **pg_show_plans y timescaledb no se pueden precargar a la vez.** Insertar en una hypertable tumba el servidor (segfault). Precarga una u otra. +- **anon y pg_duckdb no se pueden precargar a la vez.** Los hooks de anon abortan el servidor cuando DuckDB ejecuta una consulta con varios hilos. +- **Muchas extensiones precargadas arrancan background workers.** Si precargas varias, sube `max_worker_processes` (por defecto es 8). +- **wal2json** necesita `wal_level = logical`. Las versiones menores recientes exigen además `output_plugin_libraries = 'wal2json'`. +- **pg_cron** y las extensiones que dependen de ella (`pg_incremental`, `documentdb`) solo funcionan en la base de datos indicada en `cron.database_name` (`postgres` por defecto). + +## Por categoría + +La columna **Origen** indica cómo llega cada extensión a la imagen: + +- **PGDG**: paquete de [apt.postgresql.org](https://apt.postgresql.org). +- **Release**: paquete precompilado de los autores. +- **Imagen**: copiada de la imagen oficial de los autores. +- **Fuente**: compilada desde el código fuente. + +### IA y vectores + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `vector` | [pgvector](https://github.com/pgvector/pgvector): búsqueda por similitud de vectores | PGDG | | +| `vectorscale` | [pgvectorscale](https://github.com/timescale/pgvectorscale): índice DiskANN para pgvector | Release | | + +### Geoespacial + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `postgis` (+ `postgis_raster`, `postgis_topology`, `postgis_sfcgal`, `postgis_tiger_geocoder`) | [PostGIS](https://postgis.net/): tipos geometry y geography | PGDG | | +| `pgrouting` | [pgRouting](https://pgrouting.org/): rutas y análisis de redes sobre PostGIS | PGDG | | +| `h3`, `h3_postgis` | [h3-pg](https://github.com/postgis/h3-pg): rejilla hexagonal H3 de Uber | PGDG | | +| `pg_sphere` | [pgSphere](https://github.com/postgrespro/pgsphere): geometría esférica | PGDG | | + +### Búsqueda y texto + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pg_textsearch` | [pg_textsearch](https://github.com/timescale/pg_textsearch): búsqueda de texto completo con ranking BM25 | PGDG | preload; PG 17+ | +| `rum` | [RUM](https://github.com/postgrespro/rum): índice tipo GIN con ranking | PGDG | | +| `pg_bigm` | [pg_bigm](https://github.com/pgbigm/pg_bigm): búsqueda por bigramas, buena para CJK | Fuente | | +| `pg_similarity` | [pg_similarity](https://github.com/eulerto/pg_similarity): similitud de cadenas (Levenshtein, Jaro, coseno…) | PGDG | | +| `pgpcre` | [pgpcre](https://github.com/petere/pgpcre): expresiones regulares compatibles con Perl | PGDG | | +| `re2` | [pg_re2](https://github.com/ClickHouse/pg_re2): expresiones regulares RE2 | Imagen | | +| `icu_ext` | [icu_ext](https://github.com/dverite/icu_ext): funciones de ICU (transliteración, números en letra…) | PGDG | | + +### Series temporales y analítica + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `timescaledb` | [TimescaleDB](https://github.com/timescale/timescaledb): hypertables y funciones de series temporales (edición Apache-2.0) | PGDG | preload | +| `pg_duckdb` | [pg_duckdb](https://github.com/duckdb/pg_duckdb): motor analítico DuckDB dentro de PostgreSQL | Imagen | preload; no junto con anon | +| `pg_ivm` | [pg_ivm](https://github.com/sraoss/pg_ivm): vistas materializadas que se actualizan de forma incremental | PGDG | | +| `pg_incremental` | [pg_incremental](https://github.com/CrunchyData/pg_incremental): pipelines de procesamiento incremental | Fuente | necesita pg_cron; PG 15+ | +| `hll` | [postgresql-hll](https://github.com/citusdata/postgresql-hll): conteo de distintos con HyperLogLog | PGDG | | +| `tdigest` | [tdigest](https://github.com/tvondra/tdigest): estimación de percentiles | PGDG | | +| `roaringbitmap` | [pg_roaringbitmap](https://github.com/ChenHuajun/pg_roaringbitmap): bitmaps comprimidos | PGDG | | +| `first_last_agg` | [first_last_agg](https://github.com/wulczer/first_last_agg): agregados `first()` y `last()` | PGDG | | +| `extra_window_functions` | [extra_window_functions](https://github.com/xocolatl/extra_window_functions): más funciones de ventana | PGDG | | + +### Colas, tareas y trabajo en segundo plano + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pgmq` | [PGMQ](https://github.com/pgmq/pgmq): colas de mensajes | Fuente | | +| `pg_cron` | [pg_cron](https://github.com/citusdata/pg_cron): planificador de tareas estilo cron | PGDG | preload | +| `pg_background` | [pg_background](https://github.com/vibhorkum/pg_background): ejecutar SQL en background workers | PGDG | | +| `pg_partman` | [pg_partman](https://github.com/pgpartman/pg_partman): gestión automática de particiones | PGDG | preload para su background worker (opcional) | + +### JSON, documentos y APIs + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `documentdb` | [DocumentDB](https://github.com/documentdb/documentdb): motor de documentos compatible con MongoDB | PGDG | preload; necesita pg_cron; PG 15+ | +| `pg_graphql` | [pg_graphql](https://github.com/supabase/pg_graphql): API GraphQL sobre tus tablas | Release | | +| `pg_jsonschema` | [pg_jsonschema](https://github.com/supabase/pg_jsonschema): validación con JSON Schema | Release | | +| `jsquery` | [jsquery](https://github.com/postgrespro/jsquery): lenguaje de consulta para JSON | PGDG | | +| `http` | [pgsql-http](https://github.com/pramsey/pgsql-http): cliente HTTP síncrono | PGDG | | +| `pg_net` | [pg_net](https://github.com/supabase/pg_net): peticiones HTTP asíncronas | Fuente | preload | +| `pgjwt` | [pgjwt](https://github.com/michelp/pgjwt): JSON Web Tokens | Fuente | | +| `postgres_protobuf` | [postgres-protobuf](https://github.com/mpartel/postgres-protobuf): consultar Protocol Buffers | Fuente | | +| `pg_csv` | [pg_csv](https://github.com/PostgREST/pg_csv): salida en CSV | PGDG | | + +### Tipos de datos + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `age` | [Apache AGE](https://github.com/apache/age): base de datos de grafos con openCypher | PGDG | | +| `pg_uuidv7` | [pg_uuidv7](https://github.com/fboulnois/pg_uuidv7): UUIDv7 ordenados por tiempo | PGDG | | +| `pg_hashids` | [pg_hashids](https://github.com/iCyberon/pg_hashids): IDs cortos a partir de enteros | Fuente | | +| `ip4r` | [ip4r](https://github.com/RhodiumToad/ip4r): rangos IPv4/IPv6 | PGDG | | +| `prefix` | [prefix](https://github.com/dimitri/prefix): rangos de prefijos (enrutado telefónico…) | PGDG | | +| `semver` | [semver](https://github.com/theory/pg-semver): versiones semánticas | PGDG | | +| `pg_rational` | [pg_rational](https://github.com/begriffs/pg_rational): fracciones exactas | PGDG | | +| `timestamp9` | [timestamp9](https://github.com/optiver/timestamp9): timestamps con nanosegundos | PGDG | | +| `pg_rrule` | [pg_rrule](https://github.com/petropavel13/pg_rrule): reglas de recurrencia de iCalendar | PGDG | | +| `periods` | [periods](https://github.com/xocolatl/periods): periodos y versionado de sistema de SQL:2016 | PGDG | | +| `temporal_tables` | [temporal_tables](https://github.com/arkhipov/temporal_tables): tablas temporales de periodo de sistema | Fuente | | + +### Replicación y CDC + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pglogical` | [pglogical](https://github.com/2ndQuadrant/pglogical): replicación lógica | PGDG | preload; `wal_level = logical` | +| `wal2json` | [wal2json](https://github.com/eulerto/wal2json): plugin de salida JSON para decodificación lógica | PGDG | plugin de salida, ver advertencias | +| `pg_failover_slots` | [pg_failover_slots](https://github.com/EnterpriseDB/pg_failover_slots): slots lógicos que sobreviven a un failover | PGDG | preload; no se usa con `CREATE EXTENSION` | + +### Monitorización y rendimiento + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pg_stat_monitor` | [pg_stat_monitor](https://github.com/percona/pg_stat_monitor): estadísticas de consultas con histogramas | Fuente | preload | +| `pg_stat_kcache` | [pg_stat_kcache](https://github.com/powa-team/pg_stat_kcache): CPU y E/S por consulta | PGDG | preload, después de `pg_stat_statements` | +| `pg_qualstats` | [pg_qualstats](https://github.com/powa-team/pg_qualstats): estadísticas de predicados | PGDG | preload | +| `pg_wait_sampling` | [pg_wait_sampling](https://github.com/postgrespro/pg_wait_sampling): muestreo de eventos de espera | PGDG | preload | +| `pg_show_plans` | [pg_show_plans](https://github.com/cybertec-postgresql/pg_show_plans): planes de las consultas en curso | PGDG | preload; no junto con timescaledb | +| `pg_statviz` | [pg_statviz](https://github.com/vyruss/pg_statviz): snapshots de estadísticas | PGDG | | +| `hypopg` | [HypoPG](https://github.com/HypoPG/hypopg): índices hipotéticos | PGDG | | +| `pg_hint_plan` | [pg_hint_plan](https://github.com/ossc-db/pg_hint_plan): pistas al planificador en comentarios | PGDG | preload | +| `pgfincore` | [pgfincore](https://github.com/klando/pgfincore): inspección de la caché de páginas del SO | PGDG | | +| `pgnodemx` | [pgnodemx](https://github.com/CrunchyData/pgnodemx): métricas del SO y de cgroups | PGDG | preload | +| `prioritize` | [prioritize](https://github.com/schmiddy/pg_prioritize): prioridad de CPU de los backends | PGDG | | +| `toastinfo` | [toastinfo](https://github.com/credativ/toastinfo): cómo se almacenan los valores | PGDG | | +| `pg_dirtyread` | [pg_dirtyread](https://github.com/df7cb/pg_dirtyread): leer filas borradas | PGDG | | +| `pg_repack` | [pg_repack](https://github.com/reorg/pg_repack): reorganizar tablas en caliente | PGDG | | +| `pg_squeeze` | [pg_squeeze](https://github.com/cybertec-postgresql/pg_squeeze): recuperar espacio sin bloqueos pesados | PGDG | preload; `wal_level = logical` | + +### Seguridad y auditoría + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pgaudit` | [pgAudit](https://github.com/pgaudit/pgaudit): registro de auditoría | PGDG | preload | +| `anon` | [PostgreSQL Anonymizer](https://gitlab.com/dalibo/postgresql_anonymizer): enmascarado de datos | Release | preload; no junto con pg_duckdb | +| `pgsodium` | [pgsodium](https://github.com/michelp/pgsodium): criptografía con libsodium | Fuente | | +| `pg_pwhash` | [pg_pwhash](https://github.com/cybertec-postgresql/pg_pwhash): hash de contraseñas con scrypt, yescrypt y argon2 | PGDG | | +| `credcheck` | [credcheck](https://github.com/HexaCluster/credcheck): políticas de contraseñas y nombres de usuario | PGDG | preload | +| `set_user` | [set_user](https://github.com/pgaudit/set_user): cambio de rol auditado | PGDG | preload | +| `pg_permissions` | [pg_permissions](https://github.com/cybertec-postgresql/pg_permissions): revisar todos los permisos | PGDG | | + +### Datos externos + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `wrappers` | [Wrappers](https://github.com/supabase/wrappers): foreign data wrappers para Stripe, S3, BigQuery… | Release | | +| `pg_clickhouse` | [pg_clickhouse](https://github.com/ClickHouse/pg_clickhouse): consultar ClickHouse | Imagen | | +| `tds_fdw` | [tds_fdw](https://github.com/tds-fdw/tds_fdw): consultar SQL Server y Sybase | PGDG | | + +### Desarrollo y testing + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `pgtap` | [pgTAP](https://pgtap.org/): tests unitarios en SQL | PGDG | | +| `plpgsql_check` | [plpgsql_check](https://github.com/okbob/plpgsql_check): linter de PL/pgSQL | PGDG | | +| `plprofiler` | [plprofiler](https://github.com/bigsql/plprofiler): profiler de PL/pgSQL | PGDG | preload | +| `pljs` | [PLJS](https://github.com/plv8/pljs): lenguaje procedural JavaScript (QuickJS) | PGDG | | + +### Compatibilidad + +| Extensión | Descripción | Origen | Notas | +|---|---|---|---| +| `orafce` | [Orafce](https://github.com/orafce/orafce): funciones y paquetes de Oracle | PGDG | | +| `pgtt` | [pgtt](https://github.com/darold/pgtt): tablas temporales globales al estilo Oracle | PGDG | `LOAD 'pgtt'` en cada sesión | +| `db2fce` | [db2fce](https://github.com/credativ/db2fce): funciones de DB2 | PGDG | | + +## No incluidas + +Estas extensiones se evaluaron, pero no están en la imagen: + +| Extensión | Por qué | +|---|---| +| [plv8](https://github.com/plv8/plv8) | Necesita compilar V8 desde el código fuente para cada arquitectura: horas de build y gigas de caché por imagen. En su lugar se incluye `pljs` (JavaScript sobre QuickJS). | +| [pg_lake](https://github.com/Snowflake-Labs/pg_lake) | Trae su propio DuckDB, que choca con `pg_duckdb` en el mismo servidor, y necesita un toolchain pesado de C++/vcpkg. | +| [pg_durable](https://github.com/microsoft/pg_durable) | Solo se publica para PostgreSQL 17–18 en amd64; arm64 y las versiones anteriores necesitarían compilar con Rust (pgrx). | diff --git a/.docs/included-extensions.md b/.docs/included-extensions.md new file mode 100644 index 0000000..f983577 --- /dev/null +++ b/.docs/included-extensions.md @@ -0,0 +1,197 @@ +# Included extensions + +> [Leer en español](./included-extensions.es.md) + +Every image includes the extensions below, plus all of [contrib](https://www.postgresql.org/docs/current/contrib.html) (`pg_stat_statements`, `pg_trgm`, `pgcrypto`, `hstore`, …), which ships with PostgreSQL. + +The exact version of each one, for every PostgreSQL major, is in the [release notes](https://github.com/ruxwez/postgres/releases). Inside an image: + +```sh +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt +``` + +## Using them + +Nothing is enabled by default, so the image behaves exactly like the official one. Enable what you need with `CREATE EXTENSION ` in each database. + +Extensions marked **preload** also have to be added to `shared_preload_libraries`, which requires a restart: + +```yaml +services: + db: + image: ruxwez/postgres:18-v1 + command: postgres -c shared_preload_libraries=pg_cron,timescaledb -c cron.database_name=postgres +``` + +Some caveats: + +- **pg_show_plans and timescaledb can't be preloaded together.** Inserting into a hypertable crashes the server (segfault). Preload one or the other. +- **anon and pg_duckdb can't be preloaded together.** anon's hooks abort the server when DuckDB runs a query on several threads. +- **Many preloaded extensions start background workers.** If you preload several, raise `max_worker_processes` (the default is 8). +- **wal2json** needs `wal_level = logical`. Recent minor releases also require `output_plugin_libraries = 'wal2json'`. +- **pg_cron** and the extensions built on it (`pg_incremental`, `documentdb`) only work in the database named by `cron.database_name` (`postgres` by default). + +## By category + +The **Source** column says how each extension gets into the image: + +- **PGDG**: package from [apt.postgresql.org](https://apt.postgresql.org). +- **Release**: the authors' prebuilt package. +- **Image**: copied from the authors' official image. +- **Source**: built from source. + +### AI and vectors + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `vector` | [pgvector](https://github.com/pgvector/pgvector): vector similarity search | PGDG | | +| `vectorscale` | [pgvectorscale](https://github.com/timescale/pgvectorscale): DiskANN index for pgvector | Release | | + +### Geospatial + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `postgis` (+ `postgis_raster`, `postgis_topology`, `postgis_sfcgal`, `postgis_tiger_geocoder`) | [PostGIS](https://postgis.net/): geometry and geography types | PGDG | | +| `pgrouting` | [pgRouting](https://pgrouting.org/): routing and network analysis on PostGIS | PGDG | | +| `h3`, `h3_postgis` | [h3-pg](https://github.com/postgis/h3-pg): Uber's H3 hexagonal grid | PGDG | | +| `pg_sphere` | [pgSphere](https://github.com/postgrespro/pgsphere): spherical geometry | PGDG | | + +### Search and text + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pg_textsearch` | [pg_textsearch](https://github.com/timescale/pg_textsearch): BM25-ranked full-text search | PGDG | preload; PG 17+ | +| `rum` | [RUM](https://github.com/postgrespro/rum): GIN-like index with ranking | PGDG | | +| `pg_bigm` | [pg_bigm](https://github.com/pgbigm/pg_bigm): 2-gram full-text search, good for CJK | Source | | +| `pg_similarity` | [pg_similarity](https://github.com/eulerto/pg_similarity): string similarity (Levenshtein, Jaro, cosine…) | PGDG | | +| `pgpcre` | [pgpcre](https://github.com/petere/pgpcre): Perl-compatible regular expressions | PGDG | | +| `re2` | [pg_re2](https://github.com/ClickHouse/pg_re2): RE2 regular expressions | Image | | +| `icu_ext` | [icu_ext](https://github.com/dverite/icu_ext): ICU functions (transliteration, spell-out…) | PGDG | | + +### Time series and analytics + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `timescaledb` | [TimescaleDB](https://github.com/timescale/timescaledb): hypertables and time-series functions (Apache-2.0 edition) | PGDG | preload | +| `pg_duckdb` | [pg_duckdb](https://github.com/duckdb/pg_duckdb): DuckDB analytics engine inside PostgreSQL | Image | preload; not with anon | +| `pg_ivm` | [pg_ivm](https://github.com/sraoss/pg_ivm): incrementally maintained materialized views | PGDG | | +| `pg_incremental` | [pg_incremental](https://github.com/CrunchyData/pg_incremental): incremental batch pipelines | Source | needs pg_cron; PG 15+ | +| `hll` | [postgresql-hll](https://github.com/citusdata/postgresql-hll): HyperLogLog distinct counts | PGDG | | +| `tdigest` | [tdigest](https://github.com/tvondra/tdigest): percentile estimation | PGDG | | +| `roaringbitmap` | [pg_roaringbitmap](https://github.com/ChenHuajun/pg_roaringbitmap): compressed bitmaps | PGDG | | +| `first_last_agg` | [first_last_agg](https://github.com/wulczer/first_last_agg): `first()` and `last()` aggregates | PGDG | | +| `extra_window_functions` | [extra_window_functions](https://github.com/xocolatl/extra_window_functions): more window functions | PGDG | | + +### Queues, jobs and background work + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pgmq` | [PGMQ](https://github.com/pgmq/pgmq): message queues | Source | | +| `pg_cron` | [pg_cron](https://github.com/citusdata/pg_cron): cron-style job scheduler | PGDG | preload | +| `pg_background` | [pg_background](https://github.com/vibhorkum/pg_background): run SQL in background workers | PGDG | | +| `pg_partman` | [pg_partman](https://github.com/pgpartman/pg_partman): automatic partition management | PGDG | preload for its background worker (optional) | + +### JSON, documents and APIs + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `documentdb` | [DocumentDB](https://github.com/documentdb/documentdb): MongoDB-compatible document engine | PGDG | preload; needs pg_cron; PG 15+ | +| `pg_graphql` | [pg_graphql](https://github.com/supabase/pg_graphql): GraphQL API over your tables | Release | | +| `pg_jsonschema` | [pg_jsonschema](https://github.com/supabase/pg_jsonschema): JSON Schema validation | Release | | +| `jsquery` | [jsquery](https://github.com/postgrespro/jsquery): JSON query language | PGDG | | +| `http` | [pgsql-http](https://github.com/pramsey/pgsql-http): synchronous HTTP client | PGDG | | +| `pg_net` | [pg_net](https://github.com/supabase/pg_net): asynchronous HTTP requests | Source | preload | +| `pgjwt` | [pgjwt](https://github.com/michelp/pgjwt): JSON Web Tokens | Source | | +| `postgres_protobuf` | [postgres-protobuf](https://github.com/mpartel/postgres-protobuf): query Protocol Buffers | Source | | +| `pg_csv` | [pg_csv](https://github.com/PostgREST/pg_csv): CSV output | PGDG | | + +### Data types + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `age` | [Apache AGE](https://github.com/apache/age): graph database with openCypher | PGDG | | +| `pg_uuidv7` | [pg_uuidv7](https://github.com/fboulnois/pg_uuidv7): time-ordered UUIDv7 | PGDG | | +| `pg_hashids` | [pg_hashids](https://github.com/iCyberon/pg_hashids): short IDs from integers | Source | | +| `ip4r` | [ip4r](https://github.com/RhodiumToad/ip4r): IPv4/IPv6 ranges | PGDG | | +| `prefix` | [prefix](https://github.com/dimitri/prefix): prefix ranges (phone routing…) | PGDG | | +| `semver` | [semver](https://github.com/theory/pg-semver): semantic versions | PGDG | | +| `pg_rational` | [pg_rational](https://github.com/begriffs/pg_rational): exact fractions | PGDG | | +| `timestamp9` | [timestamp9](https://github.com/optiver/timestamp9): nanosecond timestamps | PGDG | | +| `pg_rrule` | [pg_rrule](https://github.com/petropavel13/pg_rrule): iCalendar recurrence rules | PGDG | | +| `periods` | [periods](https://github.com/xocolatl/periods): SQL:2016 periods and system versioning | PGDG | | +| `temporal_tables` | [temporal_tables](https://github.com/arkhipov/temporal_tables): system-period temporal tables | Source | | + +### Replication and CDC + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pglogical` | [pglogical](https://github.com/2ndQuadrant/pglogical): logical replication | PGDG | preload; `wal_level = logical` | +| `wal2json` | [wal2json](https://github.com/eulerto/wal2json): JSON output plugin for logical decoding | PGDG | output plugin, see caveats | +| `pg_failover_slots` | [pg_failover_slots](https://github.com/EnterpriseDB/pg_failover_slots): logical slots that survive failover | PGDG | preload; not a `CREATE EXTENSION` | + +### Monitoring and performance + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pg_stat_monitor` | [pg_stat_monitor](https://github.com/percona/pg_stat_monitor): query statistics with histograms | Source | preload | +| `pg_stat_kcache` | [pg_stat_kcache](https://github.com/powa-team/pg_stat_kcache): per-query CPU and I/O | PGDG | preload, after `pg_stat_statements` | +| `pg_qualstats` | [pg_qualstats](https://github.com/powa-team/pg_qualstats): predicate statistics | PGDG | preload | +| `pg_wait_sampling` | [pg_wait_sampling](https://github.com/postgrespro/pg_wait_sampling): wait event sampling | PGDG | preload | +| `pg_show_plans` | [pg_show_plans](https://github.com/cybertec-postgresql/pg_show_plans): plans of running queries | PGDG | preload; not with timescaledb | +| `pg_statviz` | [pg_statviz](https://github.com/vyruss/pg_statviz): statistics snapshots | PGDG | | +| `hypopg` | [HypoPG](https://github.com/HypoPG/hypopg): hypothetical indexes | PGDG | | +| `pg_hint_plan` | [pg_hint_plan](https://github.com/ossc-db/pg_hint_plan): planner hints in comments | PGDG | preload | +| `pgfincore` | [pgfincore](https://github.com/klando/pgfincore): OS page cache inspection | PGDG | | +| `pgnodemx` | [pgnodemx](https://github.com/CrunchyData/pgnodemx): OS and cgroup metrics | PGDG | preload | +| `prioritize` | [prioritize](https://github.com/schmiddy/pg_prioritize): backend CPU priority | PGDG | | +| `toastinfo` | [toastinfo](https://github.com/credativ/toastinfo): how values are stored | PGDG | | +| `pg_dirtyread` | [pg_dirtyread](https://github.com/df7cb/pg_dirtyread): read deleted rows | PGDG | | +| `pg_repack` | [pg_repack](https://github.com/reorg/pg_repack): online table reorganization | PGDG | | +| `pg_squeeze` | [pg_squeeze](https://github.com/cybertec-postgresql/pg_squeeze): reclaim space without heavy locks | PGDG | preload; `wal_level = logical` | + +### Security and auditing + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pgaudit` | [pgAudit](https://github.com/pgaudit/pgaudit): audit logging | PGDG | preload | +| `anon` | [PostgreSQL Anonymizer](https://gitlab.com/dalibo/postgresql_anonymizer): data masking | Release | preload; not with pg_duckdb | +| `pgsodium` | [pgsodium](https://github.com/michelp/pgsodium): libsodium cryptography | Source | | +| `pg_pwhash` | [pg_pwhash](https://github.com/cybertec-postgresql/pg_pwhash): scrypt, yescrypt and argon2 password hashing | PGDG | | +| `credcheck` | [credcheck](https://github.com/HexaCluster/credcheck): password and user name policies | PGDG | preload | +| `set_user` | [set_user](https://github.com/pgaudit/set_user): audited role switching | PGDG | preload | +| `pg_permissions` | [pg_permissions](https://github.com/cybertec-postgresql/pg_permissions): review all permissions | PGDG | | + +### Foreign data + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `wrappers` | [Wrappers](https://github.com/supabase/wrappers): foreign data wrappers for Stripe, S3, BigQuery… | Release | | +| `pg_clickhouse` | [pg_clickhouse](https://github.com/ClickHouse/pg_clickhouse): query ClickHouse | Image | | +| `tds_fdw` | [tds_fdw](https://github.com/tds-fdw/tds_fdw): query SQL Server and Sybase | PGDG | | + +### Development and testing + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `pgtap` | [pgTAP](https://pgtap.org/): unit tests in SQL | PGDG | | +| `plpgsql_check` | [plpgsql_check](https://github.com/okbob/plpgsql_check): PL/pgSQL linter | PGDG | | +| `plprofiler` | [plprofiler](https://github.com/bigsql/plprofiler): PL/pgSQL profiler | PGDG | preload | +| `pljs` | [PLJS](https://github.com/plv8/pljs): JavaScript procedural language (QuickJS) | PGDG | | + +### Compatibility + +| Extension | Description | Source | Notes | +|---|---|---|---| +| `orafce` | [Orafce](https://github.com/orafce/orafce): Oracle functions and packages | PGDG | | +| `pgtt` | [pgtt](https://github.com/darold/pgtt): Oracle-style global temporary tables | PGDG | `LOAD 'pgtt'` per session | +| `db2fce` | [db2fce](https://github.com/credativ/db2fce): DB2 functions | PGDG | | + +## Not included + +These extensions were evaluated but aren't in the image: + +| Extension | Why | +|---|---| +| [plv8](https://github.com/plv8/plv8) | Needs V8 compiled from source for every architecture: hours of build time and gigabytes of build cache per image. `pljs` (JavaScript on QuickJS) is included instead. | +| [pg_lake](https://github.com/Snowflake-Labs/pg_lake) | Bundles its own DuckDB, which conflicts with `pg_duckdb` in the same server, and needs a heavy C++/vcpkg toolchain. | +| [pg_durable](https://github.com/microsoft/pg_durable) | Only published for PostgreSQL 17–18 on amd64; arm64 and older majors would need a Rust (pgrx) build. | diff --git a/.docs/releases.es.md b/.docs/releases.es.md index e406015..11c8e6f 100644 --- a/.docs/releases.es.md +++ b/.docs/releases.es.md @@ -45,9 +45,15 @@ O crea la release desde la interfaz de GitHub con un tag nuevo. Después, el CI: Si un job de publicación falla a medias (por ejemplo, por un error de Docker Hub), usa **Re-run failed jobs**. Si relanzas todo el workflow, se detendrá en el paso 2, porque algunos tags ya existen. -## Labels +## Qué contiene una imagen -Cada imagen lleva sus versiones como labels: +Cada imagen incluye la lista de sus extensiones con sus versiones: + +```sh +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt +``` + +Y lleva estos labels: ```sh docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' @@ -58,4 +64,3 @@ docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' | `org.opencontainers.image.version` | release del proyecto (`1.4.0`); vacío fuera de las releases | | `org.opencontainers.image.revision` | commit de git | | `io.github.ruxwez.postgres.postgresql.version` | versión del paquete de PostgreSQL | -| `io.github.ruxwez.postgres..version` | versión de cada extensión (pgvector, pgmq, postgis) | diff --git a/.docs/releases.md b/.docs/releases.md index 2afb985..bf1aaa4 100644 --- a/.docs/releases.md +++ b/.docs/releases.md @@ -45,9 +45,15 @@ Or create the release from the GitHub UI with a new tag. CI then: If a publish job fails halfway (e.g. a Docker Hub error), use **Re-run failed jobs**. A full re-run would stop at step 2, because some tags already exist. -## Labels +## What an image contains -Every image carries its versions as labels: +Every image lists its extensions and their versions: + +```sh +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt +``` + +And carries these labels: ```sh docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' @@ -58,4 +64,3 @@ docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' | `org.opencontainers.image.version` | project release (`1.4.0`), empty outside releases | | `org.opencontainers.image.revision` | git commit | | `io.github.ruxwez.postgres.postgresql.version` | PostgreSQL package version | -| `io.github.ruxwez.postgres..version` | version of each extension (pgvector, pgmq, postgis) | diff --git a/.docs/tests.es.md b/.docs/tests.es.md index b13b4cc..02df18d 100644 --- a/.docs/tests.es.md +++ b/.docs/tests.es.md @@ -15,20 +15,23 @@ make test PG_VERSION=14 ## Qué hace `tests/run.sh` -1. Arranca la imagen con `POSTGRES_PASSWORD=postgres`. +1. Arranca la imagen con `POSTGRES_PASSWORD=postgres` y la configuración de todos los `extensions/*/server.conf` (los valores de `shared_preload_libraries` se combinan). 2. Espera a que PostgreSQL acepte conexiones TCP. El servidor temporal de inicialización del entrypoint solo escucha en el socket unix, así que la espera no termina antes de tiempo. -3. Para cada `extensions/*/test.sql`, crea una base de datos nueva, ejecuta el archivo con `ON_ERROR_STOP=1` e imprime `PASS` (con las versiones instaladas) o `FAIL` (con el error). -4. Termina con un código distinto de cero si algún test falla, después de ejecutarlos todos. +3. Para cada `extensions/*/test.sql`, crea una base de datos nueva, ejecuta el archivo con `ON_ERROR_STOP=1` e imprime `PASS` (con las versiones de las extensiones que creó) o `FAIL` (con el error). Las extensiones que no soportan esa versión mayor de PostgreSQL salen como `SKIP`. +4. Ejecuta los tests marcados como `isolated` en un servidor propio. +5. Termina con un código distinto de cero si algún test falla, después de ejecutarlos todos. ``` -PASS contrib (pg_stat_statements 1.12, pg_trgm 1.6) -PASS pgmq (pgmq 1.11.1) -PASS pgvector (vector 0.8.2) -PASS postgis (postgis 3.6.4) +PASS age (age 1.8.0) +PASS anon (anon 3.2.3) +… +SKIP pg_textsearch (supports PostgreSQL 17 18) +… +Passed: 81, skipped: 1, failed: 0 All tests passed. ``` -Para probar una extensión nueva, añade su `test.sql` (ver [Extensiones](./extensions.es.md)). No hace falta registrarlo en ningún sitio. +Para probar una extensión nueva, añade su `test.sql` (ver [Extensiones](./extensions.es.md#escribir-el-test), donde también se explican `server.conf`, `pg-versions`, `database` e `isolated`). No hace falta registrarlo en ningún sitio. ## CI diff --git a/.docs/tests.md b/.docs/tests.md index 1ff573c..8688924 100644 --- a/.docs/tests.md +++ b/.docs/tests.md @@ -15,20 +15,23 @@ make test PG_VERSION=14 ## What `tests/run.sh` does -1. Starts the image with `POSTGRES_PASSWORD=postgres`. +1. Starts the image with `POSTGRES_PASSWORD=postgres` and the settings from every `extensions/*/server.conf` (`shared_preload_libraries` values are merged). 2. Waits until PostgreSQL accepts TCP connections. The entrypoint's temporary init server only listens on the unix socket, so this check doesn't stop too early. -3. For every `extensions/*/test.sql`, it creates a fresh database, runs the file with `ON_ERROR_STOP=1`, and prints `PASS` (with the installed extension versions) or `FAIL` (with the error). -4. Exits non-zero if any test failed, after running all of them. +3. For every `extensions/*/test.sql`, it creates a fresh database, runs the file with `ON_ERROR_STOP=1`, and prints `PASS` (with the extension versions it created) or `FAIL` (with the error). Extensions that don't support this PostgreSQL major are reported as `SKIP`. +4. Runs the tests marked `isolated` on a server of their own. +5. Exits non-zero if any test failed, after running all of them. ``` -PASS contrib (pg_stat_statements 1.12, pg_trgm 1.6) -PASS pgmq (pgmq 1.11.1) -PASS pgvector (vector 0.8.2) -PASS postgis (postgis 3.6.4) +PASS age (age 1.8.0) +PASS anon (anon 3.2.3) +… +SKIP pg_textsearch (supports PostgreSQL 17 18) +… +Passed: 81, skipped: 1, failed: 0 All tests passed. ``` -To test a new extension, add its `test.sql` (see [Extensions](./extensions.md)). You don't need to register it anywhere. +To test a new extension, add its `test.sql` (see [Extensions](./extensions.md#writing-the-test), which also explains `server.conf`, `pg-versions`, `database` and `isolated`). You don't need to register it anywhere. ## CI diff --git a/.gitattributes b/.gitattributes index 891c9fc..244a882 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# Scripts run inside Linux containers and CI; keep LF even on Windows checkouts. -*.sh text eol=lf +# Keep LF everywhere: these files are read inside Linux containers and CI, +# where CRLF breaks shell scripts and the version lists. +* text=auto eol=lf diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh index db87cb4..895cd4e 100755 --- a/.github/scripts/release-notes.sh +++ b/.github/scripts/release-notes.sh @@ -2,9 +2,10 @@ # # Usage: release-notes.sh RELEASE SUMMARY_DIR # -# Prints a Markdown table with what each image of RELEASE contains. SUMMARY_DIR -# holds one file per PostgreSQL major, named after its bake target (pg-18, ...), -# as written by tests/run.sh with SUMMARY_FILE. +# Prints a Markdown table with what each image of RELEASE contains: one column +# per PostgreSQL major, one row per extension. SUMMARY_DIR holds one file per +# major, named after its bake target (pg-18, ...), as written by tests/run.sh +# with SUMMARY_FILE. set -euo pipefail @@ -12,12 +13,35 @@ RELEASE="${1:?usage: $0 RELEASE SUMMARY_DIR}" DIR="${2:?usage: $0 RELEASE SUMMARY_DIR}" IMAGE="${IMAGE:-ruxwez/postgres}" -echo "| Image | PostgreSQL | Extensions |" -echo "|---|---|---|" # Newest major first. -find "$DIR" -maxdepth 1 -type f -name 'pg-*' -printf '%f\n' | sort -t- -k2 -rn | while read -r file; do - major="${file#pg-}" - pg_version="$(awk '$1 == "postgresql" { print $2 }' "$DIR/$file")" - extensions="$(awk '$1 != "postgresql" { printf "%s%s %s", sep, $1, $2; sep = " · " }' "$DIR/$file")" - echo "| \`$IMAGE:$major-v$RELEASE\` | $pg_version | $extensions |" +mapfile -t majors < <(find "$DIR" -maxdepth 1 -type f -name 'pg-*' -printf '%f\n' | sed 's/^pg-//' | sort -rn) + +declare -A version=() +for major in "${majors[@]}"; do + while read -r name ver; do + [ -n "$name" ] && version[$name,$major]="$ver" + done <"$DIR/pg-$major" +done +mapfile -t extensions < <(for key in "${!version[@]}"; do echo "${key%,*}"; done | grep -vx postgresql | sort -u) + +row() { + local line="| $1 |" + shift + for cell in "$@"; do line+=" $cell |"; done + echo "$line" +} + +cells=() +for major in "${majors[@]}"; do cells+=("PG $major"); done +row "" "${cells[@]}" +row "---" "${majors[@]/*/---}" + +cells=() +for major in "${majors[@]}"; do cells+=("\`$IMAGE:$major-v$RELEASE\`"); done +row "**Image**" "${cells[@]}" + +for name in postgresql "${extensions[@]}"; do + cells=() + for major in "${majors[@]}"; do cells+=("${version[$name,$major]:-—}"); done + row "$([ "$name" = postgresql ] && echo PostgreSQL || echo "$name")" "${cells[@]}" done diff --git a/.github/workflows/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml index c4644b0..3c21e41 100644 --- a/.github/workflows/dockerhub-description.yml +++ b/.github/workflows/dockerhub-description.yml @@ -38,7 +38,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ env.TOKEN }} repository: ruxwez/postgres - short-description: PostgreSQL 14-18 with pgvector, PostGIS and pgmq. Official image, amd64 + arm64. + short-description: PostgreSQL 14-18 with 80+ extensions. pgvector, PostGIS, TimescaleDB, pgmq, pg_cron... readme-filepath: README.md # Turns relative links (./.docs/...) into absolute GitHub URLs. enable-url-completion: true diff --git a/CONTRIBUTING.es.md b/CONTRIBUTING.es.md index 7bcec9f..c4a0dc9 100644 --- a/CONTRIBUTING.es.md +++ b/CONTRIBUTING.es.md @@ -22,7 +22,7 @@ Usa las plantillas de `.github/ISSUE_TEMPLATE/`: make test PG_VERSION=18 ``` -4. Si añades una extensión, sigue [Extensiones](./.docs/extensions.es.md). Se reduce a unas líneas en el `Dockerfile`, un `extensions//test.sql` y una fila en la tabla del README. +4. Si añades una extensión, sigue [Extensiones](./.docs/extensions.es.md). Se reduce a una línea en `extensions/pgdg.txt` (o unas pocas en el `Dockerfile`), un `extensions//test.sql` y una fila en [Extensiones incluidas](./.docs/included-extensions.es.md). 5. Abre el PR contra `main`. El CI construye y prueba todas las versiones en amd64 y arm64. Al hacer merge solo se actualizan los tags móviles (`18`, `latest`). Los mantenedores sacan releases para actualizar los tags con versión (ver [Releases](./.docs/releases.es.md)). @@ -38,7 +38,9 @@ Al hacer merge solo se actualizan los tags móviles (`18`, `latest`). Los manten ``` Dockerfile Definición de la imagen (una etapa por extensión compilada) docker-bake.hcl Versiones de PostgreSQL y tags -extensions// test.sql de cada extensión +extensions/pgdg.txt Paquetes de PGDG y sus versiones +extensions/*.sh Scripts de build que usa el Dockerfile +extensions// test.sql de cada extensión (más server.conf, … opcionales) tests/run.sh Ejecuta todos los test.sql contra una imagen .github/workflows/ CI: build, test, publicación, release .github/scripts/ Scripts que usa el CI (release notes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e95089b..a2ac265 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Use the templates in `.github/ISSUE_TEMPLATE/`: make test PG_VERSION=18 ``` -4. If you're adding an extension, follow [Extensions](./.docs/extensions.md). It comes down to a few lines in the `Dockerfile`, an `extensions//test.sql`, and a row in the README table. +4. If you're adding an extension, follow [Extensions](./.docs/extensions.md). It comes down to a line in `extensions/pgdg.txt` (or a few in the `Dockerfile`), an `extensions//test.sql`, and a row in [Included extensions](./.docs/included-extensions.md). 5. Open the PR against `main`. CI builds and tests every version on amd64 and arm64. Merging only updates the floating tags (`18`, `latest`). Maintainers cut releases to update the versioned tags (see [Releases](./.docs/releases.md)). @@ -38,7 +38,9 @@ Merging only updates the floating tags (`18`, `latest`). Maintainers cut release ``` Dockerfile Image definition (one stage per extension built from source) docker-bake.hcl PostgreSQL versions and tags -extensions// test.sql for each extension +extensions/pgdg.txt PGDG packages and their versions +extensions/*.sh Build helpers used by the Dockerfile +extensions// test.sql for each extension (plus optional server.conf, ...) tests/run.sh Runs every test.sql against an image .github/workflows/ CI: build, test, publish, release .github/scripts/ Helpers used by CI (release notes) diff --git a/Dockerfile b/Dockerfile index 1d4c634..c5e5e6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,66 +2,174 @@ # Builds one image: postgres: plus the extensions below. # The list of versions/tags lives in docker-bake.hcl. +# +# Extensions come from four places, in order of preference: +# 1. PGDG apt packages, listed in extensions/pgdg.txt. +# 2. Prebuilt packages published by the extension's authors. +# 3. Files copied from the authors' official Docker image. +# 4. Built from source, for anything else. +# Build tools never reach the final image: stages 2-4 install into /out and +# only that directory is copied over. ARG POSTGRES_VERSION=18 -# Extension versions. Renovate bumps them using the comment right above each -# one. -# renovate: datasource=github-tags depName=pgvector/pgvector -ARG PGVECTOR_VERSION=0.8.2 +# Versions of the extensions that don't come from PGDG. Renovate bumps them +# using the comment right above each one. + # renovate: datasource=github-tags depName=pgmq/pgmq ARG PGMQ_VERSION=1.11.1 -# PGDG package, same version for every PostgreSQL major. PGDG keeps a few -# previous versions in its index, so the pin keeps working for a while after a -# new one comes out. -# renovate: datasource=deb depName=postgresql-18-postgis-3 registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 -ARG POSTGIS_VERSION=3.6.4 +# renovate: datasource=github-tags depName=pgbigm/pg_bigm +ARG PG_BIGM_VERSION=1.2-20250903 +# renovate: datasource=github-tags depName=iCyberon/pg_hashids +ARG PG_HASHIDS_VERSION=1.2.1 +# renovate: datasource=github-tags depName=CrunchyData/pg_incremental +ARG PG_INCREMENTAL_VERSION=1.5.0 +# renovate: datasource=github-tags depName=supabase/pg_net +ARG PG_NET_VERSION=0.20.5 +# renovate: datasource=github-tags depName=percona/pg_stat_monitor +ARG PG_STAT_MONITOR_VERSION=2.4.0 +# renovate: datasource=github-tags depName=michelp/pgsodium +ARG PGSODIUM_VERSION=3.1.11 +# renovate: datasource=github-tags depName=mpartel/postgres-protobuf +ARG POSTGRES_PROTOBUF_VERSION=0.3.3 +# renovate: datasource=github-tags depName=arkhipov/temporal_tables +ARG TEMPORAL_TABLES_VERSION=1.2.2 +# No tagged releases upstream; pinned to its latest commit (2023). +ARG PGJWT_COMMIT=f3d82fd30151e754e19ce5d6a06c71c20689ce3d + +# renovate: datasource=gitlab-releases depName=dalibo/postgresql_anonymizer +ARG ANON_VERSION=3.2.3 +# renovate: datasource=github-releases depName=supabase/pg_graphql +ARG PG_GRAPHQL_VERSION=1.6.2 +# renovate: datasource=github-releases depName=supabase/pg_jsonschema +ARG PG_JSONSCHEMA_VERSION=0.3.4 +# renovate: datasource=github-releases depName=timescale/pgvectorscale +ARG PGVECTORSCALE_VERSION=0.9.1 +# renovate: datasource=github-releases depName=supabase/wrappers +ARG WRAPPERS_VERSION=0.6.4 + +# renovate: datasource=github-releases depName=duckdb/pg_duckdb +ARG PG_DUCKDB_VERSION=1.1.1 +# renovate: datasource=github-releases depName=ClickHouse/pg_clickhouse +ARG PG_CLICKHOUSE_VERSION=0.11.0 # --------------------------------------------------------------------------- -# Toolchain for extensions compiled from source. Nothing from this stage ends -# up in the final image: each extension stage installs into /out and only that -# directory is copied over. +# Toolchain shared by the stages below. # --------------------------------------------------------------------------- FROM postgres:${POSTGRES_VERSION} AS build-base RUN apt-get update \ - && apt-get install -y ca-certificates git make gcc "postgresql-server-dev-${PG_MAJOR}" + && apt-get install -y ca-certificates curl git make gcc g++ unzip \ + "postgresql-server-dev-${PG_MAJOR}" +COPY extensions/pgxs-build.sh /usr/local/bin/pgxs-build +COPY extensions/fetch-deb.sh /usr/local/bin/fetch-deb + +# --------------------------------------------------------------------------- +# Prebuilt packages from the extensions' releases. +# --------------------------------------------------------------------------- +FROM build-base AS prebuilt +ARG TARGETARCH +ARG ANON_VERSION +ARG PG_GRAPHQL_VERSION +ARG PG_JSONSCHEMA_VERSION +ARG PGVECTORSCALE_VERSION +ARG WRAPPERS_VERSION +RUN fetch-deb "https://gitlab.com/api/v4/projects/dalibo%2Fpostgresql_anonymizer/packages/generic/release-assets/${ANON_VERSION}/postgresql_anonymizer_${PG_MAJOR}_${ANON_VERSION}_${TARGETARCH}.deb" \ + && fetch-deb "https://github.com/supabase/pg_graphql/releases/download/v${PG_GRAPHQL_VERSION}/pg_graphql-v${PG_GRAPHQL_VERSION}-pg${PG_MAJOR}-${TARGETARCH}-linux-gnu.deb" \ + && fetch-deb "https://github.com/supabase/pg_jsonschema/releases/download/v${PG_JSONSCHEMA_VERSION}/pg_jsonschema-v${PG_JSONSCHEMA_VERSION}-pg${PG_MAJOR}-${TARGETARCH}-linux-gnu.deb" \ + && fetch-deb "https://github.com/timescale/pgvectorscale/releases/download/${PGVECTORSCALE_VERSION}/pgvectorscale-${PGVECTORSCALE_VERSION}-pg${PG_MAJOR}-${TARGETARCH}.zip" \ + && fetch-deb "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAJOR}-${TARGETARCH}-linux-gnu.deb" + +# --------------------------------------------------------------------------- +# Files copied from the extensions' official images, which ship the only +# prebuilt binaries (both bundle large C++ engines). +# --------------------------------------------------------------------------- +FROM pgduckdb/pgduckdb:${POSTGRES_VERSION}-v${PG_DUCKDB_VERSION} AS pg-duckdb-upstream +USER root +RUN mkdir -p /out/usr/lib/postgresql/$PG_MAJOR/lib /out/usr/share/postgresql/$PG_MAJOR/extension \ + && cp -a /usr/lib/postgresql/$PG_MAJOR/lib/pg_duckdb.so /usr/lib/postgresql/$PG_MAJOR/lib/libduckdb.so \ + /out/usr/lib/postgresql/$PG_MAJOR/lib/ \ + && cp -a /usr/share/postgresql/$PG_MAJOR/extension/pg_duckdb* /out/usr/share/postgresql/$PG_MAJOR/extension/ -FROM build-base AS pgvector -ARG PGVECTOR_VERSION -# OPTFLAGS="" avoids -march=native, so the binary runs on any CPU and not just -# the one that built it. -RUN git clone --depth 1 --branch "v${PGVECTOR_VERSION}" https://github.com/pgvector/pgvector.git /src \ - && make -C /src OPTFLAGS="" \ - && make -C /src install OPTFLAGS="" DESTDIR=/out +# Also provides re2 (ClickHouse/pg_re2), its companion for regex pushdown. +FROM ghcr.io/clickhouse/pg_clickhouse:${POSTGRES_VERSION}-${PG_CLICKHOUSE_VERSION} AS pg-clickhouse-upstream +USER root +RUN mkdir -p /out/usr/lib/postgresql/$PG_MAJOR/lib /out/usr/share/postgresql/$PG_MAJOR/extension \ + && cp -a /usr/lib/postgresql/$PG_MAJOR/lib/pg_clickhouse.so /usr/lib/postgresql/$PG_MAJOR/lib/re2.so \ + /out/usr/lib/postgresql/$PG_MAJOR/lib/ \ + && cp -a /usr/share/postgresql/$PG_MAJOR/extension/pg_clickhouse* /usr/share/postgresql/$PG_MAJOR/extension/re2* \ + /out/usr/share/postgresql/$PG_MAJOR/extension/ -FROM build-base AS pgmq +# --------------------------------------------------------------------------- +# Built from source. +# --------------------------------------------------------------------------- +FROM build-base AS source +RUN apt-get install -y libcurl4-openssl-dev libicu-dev cmake zlib1g-dev ruby +# pgsodium needs libsodium 1.0.21+, newer than Debian's. Built as a static +# library so pgsodium carries it and the final image needs nothing extra. +# Bump it together with pgsodium. +ARG LIBSODIUM_VERSION=1.0.22 +RUN curl -fsSL --retry 5 --retry-all-errors \ + "https://github.com/jedisct1/libsodium/releases/download/${LIBSODIUM_VERSION}-RELEASE/libsodium-${LIBSODIUM_VERSION}.tar.gz" \ + | tar xz -C /tmp \ + && cd "/tmp/libsodium-${LIBSODIUM_VERSION}" \ + && ./configure --prefix=/usr/local --disable-shared --with-pic \ + && make -j"$(nproc)" install ARG PGMQ_VERSION -RUN git clone --depth 1 --branch "v${PGMQ_VERSION}" https://github.com/pgmq/pgmq.git /src \ - && make -C /src/pgmq-extension \ - && make -C /src/pgmq-extension install DESTDIR=/out +ARG PG_BIGM_VERSION +ARG PG_HASHIDS_VERSION +ARG PG_INCREMENTAL_VERSION +ARG PG_NET_VERSION +ARG PG_STAT_MONITOR_VERSION +ARG PGSODIUM_VERSION +ARG POSTGRES_PROTOBUF_VERSION +ARG TEMPORAL_TABLES_VERSION +ARG PGJWT_COMMIT +RUN pgxs-build https://github.com/pgmq/pgmq.git "v${PGMQ_VERSION}" pgmq-extension +RUN pgxs-build https://github.com/pgbigm/pg_bigm.git "v${PG_BIGM_VERSION}" . USE_PGXS=1 +RUN pgxs-build https://github.com/iCyberon/pg_hashids.git "v${PG_HASHIDS_VERSION}" +RUN MIN_PG_MAJOR=15 pgxs-build https://github.com/CrunchyData/pg_incremental.git "v${PG_INCREMENTAL_VERSION}" +RUN pgxs-build https://github.com/supabase/pg_net.git "v${PG_NET_VERSION}" +RUN pgxs-build https://github.com/percona/pg_stat_monitor.git "${PG_STAT_MONITOR_VERSION}" . USE_PGXS=1 +RUN pgxs-build https://github.com/michelp/pgsodium.git "v${PGSODIUM_VERSION}" +RUN pgxs-build https://github.com/arkhipov/temporal_tables.git "v${TEMPORAL_TABLES_VERSION}" +RUN pgxs-build https://github.com/michelp/pgjwt.git "${PGJWT_COMMIT}" +# Links a static protobuf library that its own script downloads and builds. +RUN git clone --depth 1 --branch "v${POSTGRES_PROTOBUF_VERSION}" --recurse-submodules --shallow-submodules \ + https://github.com/mpartel/postgres-protobuf.git /tmp/postgres-protobuf \ + && cd /tmp/postgres-protobuf \ + && ./build-protobuf-library.sh \ + && make -j"$(nproc)" \ + && make install DESTDIR=/out # --------------------------------------------------------------------------- # Final image # --------------------------------------------------------------------------- FROM postgres:${POSTGRES_VERSION} -ARG PGVECTOR_VERSION -ARG PGMQ_VERSION -ARG POSTGIS_VERSION -# Extensions available as PGDG packages. contrib (pg_stat_statements, -# pg_trgm, ...) already ships with the base image. "+*" matches any Debian -# revision of the pinned upstream version (3.6.4+dfsg-2.pgdg13+1, ...). -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ - && rm -rf /var/lib/apt/lists/* +# PGDG packages from extensions/pgdg.txt (the lines for this major), plus the +# runtime libraries of the extensions copied in below. contrib +# (pg_stat_statements, pg_trgm, ...) already ships with the base image. +RUN --mount=type=bind,source=extensions/pgdg.txt,target=/tmp/pgdg.txt \ + set -eu; \ + packages="$(awk -v major="$PG_MAJOR" ' \ + /^[[:space:]]*(#|$)/ { next } \ + { split($1, name, "/"); if (name[2] != "" && name[2] != major) next; \ + printf "postgresql-%s-%s=%s* ", major, name[1], $2 }' /tmp/pgdg.txt)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends $packages \ + libcurl4t64 liblz4-1 libre2-11 libxxhash0; \ + rm -rf /var/lib/apt/lists/* + +COPY --from=prebuilt /out/ / +COPY --from=pg-duckdb-upstream /out/ / +COPY --from=pg-clickhouse-upstream /out/ / +COPY --from=source /out/ / -COPY --from=pgvector /out/ / -COPY --from=pgmq /out/ / +# Every extension and its version, readable without starting PostgreSQL: +# docker run --rm ruxwez/postgres:18 cat /usr/share/postgres-extensor/extensions.txt +RUN --mount=type=bind,source=extensions/list-extensions.sh,target=/tmp/list-extensions.sh \ + mkdir -p /usr/share/postgres-extensor \ + && /tmp/list-extensions.sh > /usr/share/postgres-extensor/extensions.txt -# What's inside, readable with `docker inspect` without starting the image. # PG_VERSION is the Debian package version set by the base image. -LABEL io.github.ruxwez.postgres.postgresql.version="${PG_VERSION}" \ - io.github.ruxwez.postgres.pgvector.version="${PGVECTOR_VERSION}" \ - io.github.ruxwez.postgres.pgmq.version="${PGMQ_VERSION}" \ - io.github.ruxwez.postgres.postgis.version="${POSTGIS_VERSION}" +LABEL io.github.ruxwez.postgres.postgresql.version="${PG_VERSION}" diff --git a/README.es.md b/README.es.md index 09a6cda..ba625ed 100644 --- a/README.es.md +++ b/README.es.md @@ -7,10 +7,10 @@ > [Read in English](./README.md) -**La imagen oficial de PostgreSQL con pgvector, PostGIS y pgmq preinstalados.** Búsqueda vectorial, consultas geoespaciales y colas de mensajes en un solo Postgres: sin servicios extra y sin tener que mantener una imagen propia. +**La imagen oficial de PostgreSQL con más de 80 extensiones preinstaladas**: pgvector, PostGIS, pgmq, TimescaleDB, pg_cron, pg_duckdb, Apache AGE, pg_graphql y [muchas más](./.docs/included-extensions.es.md). Búsqueda vectorial, consultas geoespaciales, colas, series temporales y analítica en un solo Postgres: sin servicios extra y sin tener que mantener una imagen propia. -- **Sustituye directamente** a `postgres`. Mismo entrypoint, mismas variables de entorno, todo igual; solo se añaden las extensiones. -- **PostgreSQL 14 a 18**, en `linux/amd64` y `linux/arm64`. Las dos arquitecturas se prueban antes de cada publicación. +- **Sustituye directamente** a `postgres`. Mismo entrypoint, mismas variables de entorno, misma configuración por defecto; no se activa nada hasta que haces `CREATE EXTENSION`. +- **PostgreSQL 14 a 18**, en `linux/amd64` y `linux/arm64`. Cada extensión tiene un test funcional, que se ejecuta en las dos arquitecturas antes de cada publicación. - **Tags inmutables con versión** (`18-v1.0.0`): una actualización nunca te llega si no la pides. - **Se reconstruye cada semana** con los parches de seguridad de la imagen base. @@ -73,14 +73,14 @@ SELECT msg_id, message FROM pgmq.read(queue_name => 'embeddings', vt => 30, qty Si solo necesitas una extensión, su propia imagen es una gran opción. Esta es para cuando necesitas varias: -| | `postgres` | `pgvector/pgvector` | `postgis/postgis` | **`ruxwez/postgres`** | -|---|:-:|:-:|:-:|:-:| -| pgvector | — | ✅ | — | ✅ | -| PostGIS | — | — | ✅ | ✅ | -| pgmq | — | — | — | ✅ | -| `linux/arm64` (Apple Silicon, Graviton, Raspberry Pi) | ✅ | ✅ | — | ✅ | +| | `postgres` | `pgvector/pgvector` | `postgis/postgis` | `pglayers-full` | **`ruxwez/postgres`** | +|---|:-:|:-:|:-:|:-:|:-:| +| Extensiones además de contrib | — | 1 | 1 | 80+ | 80+ | +| PostgreSQL 14–16 | ✅ | ✅ | ✅ | — | ✅ | +| `linux/arm64` (Apple Silicon, Graviton, Raspberry Pi) | ✅ | ✅ | — | ✅ | ✅ | +| Misma configuración por defecto que la oficial | ✅ | ✅ | ✅ | — (precarga librerías) | ✅ | -Comprobado en los tags de PostgreSQL 18 de cada imagen (`pg18`, `18-3.6`) en octubre de 2026. +Comprobado en octubre de 2026 en los tags de PostgreSQL 14 y 18 de cada imagen. [pglayers](https://github.com/pglayers/pglayers) publica además cada extensión como una capa separada, lo que te encaja si quieres montar tu propia imagen. ## Tags @@ -95,22 +95,32 @@ Elige el tag según cuántos cambios estés dispuesto a aceptar: La versión `vX.Y.Z` es la de este proyecto, no la de PostgreSQL: cambia cuando cambia una extensión. Cada [release](https://github.com/ruxwez/postgres/releases) indica las versiones exactas de PostgreSQL y de las extensiones de cada imagen. En [Releases](./.docs/releases.es.md) se explica qué significa cada tipo de subida. -Las versiones también están en los labels de la imagen: +Cada imagen incluye también la lista de lo que contiene: ```sh -docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt ``` Para una reproducibilidad exacta, fija un digest: `ruxwez/postgres@sha256:…`. ### Extensiones incluidas -| Extensión | Versión | Origen | -|-----------|---------|--------| -| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Compilada desde el código fuente | -| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Compilada desde el código fuente | -| [PostGIS](https://postgis.net/) | 3.6.4 | Paquete apt de PGDG (`postgresql-N-postgis-3`) | -| [contrib](https://www.postgresql.org/docs/current/contrib.html) | — | Incluida en la imagen base (`pg_stat_statements`, `pg_trgm`, …) | +| Categoría | Extensiones | +|---|---| +| IA y vectores | pgvector, pgvectorscale | +| Geoespacial | PostGIS, pgRouting, H3, pgSphere | +| Búsqueda y texto | pg_textsearch (BM25), RUM, pg_bigm, pg_similarity, pgpcre, re2, icu_ext | +| Series temporales y analítica | TimescaleDB, pg_duckdb, pg_ivm, pg_incremental, hll, tdigest, roaringbitmap, … | +| Colas y tareas | pgmq, pg_cron, pg_background, pg_partman | +| JSON, documentos y APIs | DocumentDB, pg_graphql, pg_jsonschema, jsquery, http, pg_net, pgjwt, … | +| Tipos de datos | Apache AGE (grafos), pg_uuidv7, pg_hashids, ip4r, semver, periods, temporal_tables, … | +| Replicación y CDC | pglogical, wal2json, pg_failover_slots | +| Monitorización y rendimiento | pg_stat_monitor, pg_stat_kcache, pg_qualstats, hypopg, pg_hint_plan, pg_repack, pg_squeeze, … | +| Seguridad | pgaudit, PostgreSQL Anonymizer, pgsodium, credcheck, set_user, … | +| Datos externos | Supabase Wrappers, pg_clickhouse, tds_fdw | +| Desarrollo y compatibilidad | pgTAP, plpgsql_check, plprofiler, PLJS, orafce, pgtt, db2fce | + +Más todo [contrib](https://www.postgresql.org/docs/current/contrib.html). En la [lista completa](./.docs/included-extensions.es.md) tienes los enlaces, cuáles necesitan `shared_preload_libraries` y las advertencias conocidas. ## Úsala en tests y CI @@ -153,9 +163,11 @@ with PostgresContainer("ruxwez/postgres:18-v1") as postgres: Todo está en dos archivos: -- [`Dockerfile`](./Dockerfile): parte de `postgres:N`, instala las extensiones que hay en apt y copia las que se compilan en etapas de build separadas. Los compiladores y headers nunca llegan a la imagen final. +- [`Dockerfile`](./Dockerfile): parte de `postgres:N`, instala los paquetes de PGDG listados en [`extensions/pgdg.txt`](./extensions/pgdg.txt) y copia las extensiones que vienen de las releases de sus autores o que se compilan en etapas de build separadas. Los compiladores y headers nunca llegan a la imagen final. - [`docker-bake.hcl`](./docker-bake.hcl): la lista de versiones de PostgreSQL y sus tags. El CI también la lee. +Cada extensión tiene un test funcional en `extensions//test.sql`. + Para construir y probar en local (requiere Docker con Buildx): ```sh @@ -166,6 +178,7 @@ make build-all # construye todas las versiones ## Documentación +- [Extensiones incluidas](./.docs/included-extensions.es.md): la lista completa, con notas de configuración. - [Extensiones](./.docs/extensions.es.md): añadir o actualizar una extensión o una versión de PostgreSQL. - [Tests](./.docs/tests.es.md): cómo funcionan los tests y el CI. - [Releases](./.docs/releases.es.md): versionado y cómo publicar una release. diff --git a/README.md b/README.md index 485f51e..41bf360 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ > [Leer en español](./README.es.md) -**The official PostgreSQL image with pgvector, PostGIS and pgmq preinstalled.** Vector search, geospatial queries and message queues in a single Postgres: no extra services, and no custom image to maintain. +**The official PostgreSQL image with 80+ extensions preinstalled**: pgvector, PostGIS, pgmq, TimescaleDB, pg_cron, pg_duckdb, Apache AGE, pg_graphql and [many more](./.docs/included-extensions.md). Vector search, geospatial queries, queues, time series and analytics in a single Postgres: no extra services, and no custom image to maintain. -- **Drop-in replacement** for `postgres`. Same entrypoint, same environment variables, same everything; only the extensions are added. -- **PostgreSQL 14 to 18**, on `linux/amd64` and `linux/arm64`. Both architectures are tested before every publish. +- **Drop-in replacement** for `postgres`. Same entrypoint, same environment variables, same defaults; nothing is enabled until you run `CREATE EXTENSION`. +- **PostgreSQL 14 to 18**, on `linux/amd64` and `linux/arm64`. Every extension has a functional test, run on both architectures before every publish. - **Immutable versioned tags** (`18-v1.0.0`), so an update never reaches you unless you ask for it. - **Rebuilt weekly** with the base image's security fixes. @@ -73,14 +73,14 @@ SELECT msg_id, message FROM pgmq.read(queue_name => 'embeddings', vt => 30, qty If you only need one extension, its own image is a great choice. This one is for when you need several: -| | `postgres` | `pgvector/pgvector` | `postgis/postgis` | **`ruxwez/postgres`** | -|---|:-:|:-:|:-:|:-:| -| pgvector | — | ✅ | — | ✅ | -| PostGIS | — | — | ✅ | ✅ | -| pgmq | — | — | — | ✅ | -| `linux/arm64` (Apple Silicon, Graviton, Raspberry Pi) | ✅ | ✅ | — | ✅ | +| | `postgres` | `pgvector/pgvector` | `postgis/postgis` | `pglayers-full` | **`ruxwez/postgres`** | +|---|:-:|:-:|:-:|:-:|:-:| +| Extensions beyond contrib | — | 1 | 1 | 80+ | 80+ | +| PostgreSQL 14–16 | ✅ | ✅ | ✅ | — | ✅ | +| `linux/arm64` (Apple Silicon, Graviton, Raspberry Pi) | ✅ | ✅ | — | ✅ | ✅ | +| Same defaults as the official image | ✅ | ✅ | ✅ | — (preloads libraries) | ✅ | -Checked on the PostgreSQL 18 tags of each image (`pg18`, `18-3.6`) in October 2026. +Checked in October 2026 on the PostgreSQL 14 and 18 tags of each image. [pglayers](https://github.com/pglayers/pglayers) also publishes each extension as a separate layer, which suits you if you want to assemble your own image. ## Tags @@ -95,22 +95,32 @@ Pick a tag depending on how much change you're willing to accept: The `vX.Y.Z` version belongs to this project, not to PostgreSQL: it changes when an extension changes. Each [release](https://github.com/ruxwez/postgres/releases) lists the exact PostgreSQL and extension versions of every image. See [Releases](./.docs/releases.md) for what each kind of bump means. -The versions are also in the image labels: +Each image also lists what it contains: ```sh -docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' +docker run --rm ruxwez/postgres:18-v1 cat /usr/share/postgres-extensor/extensions.txt ``` For byte-for-byte reproducibility, pin a digest: `ruxwez/postgres@sha256:…`. ### Included extensions -| Extension | Version | Source | -|-----------|---------|--------| -| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Built from source | -| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Built from source | -| [PostGIS](https://postgis.net/) | 3.6.4 | PGDG apt package (`postgresql-N-postgis-3`) | -| [contrib](https://www.postgresql.org/docs/current/contrib.html) | — | Included in the base image (`pg_stat_statements`, `pg_trgm`, …) | +| Category | Extensions | +|---|---| +| AI and vectors | pgvector, pgvectorscale | +| Geospatial | PostGIS, pgRouting, H3, pgSphere | +| Search and text | pg_textsearch (BM25), RUM, pg_bigm, pg_similarity, pgpcre, re2, icu_ext | +| Time series and analytics | TimescaleDB, pg_duckdb, pg_ivm, pg_incremental, hll, tdigest, roaringbitmap, … | +| Queues and jobs | pgmq, pg_cron, pg_background, pg_partman | +| JSON, documents and APIs | DocumentDB, pg_graphql, pg_jsonschema, jsquery, http, pg_net, pgjwt, … | +| Data types | Apache AGE (graphs), pg_uuidv7, pg_hashids, ip4r, semver, periods, temporal_tables, … | +| Replication and CDC | pglogical, wal2json, pg_failover_slots | +| Monitoring and performance | pg_stat_monitor, pg_stat_kcache, pg_qualstats, hypopg, pg_hint_plan, pg_repack, pg_squeeze, … | +| Security | pgaudit, PostgreSQL Anonymizer, pgsodium, credcheck, set_user, … | +| Foreign data | Supabase Wrappers, pg_clickhouse, tds_fdw | +| Development and compatibility | pgTAP, plpgsql_check, plprofiler, PLJS, orafce, pgtt, db2fce | + +Plus all of [contrib](https://www.postgresql.org/docs/current/contrib.html). See the [full list](./.docs/included-extensions.md) for links, which ones need `shared_preload_libraries`, and known caveats. ## Use it in tests and CI @@ -153,9 +163,11 @@ with PostgresContainer("ruxwez/postgres:18-v1") as postgres: Everything is in two files: -- [`Dockerfile`](./Dockerfile): starts from `postgres:N`, installs the apt-packaged extensions, and copies in the ones compiled in separate build stages. Compilers and headers never reach the final image. +- [`Dockerfile`](./Dockerfile): starts from `postgres:N`, installs the PGDG packages listed in [`extensions/pgdg.txt`](./extensions/pgdg.txt), and copies in the extensions that come from the authors' releases or are compiled in separate build stages. Compilers and headers never reach the final image. - [`docker-bake.hcl`](./docker-bake.hcl): the list of PostgreSQL versions and their tags. CI reads it too. +Each extension has a functional test in `extensions//test.sql`. + To build and test locally (requires Docker with Buildx): ```sh @@ -166,6 +178,7 @@ make build-all # build every version ## Documentation +- [Included extensions](./.docs/included-extensions.md): the full list, with setup notes. - [Extensions](./.docs/extensions.md): adding or upgrading an extension or a PostgreSQL version. - [Tests](./.docs/tests.md): how the tests and CI work. - [Releases](./.docs/releases.md): versioning and how to publish a release. diff --git a/extensions/age/test.sql b/extensions/age/test.sql new file mode 100644 index 0000000..a896a29 --- /dev/null +++ b/extensions/age/test.sql @@ -0,0 +1,14 @@ +CREATE EXTENSION age; +LOAD 'age'; +SET search_path = ag_catalog, "$user", public; + +SELECT create_graph('people'); +SELECT * FROM cypher('people', $$ + CREATE (:Person {name: 'Alice'})-[:KNOWS]->(:Person {name: 'Bob'}) +$$) AS (v agtype); + +SELECT assert_true( + (SELECT name::text FROM cypher('people', $$ + MATCH (:Person {name: 'Alice'})-[:KNOWS]->(p) RETURN p.name + $$) AS (name agtype)) = 'Bob', + 'age: Alice should know Bob'); diff --git a/extensions/anon/server.conf b/extensions/anon/server.conf new file mode 100644 index 0000000..d3fe1d6 --- /dev/null +++ b/extensions/anon/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'anon' diff --git a/extensions/anon/test.sql b/extensions/anon/test.sql new file mode 100644 index 0000000..bd7b53e --- /dev/null +++ b/extensions/anon/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION anon; + +SELECT assert_true(anon.partial('abcdefgh', 2, 'xxxx', 2) = 'abxxxxgh', 'anon: partial()'); diff --git a/extensions/credcheck/server.conf b/extensions/credcheck/server.conf new file mode 100644 index 0000000..6e12b8a --- /dev/null +++ b/extensions/credcheck/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'credcheck' diff --git a/extensions/credcheck/test.sql b/extensions/credcheck/test.sql new file mode 100644 index 0000000..7ee3b09 --- /dev/null +++ b/extensions/credcheck/test.sql @@ -0,0 +1,14 @@ +-- credcheck is a hook loaded with shared_preload_libraries, not an extension. +SET credcheck.password_min_length = 12; + +DO $$ +DECLARE + rejected boolean := false; +BEGIN + BEGIN + CREATE ROLE credcheck_weak LOGIN PASSWORD 'short'; + EXCEPTION WHEN OTHERS THEN + rejected := true; + END; + PERFORM assert_true(rejected, 'credcheck: a short password should be rejected'); +END $$; diff --git a/extensions/db2fce/test.sql b/extensions/db2fce/test.sql new file mode 100644 index 0000000..d8dd0a8 --- /dev/null +++ b/extensions/db2fce/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION db2fce; + +SELECT assert_true(db2.day(DATE '2020-03-15') = 15, 'db2fce: day()'); +SELECT assert_true(db2.months_between(DATE '2020-03-15', DATE '2020-01-15') = 2, 'db2fce: months_between()'); diff --git a/extensions/documentdb/database b/extensions/documentdb/database new file mode 100644 index 0000000..f134994 --- /dev/null +++ b/extensions/documentdb/database @@ -0,0 +1 @@ +postgres diff --git a/extensions/documentdb/pg-versions b/extensions/documentdb/pg-versions new file mode 100644 index 0000000..4b20824 --- /dev/null +++ b/extensions/documentdb/pg-versions @@ -0,0 +1 @@ +15 16 17 18 diff --git a/extensions/documentdb/server.conf b/extensions/documentdb/server.conf new file mode 100644 index 0000000..1873a46 --- /dev/null +++ b/extensions/documentdb/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_cron, pg_documentdb_core, pg_documentdb' diff --git a/extensions/documentdb/test.sql b/extensions/documentdb/test.sql new file mode 100644 index 0000000..cc30ad3 --- /dev/null +++ b/extensions/documentdb/test.sql @@ -0,0 +1,12 @@ +-- Runs in the pg_cron database (see the database file): documentdb needs pg_cron. +CREATE EXTENSION IF NOT EXISTS documentdb CASCADE; + +SELECT documentdb_api.insert_one('library', 'books', '{"title": "Dune"}'); +SELECT documentdb_api.insert_one('library', 'books', '{"title": "Neuromancer"}'); + +SELECT assert_true( + (SELECT documentdb_core.bson_to_json_string(document) FROM documentdb_api.count_query('library', '{"count": "books"}'))::text + LIKE '%"n" : { "$numberInt" : "2" }%', + 'documentdb: count documents in a collection'); + +SELECT documentdb_api.drop_collection('library', 'books'); diff --git a/extensions/extra_window_functions/test.sql b/extensions/extra_window_functions/test.sql new file mode 100644 index 0000000..752a960 --- /dev/null +++ b/extensions/extra_window_functions/test.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION extra_window_functions; + +SELECT assert_true( + (SELECT last_value_ignore_nulls(x) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + FROM (VALUES (1, 10), (2, NULL::int)) t(id, x) + ORDER BY id DESC LIMIT 1) = 10, + 'extra_window_functions: last_value_ignore_nulls()'); diff --git a/extensions/fetch-deb.sh b/extensions/fetch-deb.sh new file mode 100755 index 0000000..e1aa33e --- /dev/null +++ b/extensions/fetch-deb.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# +# Usage: fetch-deb.sh URL +# +# Downloads a prebuilt extension package (.deb, or a .zip containing one) and +# copies its PostgreSQL files for this major into /out. The package is only +# unpacked, not installed, so its dependency metadata doesn't matter. + +set -eu + +url="$1" +work="$(mktemp -d)" +mkdir -p /out + +curl -fsSL --retry 5 --retry-all-errors -o "$work/pkg" "$url" +case "$url" in + *.zip) + unzip -q "$work/pkg" -d "$work/zip" + deb="$(find "$work/zip" -name '*.deb' ! -name '*dbgsym*' | head -n1)" ;; + *) + deb="$work/pkg" ;; +esac + +dpkg-deb -x "$deb" "$work/root" +for dir in "usr/lib/postgresql/$PG_MAJOR" "usr/share/postgresql/$PG_MAJOR"; do + if [ -d "$work/root/$dir" ]; then + mkdir -p "/out/$(dirname "$dir")" + # -L: some packages ship symlinks into other directories of the package. + cp -RL "$work/root/$dir" "/out/$(dirname "$dir")/" + fi +done +rm -rf "$work" diff --git a/extensions/first_last_agg/test.sql b/extensions/first_last_agg/test.sql new file mode 100644 index 0000000..9bd3055 --- /dev/null +++ b/extensions/first_last_agg/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION first_last_agg; + +SELECT assert_true((SELECT first(x ORDER BY x) FROM (VALUES (30), (10), (20)) t(x)) = 10, 'first_last_agg: first()'); +SELECT assert_true((SELECT last(x ORDER BY x) FROM (VALUES (30), (10), (20)) t(x)) = 30, 'first_last_agg: last()'); diff --git a/extensions/h3/test.sql b/extensions/h3/test.sql new file mode 100644 index 0000000..2ee60f0 --- /dev/null +++ b/extensions/h3/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION h3; +CREATE EXTENSION h3_postgis CASCADE; + +SELECT assert_true(h3_get_resolution(h3_lat_lng_to_cell(point(2.3522, 48.8566), 7)) = 7, 'h3: cell resolution'); +SELECT assert_true(h3_lat_lng_to_cell(ST_Point(2.3522, 48.8566), 7) = h3_lat_lng_to_cell(point(2.3522, 48.8566), 7), 'h3_postgis: same cell from a geometry'); diff --git a/extensions/hll/test.sql b/extensions/hll/test.sql new file mode 100644 index 0000000..03bdd49 --- /dev/null +++ b/extensions/hll/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION hll; + +SELECT assert_true( + abs(hll_cardinality((SELECT hll_add_agg(hll_hash_integer(g)) FROM generate_series(1, 1000) g)) - 1000) < 50, + 'hll: cardinality estimate within 5%'); diff --git a/extensions/http/test.sql b/extensions/http/test.sql new file mode 100644 index 0000000..6ae21f0 --- /dev/null +++ b/extensions/http/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION http; + +-- No network calls in tests: check the request and response types. +SELECT assert_true((http_header('Accept', 'application/json')).field = 'Accept', 'http: http_header()'); +SELECT assert_true(to_regprocedure('http_get(character varying)') IS NOT NULL, 'http: http_get()'); diff --git a/extensions/hypopg/test.sql b/extensions/hypopg/test.sql new file mode 100644 index 0000000..0512553 --- /dev/null +++ b/extensions/hypopg/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION hypopg; + +CREATE TABLE items AS SELECT g AS id FROM generate_series(1, 10000) g; +ANALYZE items; +SELECT * FROM hypopg_create_index('CREATE INDEX ON items (id)'); + +SELECT assert_true((SELECT count(*) FROM hypopg()) = 1, 'hypopg: hypothetical index'); +SELECT assert_true((SELECT hypopg_relation_size(indexrelid) FROM hypopg()) > 0, 'hypopg: estimated size'); diff --git a/extensions/icu_ext/test.sql b/extensions/icu_ext/test.sql new file mode 100644 index 0000000..42ed3ac --- /dev/null +++ b/extensions/icu_ext/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION icu_ext; + +SELECT assert_true(icu_char_name('A') = 'LATIN CAPITAL LETTER A', 'icu_ext: icu_char_name()'); +SELECT assert_true(icu_transform('Æ', 'Latin-ASCII') = 'AE', 'icu_ext: icu_transform()'); diff --git a/extensions/ip4r/test.sql b/extensions/ip4r/test.sql new file mode 100644 index 0000000..0e776a4 --- /dev/null +++ b/extensions/ip4r/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION ip4r; + +SELECT assert_true('10.0.0.0/8'::ip4r >>= '10.1.2.3'::ip4, 'ip4r: range contains address'); + +CREATE TABLE networks (network ip4r); +INSERT INTO networks VALUES ('10.0.0.0/24'), ('172.16.0.0/16'), ('192.168.1.0/24'); +CREATE INDEX ON networks USING gist (network); +SELECT assert_true((SELECT count(*) FROM networks WHERE network >>= '172.16.5.10') = 1, 'ip4r: indexed lookup'); diff --git a/extensions/jsquery/test.sql b/extensions/jsquery/test.sql new file mode 100644 index 0000000..87adca3 --- /dev/null +++ b/extensions/jsquery/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION jsquery; + +SELECT assert_true('{"a": {"b": 3}}'::jsonb @@ 'a.b = 3'::jsquery, 'jsquery: nested match'); +SELECT assert_true(NOT ('{"a": 2}'::jsonb @@ 'a = 1'::jsquery), 'jsquery: non-match'); diff --git a/extensions/list-extensions.sh b/extensions/list-extensions.sh new file mode 100755 index 0000000..11cd68d --- /dev/null +++ b/extensions/list-extensions.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# +# Prints "name version" for every extension available to PostgreSQL, read from +# the .control files (default_version may be quoted or not). + +set -eu + +for control in /usr/share/postgresql/"$PG_MAJOR"/extension/*.control; do + version="$(sed -n "s/^[[:space:]]*default_version[[:space:]]*=[[:space:]]*['\"]\{0,1\}\([^'\"[:space:]]*\).*/\1/p" "$control")" + echo "$(basename "$control" .control) ${version:--}" +done | sort diff --git a/extensions/orafce/test.sql b/extensions/orafce/test.sql new file mode 100644 index 0000000..b5112a5 --- /dev/null +++ b/extensions/orafce/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION orafce; + +SELECT assert_true(oracle.nvl(NULL::text, 'fallback') = 'fallback', 'orafce: nvl()'); +SELECT assert_true(oracle.decode(2, 1, 'one', 2, 'two') = 'two', 'orafce: decode()'); +SELECT assert_true(oracle.last_day('2024-02-15'::date) = '2024-02-29'::date, 'orafce: last_day()'); diff --git a/extensions/periods/test.sql b/extensions/periods/test.sql new file mode 100644 index 0000000..d863733 --- /dev/null +++ b/extensions/periods/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION periods CASCADE; + +CREATE TABLE prices (id int, valid_from date, valid_to date); +SELECT periods.add_period('prices', 'validity', 'valid_from', 'valid_to'); + +SELECT assert_true( + (SELECT count(*) FROM periods.periods WHERE table_name = 'prices'::regclass AND period_name = 'validity') = 1, + 'periods: add_period()'); diff --git a/extensions/pg_background/server.conf b/extensions/pg_background/server.conf new file mode 100644 index 0000000..8366e5a --- /dev/null +++ b/extensions/pg_background/server.conf @@ -0,0 +1,3 @@ +# The tests preload many extensions that start background workers, and the +# default of 8 leaves no free slot for the ones pg_background launches. +max_worker_processes = 64 diff --git a/extensions/pg_background/test.sql b/extensions/pg_background/test.sql new file mode 100644 index 0000000..7f2ba6b --- /dev/null +++ b/extensions/pg_background/test.sql @@ -0,0 +1,6 @@ +CREATE EXTENSION pg_background; + +SELECT assert_true( + (WITH h AS (SELECT pg_background_launch('SELECT 42') AS handle) + SELECT v FROM h, pg_background_result((h.handle).pid, (h.handle).cookie) AS r(v int)) = 42, + 'pg_background: result of a background query'); diff --git a/extensions/pg_bigm/test.sql b/extensions/pg_bigm/test.sql new file mode 100644 index 0000000..02faf0e --- /dev/null +++ b/extensions/pg_bigm/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION pg_bigm; + +CREATE TABLE docs (content text); +INSERT INTO docs VALUES ('PostgreSQL'), ('full text search'), ('bigram index'); +CREATE INDEX ON docs USING gin (content gin_bigm_ops); + +SELECT assert_true((SELECT count(*) FROM docs WHERE content LIKE '%text%') = 1, 'pg_bigm: LIKE search'); +SELECT assert_true(bigm_similarity('PostgreSQL', 'PostgreSQ') > 0.5, 'pg_bigm: bigm_similarity()'); diff --git a/extensions/pg_clickhouse/test.sql b/extensions/pg_clickhouse/test.sql new file mode 100644 index 0000000..a603ecf --- /dev/null +++ b/extensions/pg_clickhouse/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION pg_clickhouse; + +-- No ClickHouse server in tests: check the wrapper and that a server can be defined. +CREATE SERVER clickhouse FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS (host 'localhost', port '8123'); +SELECT assert_true((SELECT count(*) FROM pg_foreign_server WHERE srvname = 'clickhouse') = 1, 'pg_clickhouse: foreign server'); diff --git a/extensions/pg_cron/database b/extensions/pg_cron/database new file mode 100644 index 0000000..f134994 --- /dev/null +++ b/extensions/pg_cron/database @@ -0,0 +1 @@ +postgres diff --git a/extensions/pg_cron/server.conf b/extensions/pg_cron/server.conf new file mode 100644 index 0000000..beba6db --- /dev/null +++ b/extensions/pg_cron/server.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = 'pg_cron' +cron.database_name = 'postgres' diff --git a/extensions/pg_cron/test.sql b/extensions/pg_cron/test.sql new file mode 100644 index 0000000..55b51a0 --- /dev/null +++ b/extensions/pg_cron/test.sql @@ -0,0 +1,6 @@ +-- Runs in the database set by cron.database_name (see the database file). +CREATE EXTENSION IF NOT EXISTS pg_cron; + +SELECT assert_true(cron.schedule('test-job', '*/5 * * * *', 'SELECT 1') > 0, 'pg_cron: schedule()'); +SELECT assert_true((SELECT count(*) FROM cron.job WHERE jobname = 'test-job') = 1, 'pg_cron: job listed'); +SELECT assert_true(cron.unschedule('test-job'), 'pg_cron: unschedule()'); diff --git a/extensions/pg_csv/test.sql b/extensions/pg_csv/test.sql new file mode 100644 index 0000000..1513562 --- /dev/null +++ b/extensions/pg_csv/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_csv; + +SELECT assert_true((SELECT csv_agg(t) FROM (VALUES (1, 'a'), (2, 'b')) t(id, name)) LIKE '%1,a%2,b%', 'pg_csv: csv_agg()'); diff --git a/extensions/pg_dirtyread/test.sql b/extensions/pg_dirtyread/test.sql new file mode 100644 index 0000000..abd7c60 --- /dev/null +++ b/extensions/pg_dirtyread/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION pg_dirtyread; + +CREATE TABLE notes (id int) WITH (autovacuum_enabled = false); +INSERT INTO notes VALUES (1), (2); +DELETE FROM notes WHERE id = 2; + +SELECT assert_true((SELECT count(*) FROM notes) = 1, 'pg_dirtyread: row deleted'); +SELECT assert_true((SELECT count(*) FROM pg_dirtyread('notes') AS t(id int)) = 2, 'pg_dirtyread: deleted row still readable'); diff --git a/extensions/pg_duckdb/isolated b/extensions/pg_duckdb/isolated new file mode 100644 index 0000000..4636e41 --- /dev/null +++ b/extensions/pg_duckdb/isolated @@ -0,0 +1,3 @@ +pg_duckdb aborts the server when it's preloaded together with anon: anon's +Rust (pgrx) hooks panic when DuckDB calls them from its worker threads. +Tested on its own server. diff --git a/extensions/pg_duckdb/server.conf b/extensions/pg_duckdb/server.conf new file mode 100644 index 0000000..e5cb9be --- /dev/null +++ b/extensions/pg_duckdb/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_duckdb' diff --git a/extensions/pg_duckdb/test.sql b/extensions/pg_duckdb/test.sql new file mode 100644 index 0000000..e493fdd --- /dev/null +++ b/extensions/pg_duckdb/test.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION pg_duckdb; + +-- Queries that touch DuckDB run entirely in DuckDB, which doesn't know +-- assert_true(): fetch the results first, then check them. +SELECT count(*) AS rows FROM duckdb.query($$ SELECT 42 AS answer $$) \gset +SELECT assert_true(:rows = 1, 'pg_duckdb: duckdb.query()'); + +CREATE TABLE sales (amount int); +INSERT INTO sales SELECT g FROM generate_series(1, 100) g; +SET duckdb.force_execution = true; +SELECT sum(amount) AS total FROM sales \gset +RESET duckdb.force_execution; +SELECT assert_true(:total = 5050, 'pg_duckdb: aggregate executed by DuckDB'); diff --git a/extensions/pg_failover_slots/server.conf b/extensions/pg_failover_slots/server.conf new file mode 100644 index 0000000..ba11d01 --- /dev/null +++ b/extensions/pg_failover_slots/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_failover_slots' diff --git a/extensions/pg_failover_slots/test.sql b/extensions/pg_failover_slots/test.sql new file mode 100644 index 0000000..a4d4c68 --- /dev/null +++ b/extensions/pg_failover_slots/test.sql @@ -0,0 +1,2 @@ +-- A background worker loaded with shared_preload_libraries, not an extension. +SELECT assert_true(current_setting('pg_failover_slots.synchronize_slot_names', true) IS NOT NULL, 'pg_failover_slots: settings registered'); diff --git a/extensions/pg_graphql/test.sql b/extensions/pg_graphql/test.sql new file mode 100644 index 0000000..c06bf75 --- /dev/null +++ b/extensions/pg_graphql/test.sql @@ -0,0 +1,10 @@ +CREATE EXTENSION pg_graphql; + +CREATE TABLE book (id int PRIMARY KEY, title text); +INSERT INTO book VALUES (1, 'Dune'); + +SELECT assert_true(graphql.resolve($$ { __typename } $$) -> 'data' ->> '__typename' = 'Query', 'pg_graphql: resolve()'); +SELECT assert_true( + graphql.resolve($$ { bookCollection { edges { node { title } } } } $$) + #>> '{data,bookCollection,edges,0,node,title}' = 'Dune', + 'pg_graphql: query a table'); diff --git a/extensions/pg_hashids/test.sql b/extensions/pg_hashids/test.sql new file mode 100644 index 0000000..b051efb --- /dev/null +++ b/extensions/pg_hashids/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_hashids; + +SELECT assert_true((id_decode(id_encode(42, 'salt'), 'salt'))[1] = 42, 'pg_hashids: encode/decode round trip'); +SELECT assert_true(id_encode(42, 'salt') <> id_encode(42, 'pepper'), 'pg_hashids: salt changes the hash'); diff --git a/extensions/pg_hint_plan/server.conf b/extensions/pg_hint_plan/server.conf new file mode 100644 index 0000000..1b82375 --- /dev/null +++ b/extensions/pg_hint_plan/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_hint_plan' diff --git a/extensions/pg_hint_plan/test.sql b/extensions/pg_hint_plan/test.sql new file mode 100644 index 0000000..cdb1712 --- /dev/null +++ b/extensions/pg_hint_plan/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION pg_hint_plan; + +SET pg_hint_plan.enable_hint = on; +SELECT assert_true(current_setting('pg_hint_plan.enable_hint') = 'on', 'pg_hint_plan: settings registered'); +SELECT assert_true(to_regclass('hint_plan.hints') IS NOT NULL, 'pg_hint_plan: hint table'); diff --git a/extensions/pg_incremental/database b/extensions/pg_incremental/database new file mode 100644 index 0000000..f134994 --- /dev/null +++ b/extensions/pg_incremental/database @@ -0,0 +1 @@ +postgres diff --git a/extensions/pg_incremental/pg-versions b/extensions/pg_incremental/pg-versions new file mode 100644 index 0000000..4b20824 --- /dev/null +++ b/extensions/pg_incremental/pg-versions @@ -0,0 +1 @@ +15 16 17 18 diff --git a/extensions/pg_incremental/test.sql b/extensions/pg_incremental/test.sql new file mode 100644 index 0000000..3a6a875 --- /dev/null +++ b/extensions/pg_incremental/test.sql @@ -0,0 +1,22 @@ +-- Runs in the pg_cron database (see the database file): pipelines are pg_cron jobs. +CREATE EXTENSION IF NOT EXISTS pg_incremental CASCADE; +SET client_min_messages = warning; + +CREATE TABLE incremental_events (id bigint GENERATED ALWAYS AS IDENTITY, client_id int NOT NULL); +CREATE TABLE incremental_counts (client_id int PRIMARY KEY, events bigint NOT NULL); +INSERT INTO incremental_events (client_id) SELECT g % 10 FROM generate_series(1, 100) g; + +-- Processes the existing rows right away. +SELECT incremental.create_sequence_pipeline('count-events', 'incremental_events', $$ + INSERT INTO incremental_counts + SELECT client_id, count(*) FROM incremental_events WHERE id BETWEEN $1 AND $2 GROUP BY client_id + ON CONFLICT (client_id) DO UPDATE SET events = incremental_counts.events + excluded.events +$$, schedule := '0 0 1 1 *'); +SELECT assert_true((SELECT sum(events) FROM incremental_counts) = 100, 'pg_incremental: initial run'); + +INSERT INTO incremental_events (client_id) SELECT g % 10 FROM generate_series(1, 50) g; +CALL incremental.execute_pipeline('count-events'); +SELECT assert_true((SELECT sum(events) FROM incremental_counts) = 150, 'pg_incremental: only new rows processed'); + +SELECT incremental.drop_pipeline('count-events'); +DROP TABLE incremental_events, incremental_counts; diff --git a/extensions/pg_ivm/test.sql b/extensions/pg_ivm/test.sql new file mode 100644 index 0000000..61bb85c --- /dev/null +++ b/extensions/pg_ivm/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION pg_ivm; + +CREATE TABLE orders (product text, amount int); +INSERT INTO orders VALUES ('A', 100), ('B', 200); +SELECT pgivm.create_immv('order_totals', 'SELECT product, sum(amount) AS total FROM orders GROUP BY product'); +INSERT INTO orders VALUES ('A', 50); + +SELECT assert_true((SELECT total FROM order_totals WHERE product = 'A') = 150, 'pg_ivm: view updated incrementally'); diff --git a/extensions/pg_jsonschema/test.sql b/extensions/pg_jsonschema/test.sql new file mode 100644 index 0000000..273eae2 --- /dev/null +++ b/extensions/pg_jsonschema/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_jsonschema; + +SELECT assert_true(json_matches_schema('{"type": "object", "properties": {"age": {"type": "integer"}}}', '{"age": 30}'), 'pg_jsonschema: valid document'); +SELECT assert_true(NOT json_matches_schema('{"type": "object", "properties": {"age": {"type": "integer"}}}', '{"age": "thirty"}'), 'pg_jsonschema: invalid document'); diff --git a/extensions/pg_net/database b/extensions/pg_net/database new file mode 100644 index 0000000..f134994 --- /dev/null +++ b/extensions/pg_net/database @@ -0,0 +1 @@ +postgres diff --git a/extensions/pg_net/server.conf b/extensions/pg_net/server.conf new file mode 100644 index 0000000..24f43c8 --- /dev/null +++ b/extensions/pg_net/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_net' diff --git a/extensions/pg_net/test.sql b/extensions/pg_net/test.sql new file mode 100644 index 0000000..9304863 --- /dev/null +++ b/extensions/pg_net/test.sql @@ -0,0 +1,6 @@ +-- Runs in pg_net.database_name, where its background worker looks for it. +CREATE EXTENSION IF NOT EXISTS pg_net; + +-- No network calls in tests: check the API. +SELECT assert_true(to_regproc('net.http_get') IS NOT NULL, 'pg_net: net.http_get()'); +SELECT assert_true(to_regclass('net.http_request_queue') IS NOT NULL, 'pg_net: request queue'); diff --git a/extensions/pg_partman/server.conf b/extensions/pg_partman/server.conf new file mode 100644 index 0000000..06bfcac --- /dev/null +++ b/extensions/pg_partman/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_partman_bgw' diff --git a/extensions/pg_partman/test.sql b/extensions/pg_partman/test.sql new file mode 100644 index 0000000..3637bfa --- /dev/null +++ b/extensions/pg_partman/test.sql @@ -0,0 +1,7 @@ +CREATE SCHEMA partman; +CREATE EXTENSION pg_partman SCHEMA partman; + +CREATE TABLE events (id bigint, created_at timestamptz NOT NULL) PARTITION BY RANGE (created_at); +SELECT partman.create_parent('public.events', 'created_at', '1 day'); + +SELECT assert_true((SELECT count(*) FROM pg_inherits WHERE inhparent = 'events'::regclass) > 1, 'pg_partman: partitions created'); diff --git a/extensions/pg_permissions/test.sql b/extensions/pg_permissions/test.sql new file mode 100644 index 0000000..10504be --- /dev/null +++ b/extensions/pg_permissions/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_permissions; + +CREATE TABLE secrets (id int); +SELECT assert_true((SELECT count(*) FROM table_permissions WHERE object_name = 'secrets') > 0, 'pg_permissions: table_permissions view'); diff --git a/extensions/pg_pwhash/test.sql b/extensions/pg_pwhash/test.sql new file mode 100644 index 0000000..8da90fe --- /dev/null +++ b/extensions/pg_pwhash/test.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION pg_pwhash; + +-- Like pgcrypto's crypt(): the hash itself is the salt to verify against. +SELECT pwhash_crypt('secret', pwhash_gen_salt('scrypt')) AS hash \gset +SELECT assert_true(:'hash' LIKE '$scrypt$%', 'pg_pwhash: scrypt hash'); +SELECT assert_true(pwhash_crypt('secret', :'hash') = :'hash', 'pg_pwhash: right password verifies'); +SELECT assert_true(pwhash_crypt('wrong', :'hash') <> :'hash', 'pg_pwhash: wrong password rejected'); diff --git a/extensions/pg_qualstats/server.conf b/extensions/pg_qualstats/server.conf new file mode 100644 index 0000000..e76c9a2 --- /dev/null +++ b/extensions/pg_qualstats/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_qualstats' diff --git a/extensions/pg_qualstats/test.sql b/extensions/pg_qualstats/test.sql new file mode 100644 index 0000000..403519f --- /dev/null +++ b/extensions/pg_qualstats/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_qualstats; + +SELECT assert_true(to_regclass('pg_qualstats') IS NOT NULL, 'pg_qualstats: view'); +SELECT assert_true((SELECT count(*) FROM pg_qualstats) >= 0, 'pg_qualstats: view readable'); diff --git a/extensions/pg_re2/test.sql b/extensions/pg_re2/test.sql new file mode 100644 index 0000000..081974f --- /dev/null +++ b/extensions/pg_re2/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION re2; + +SELECT assert_true(re2match('hello world', 'h.*o') AND NOT re2match('hello world', '^xyz'), 're2: re2match()'); +SELECT assert_true(re2extract('2024-08-12', '([0-9]{4})') = '2024', 're2: re2extract()'); +SELECT assert_true(re2countmatches('a1b2c3', '[0-9]') = 3, 're2: re2countmatches()'); diff --git a/extensions/pg_repack/test.sql b/extensions/pg_repack/test.sql new file mode 100644 index 0000000..8776b19 --- /dev/null +++ b/extensions/pg_repack/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_repack; + +SELECT assert_true(repack.version() LIKE 'pg_repack%', 'pg_repack: version()'); diff --git a/extensions/pg_rrule/test.sql b/extensions/pg_rrule/test.sql new file mode 100644 index 0000000..8644be7 --- /dev/null +++ b/extensions/pg_rrule/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_rrule; + +SELECT assert_true(array_length(get_occurrences('FREQ=DAILY;COUNT=3'::rrule, '2020-01-01 00:00:00'::timestamp), 1) = 3, 'pg_rrule: get_occurrences()'); diff --git a/extensions/pg_show_plans/isolated b/extensions/pg_show_plans/isolated new file mode 100644 index 0000000..cfdc656 --- /dev/null +++ b/extensions/pg_show_plans/isolated @@ -0,0 +1,3 @@ +pg_show_plans crashes the server (segfault) when it's preloaded together +with timescaledb and a row is inserted into a hypertable. Tested on its own +server. diff --git a/extensions/pg_show_plans/server.conf b/extensions/pg_show_plans/server.conf new file mode 100644 index 0000000..83514c8 --- /dev/null +++ b/extensions/pg_show_plans/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_show_plans' diff --git a/extensions/pg_show_plans/test.sql b/extensions/pg_show_plans/test.sql new file mode 100644 index 0000000..a88d96f --- /dev/null +++ b/extensions/pg_show_plans/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_show_plans; + +SELECT assert_true((SELECT count(*) FROM pg_show_plans WHERE pid = pg_backend_pid()) >= 1, 'pg_show_plans: own query plan visible'); diff --git a/extensions/pg_similarity/test.sql b/extensions/pg_similarity/test.sql new file mode 100644 index 0000000..50fe091 --- /dev/null +++ b/extensions/pg_similarity/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_similarity; + +SELECT assert_true(jaro('martha', 'marhta') > 0.9, 'pg_similarity: jaro()'); +SELECT assert_true(lev('hello', 'hello') = 1, 'pg_similarity: lev() of identical strings'); diff --git a/extensions/pg_squeeze/server.conf b/extensions/pg_squeeze/server.conf new file mode 100644 index 0000000..eb232ae --- /dev/null +++ b/extensions/pg_squeeze/server.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = 'pg_squeeze' +wal_level = logical diff --git a/extensions/pg_squeeze/test.sql b/extensions/pg_squeeze/test.sql new file mode 100644 index 0000000..3bb869c --- /dev/null +++ b/extensions/pg_squeeze/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_squeeze; + +SELECT assert_true(to_regclass('squeeze.tables') IS NOT NULL, 'pg_squeeze: squeeze.tables'); diff --git a/extensions/pg_stat_kcache/server.conf b/extensions/pg_stat_kcache/server.conf new file mode 100644 index 0000000..ed11a82 --- /dev/null +++ b/extensions/pg_stat_kcache/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_stat_statements, pg_stat_kcache' diff --git a/extensions/pg_stat_kcache/test.sql b/extensions/pg_stat_kcache/test.sql new file mode 100644 index 0000000..dd462f9 --- /dev/null +++ b/extensions/pg_stat_kcache/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION pg_stat_statements; +CREATE EXTENSION pg_stat_kcache; + +SELECT sum(length(md5(g::text))) FROM generate_series(1, 10000) g; +SELECT assert_true((SELECT count(*) FROM pg_stat_kcache()) > 0, 'pg_stat_kcache: statistics collected'); diff --git a/extensions/pg_stat_monitor/server.conf b/extensions/pg_stat_monitor/server.conf new file mode 100644 index 0000000..0379617 --- /dev/null +++ b/extensions/pg_stat_monitor/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_stat_monitor' diff --git a/extensions/pg_stat_monitor/test.sql b/extensions/pg_stat_monitor/test.sql new file mode 100644 index 0000000..9355e06 --- /dev/null +++ b/extensions/pg_stat_monitor/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_stat_monitor; + +SELECT 1 + 1; +SELECT assert_true((SELECT count(*) FROM pg_stat_monitor) >= 0, 'pg_stat_monitor: view readable'); diff --git a/extensions/pg_statviz/test.sql b/extensions/pg_statviz/test.sql new file mode 100644 index 0000000..2858259 --- /dev/null +++ b/extensions/pg_statviz/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_statviz; + +SELECT pgstatviz.snapshot(); +SELECT assert_true((SELECT count(*) FROM pgstatviz.snapshots) > 0, 'pg_statviz: snapshot()'); diff --git a/extensions/pg_textsearch/pg-versions b/extensions/pg_textsearch/pg-versions new file mode 100644 index 0000000..e126066 --- /dev/null +++ b/extensions/pg_textsearch/pg-versions @@ -0,0 +1 @@ +17 18 diff --git a/extensions/pg_textsearch/server.conf b/extensions/pg_textsearch/server.conf new file mode 100644 index 0000000..8fc808a --- /dev/null +++ b/extensions/pg_textsearch/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_textsearch' diff --git a/extensions/pg_textsearch/test.sql b/extensions/pg_textsearch/test.sql new file mode 100644 index 0000000..3b75405 --- /dev/null +++ b/extensions/pg_textsearch/test.sql @@ -0,0 +1,12 @@ +CREATE EXTENSION pg_textsearch; + +CREATE TABLE articles (id serial PRIMARY KEY, content text NOT NULL); +INSERT INTO articles (content) VALUES + ('database query optimization techniques'), + ('machine learning neural network'), + ('distributed systems consensus protocol'); +CREATE INDEX articles_bm25 ON articles USING bm25 (content) WITH (text_config = 'english'); + +SELECT assert_true( + (SELECT content FROM articles ORDER BY content <@> to_bm25query('database optimization', 'articles_bm25') LIMIT 1) LIKE '%database%', + 'pg_textsearch: BM25 ranking'); diff --git a/extensions/pg_uuidv7/test.sql b/extensions/pg_uuidv7/test.sql new file mode 100644 index 0000000..672a408 --- /dev/null +++ b/extensions/pg_uuidv7/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pg_uuidv7; + +SELECT assert_true(uuid_v7_to_timestamptz(uuid_generate_v7()) > now() - interval '1 minute', 'pg_uuidv7: timestamp in the UUID'); +SELECT assert_true(uuid_generate_v7() > uuid_timestamptz_to_v7('2020-01-01'), 'pg_uuidv7: time ordered'); diff --git a/extensions/pg_wait_sampling/server.conf b/extensions/pg_wait_sampling/server.conf new file mode 100644 index 0000000..54c013d --- /dev/null +++ b/extensions/pg_wait_sampling/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pg_wait_sampling' diff --git a/extensions/pg_wait_sampling/test.sql b/extensions/pg_wait_sampling/test.sql new file mode 100644 index 0000000..3a79cf3 --- /dev/null +++ b/extensions/pg_wait_sampling/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_wait_sampling; + +SELECT assert_true((SELECT count(*) FROM pg_wait_sampling_profile) >= 0, 'pg_wait_sampling: profile readable'); diff --git a/extensions/pgaudit/server.conf b/extensions/pgaudit/server.conf new file mode 100644 index 0000000..47329c4 --- /dev/null +++ b/extensions/pgaudit/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'pgaudit' diff --git a/extensions/pgaudit/test.sql b/extensions/pgaudit/test.sql new file mode 100644 index 0000000..7225001 --- /dev/null +++ b/extensions/pgaudit/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pgaudit; + +SET pgaudit.log = 'ddl'; +SELECT assert_true(current_setting('pgaudit.log') = 'ddl', 'pgaudit: settings registered'); diff --git a/extensions/pgdg.txt b/extensions/pgdg.txt new file mode 100755 index 0000000..6340b2b --- /dev/null +++ b/extensions/pgdg.txt @@ -0,0 +1,103 @@ +# Extensions installed from the PGDG apt repository (apt.postgresql.org) as +# postgresql--, pinned to an upstream version. The Debian +# revision is matched with "*" (3.6.4 matches 3.6.4+dfsg-2.pgdg13+1). +# +# same version on every PostgreSQL major +# / one line per major, for packages whose +# version follows the PostgreSQL major or +# that don't exist for every major +# +# Renovate keeps these up to date. To list what PGDG has for a package: +# docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-' + +credcheck 5.0 +cron 1.6.8 +db2fce 0.0.17 +dirtyread 2.8 +extra-window-functions 2.0 +first-last-agg 0.1.4 +h3 4.2.3 +hll 2.21 +http 1.7.2 +hypopg 1.4.3 +icu-ext 1.11.0 +ip4r 2.4.3 +jsquery 1.2 +orafce 4.16.13 +partman 5.5.0 +periods 1.2.3 +pg-background 2.0.3 +pg-csv 1.0.2 +pg-failover-slots 1.2.1 +pg-ivm 1.15 +pg-permissions 1.4.1 +pg-pwhash 1.0 +pg-qualstats 2.1.4 +pg-rrule 0.3.0 +pg-stat-kcache 2.3.2 +pg-uuidv7 1.7.0 +pg-wait-sampling 1.1.11 +pgfincore 1.4.0 +pglogical 2.4.8 +pgnodemx 2.0.1 +pgpcre 0.20190509 +pgrouting 4.0.1 +pgsphere 1.5.2 +pgtap 1.3.4 +pgtt 4.6 +pgvector 0.8.7 +pljs 1.0.5 +plpgsql-check 2.10.13 +plprofiler 4.2.5 +postgis-3 3.6.4 +prefix 1.2.11 +prioritize 1.0.4 +rational 0.0.3 +repack 1.5.3 +roaringbitmap 1.2.0 +rum 1.3.15 +semver 0.42.0 +set-user 4.2.0 +show-plans 2.1.8 +similarity 1.0 +squeeze 1.9.4 +statviz 1.2.1 +tdigest 1.4.7 +tds-fdw 2.0.5 +timestamp9 1.4.0 +toastinfo 1.7 +wal2json 2.6 + +age/14 1.6.0 +age/15 1.8.0 +age/16 1.6.0 +age/17 1.7.0 +age/18 1.8.0 + +documentdb/15 1.0 +documentdb/16 1.0 +documentdb/17 1.0 +documentdb/18 1.0 + +pg-hint-plan/14 1.4.4 +pg-hint-plan/15 1.5.3 +pg-hint-plan/16 1.6.2 +pg-hint-plan/17 1.7.1 +pg-hint-plan/18 1.8.0 + +pg-textsearch/17 1.5.1 +pg-textsearch/18 1.5.1 + +pgaudit/14 1.6.3 +pgaudit/15 1.7.1 +pgaudit/16 16.1 +pgaudit/17 17.1 +pgaudit/18 18.0 + +# Apache-2.0 edition. Features under the Timescale License (compression, +# continuous aggregates policies, ...) are not included. +timescaledb/14 2.19.3 +timescaledb/15 2.28.3 +timescaledb/16 2.30.2 +timescaledb/17 2.30.2 +timescaledb/18 2.30.2 diff --git a/extensions/pgfincore/test.sql b/extensions/pgfincore/test.sql new file mode 100644 index 0000000..fe2edd9 --- /dev/null +++ b/extensions/pgfincore/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION pgfincore; + +CREATE TABLE cached AS SELECT g FROM generate_series(1, 1000) g; +SELECT assert_true((pgsysconf()).os_page_size > 0, 'pgfincore: pgsysconf()'); +SELECT assert_true((SELECT count(*) FROM pgfincore('cached')) > 0, 'pgfincore: pgfincore()'); diff --git a/extensions/pgjwt/test.sql b/extensions/pgjwt/test.sql new file mode 100644 index 0000000..1bff04c --- /dev/null +++ b/extensions/pgjwt/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pgjwt CASCADE; + +SELECT assert_true((SELECT valid FROM verify(sign('{"sub": "alice"}', 'secret'), 'secret')), 'pgjwt: sign/verify'); +SELECT assert_true(NOT (SELECT valid FROM verify(sign('{"sub": "alice"}', 'secret'), 'wrong')), 'pgjwt: wrong secret rejected'); diff --git a/extensions/pglogical/server.conf b/extensions/pglogical/server.conf new file mode 100644 index 0000000..e10331a --- /dev/null +++ b/extensions/pglogical/server.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = 'pglogical' +wal_level = logical diff --git a/extensions/pglogical/test.sql b/extensions/pglogical/test.sql new file mode 100644 index 0000000..c68593c --- /dev/null +++ b/extensions/pglogical/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pglogical; + +SELECT assert_true(pglogical.pglogical_version() IS NOT NULL, 'pglogical: pglogical_version()'); +SELECT assert_true(to_regproc('pglogical.create_node') IS NOT NULL, 'pglogical: create_node()'); diff --git a/extensions/pgnodemx/server.conf b/extensions/pgnodemx/server.conf new file mode 100644 index 0000000..9a17d69 --- /dev/null +++ b/extensions/pgnodemx/server.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = 'pgnodemx' +pgnodemx.kdapi_enabled = off diff --git a/extensions/pgnodemx/test.sql b/extensions/pgnodemx/test.sql new file mode 100644 index 0000000..08e041e --- /dev/null +++ b/extensions/pgnodemx/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pgnodemx; + +SELECT assert_true(cgroup_mode() IS NOT NULL, 'pgnodemx: cgroup_mode()'); +SELECT assert_true(envvar_text('PGDATA') IS NOT NULL, 'pgnodemx: envvar_text()'); diff --git a/extensions/pgpcre/test.sql b/extensions/pgpcre/test.sql new file mode 100644 index 0000000..96da2e9 --- /dev/null +++ b/extensions/pgpcre/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pgpcre; + +SELECT assert_true('foobar' ~ 'f.ob'::pcre, 'pgpcre: match'); +SELECT assert_true(pcre_match('([0-9]+)', 'abc123') = '123', 'pgpcre: pcre_match()'); diff --git a/extensions/pgrouting/test.sql b/extensions/pgrouting/test.sql new file mode 100644 index 0000000..0d3ee3e --- /dev/null +++ b/extensions/pgrouting/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION pgrouting CASCADE; + +CREATE TABLE roads (id int, source int, target int, cost float8); +INSERT INTO roads VALUES (1, 1, 2, 1), (2, 2, 3, 2), (3, 1, 3, 5); + +SELECT assert_true( + (SELECT agg_cost FROM pgr_dijkstra('SELECT id, source, target, cost FROM roads', 1, 3) ORDER BY seq DESC LIMIT 1) = 3, + 'pgrouting: shortest path goes through node 2'); diff --git a/extensions/pgsodium/test.sql b/extensions/pgsodium/test.sql new file mode 100644 index 0000000..9b695ea --- /dev/null +++ b/extensions/pgsodium/test.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION pgsodium; + +SELECT assert_true(length(pgsodium.randombytes_buf(32)) = 32, 'pgsodium: randombytes_buf()'); +SELECT assert_true( + (SELECT pgsodium.crypto_secretbox_open(pgsodium.crypto_secretbox('hello', nonce, key), nonce, key) = 'hello'::bytea + FROM (SELECT pgsodium.crypto_secretbox_keygen() AS key, pgsodium.crypto_secretbox_noncegen() AS nonce) k), + 'pgsodium: secretbox round trip'); diff --git a/extensions/pgsphere/test.sql b/extensions/pgsphere/test.sql new file mode 100644 index 0000000..78cca00 --- /dev/null +++ b/extensions/pgsphere/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_sphere; + +SELECT assert_true(abs((spoint '(0d,0d)' <-> spoint '(90d,0d)') - pi() / 2) < 1e-9, 'pg_sphere: distance on the sphere'); diff --git a/extensions/pgtap/test.sql b/extensions/pgtap/test.sql new file mode 100644 index 0000000..0872238 --- /dev/null +++ b/extensions/pgtap/test.sql @@ -0,0 +1,6 @@ +CREATE EXTENSION pgtap; + +SELECT plan(2); +SELECT assert_true(ok(1 = 1, 'one equals one') LIKE 'ok%', 'pgtap: ok()'); +SELECT assert_true(is(2 + 2, 4) LIKE 'ok%', 'pgtap: is()'); +SELECT * FROM finish(); diff --git a/extensions/pgtt/test.sql b/extensions/pgtt/test.sql new file mode 100644 index 0000000..c53e2ab --- /dev/null +++ b/extensions/pgtt/test.sql @@ -0,0 +1,6 @@ +CREATE EXTENSION pgtt; +LOAD 'pgtt'; + +CREATE /*GLOBAL*/ TEMPORARY TABLE session_cart (item text) ON COMMIT PRESERVE ROWS; +INSERT INTO session_cart VALUES ('book'); +SELECT assert_true((SELECT count(*) FROM session_cart) = 1, 'pgtt: global temporary table'); diff --git a/extensions/pgvectorscale/test.sql b/extensions/pgvectorscale/test.sql new file mode 100644 index 0000000..94f3246 --- /dev/null +++ b/extensions/pgvectorscale/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION vectorscale CASCADE; + +CREATE TABLE embeddings (id int, embedding vector(3)); +INSERT INTO embeddings VALUES (1, '[1,2,3]'), (2, '[4,5,6]'), (3, '[7,8,9]'); +CREATE INDEX ON embeddings USING diskann (embedding vector_cosine_ops); + +SET enable_seqscan = off; +SELECT assert_true((SELECT id FROM embeddings ORDER BY embedding <=> '[1,2,3]' LIMIT 1) = 1, 'vectorscale: diskann search'); diff --git a/extensions/pgxs-build.sh b/extensions/pgxs-build.sh new file mode 100755 index 0000000..98ddeb9 --- /dev/null +++ b/extensions/pgxs-build.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# +# Usage: pgxs-build.sh REPO REF [SUBDIR [MAKE_ARGS...]] +# +# Fetches REPO at REF (a tag or a commit) and builds it with PGXS, installing +# into /out. If MIN_PG_MAJOR is set and the PostgreSQL major is lower, does +# nothing: the extension doesn't support that major (see +# extensions/*/pg-versions). + +set -eu + +repo="$1" +ref="$2" +dir="${3:-.}" +shift 2 +[ "$#" -gt 0 ] && shift + +mkdir -p /out +if [ -n "${MIN_PG_MAJOR:-}" ] && [ "$PG_MAJOR" -lt "$MIN_PG_MAJOR" ]; then + echo "Skipping $repo: needs PostgreSQL $MIN_PG_MAJOR+, this is $PG_MAJOR" + exit 0 +fi + +src="$(mktemp -d)" +git -C "$src" init -q +git -C "$src" fetch -q --depth 1 "$repo" "$ref" +git -C "$src" checkout -q FETCH_HEAD +make -C "$src/$dir" -j"$(nproc)" "$@" +make -C "$src/$dir" install DESTDIR=/out "$@" +rm -rf "$src" diff --git a/extensions/pljs/test.sql b/extensions/pljs/test.sql new file mode 100644 index 0000000..52795ae --- /dev/null +++ b/extensions/pljs/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION pljs; + +CREATE FUNCTION js_add(a int, b int) RETURNS int LANGUAGE pljs AS 'return a + b;'; +SELECT assert_true(js_add(2, 3) = 5, 'pljs: JavaScript function'); diff --git a/extensions/plpgsql_check/test.sql b/extensions/plpgsql_check/test.sql new file mode 100644 index 0000000..83a8fc8 --- /dev/null +++ b/extensions/plpgsql_check/test.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION plpgsql_check; + +CREATE FUNCTION good() RETURNS int LANGUAGE plpgsql AS $$ BEGIN RETURN 42; END $$; +CREATE FUNCTION bad() RETURNS int LANGUAGE plpgsql AS $$ BEGIN RETURN missing_column; END $$; + +SELECT assert_true((SELECT count(*) FROM plpgsql_check_function('good()')) = 0, 'plpgsql_check: valid function'); +SELECT assert_true((SELECT count(*) FROM plpgsql_check_function('bad()')) > 0, 'plpgsql_check: error found'); diff --git a/extensions/plprofiler/server.conf b/extensions/plprofiler/server.conf new file mode 100644 index 0000000..f8ce047 --- /dev/null +++ b/extensions/plprofiler/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'plprofiler' diff --git a/extensions/plprofiler/test.sql b/extensions/plprofiler/test.sql new file mode 100644 index 0000000..5bbcd5c --- /dev/null +++ b/extensions/plprofiler/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION plprofiler; + +SELECT assert_true(pl_profiler_version() > 0, 'plprofiler: pl_profiler_version()'); diff --git a/extensions/postgres_protobuf/test.sql b/extensions/postgres_protobuf/test.sql new file mode 100644 index 0000000..61cf421 --- /dev/null +++ b/extensions/postgres_protobuf/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION postgres_protobuf; + +SELECT assert_true(to_regproc('protobuf_query') IS NOT NULL, 'postgres_protobuf: protobuf_query()'); +SELECT assert_true(to_regproc('protobuf_to_json_text') IS NOT NULL, 'postgres_protobuf: protobuf_to_json_text()'); diff --git a/extensions/prefix/test.sql b/extensions/prefix/test.sql new file mode 100644 index 0000000..dab60f5 --- /dev/null +++ b/extensions/prefix/test.sql @@ -0,0 +1,9 @@ +CREATE EXTENSION prefix; + +CREATE TABLE phone_prefixes (prefix prefix_range, country text); +INSERT INTO phone_prefixes VALUES ('33', 'FR'), ('34', 'ES'), ('349', 'ES mobile'); +CREATE INDEX ON phone_prefixes USING gist (prefix); + +SELECT assert_true( + (SELECT country FROM phone_prefixes WHERE prefix @> '34912345678' ORDER BY length(prefix) DESC LIMIT 1) = 'ES mobile', + 'prefix: longest prefix match'); diff --git a/extensions/prioritize/test.sql b/extensions/prioritize/test.sql new file mode 100644 index 0000000..e0f64b3 --- /dev/null +++ b/extensions/prioritize/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION prioritize; + +SELECT assert_true(get_backend_priority(pg_backend_pid()) IS NOT NULL, 'prioritize: get_backend_priority()'); diff --git a/extensions/rational/test.sql b/extensions/rational/test.sql new file mode 100644 index 0000000..7715605 --- /dev/null +++ b/extensions/rational/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pg_rational; + +SELECT assert_true('1/3'::rational + '1/6'::rational = '1/2'::rational, 'pg_rational: addition'); diff --git a/extensions/roaringbitmap/test.sql b/extensions/roaringbitmap/test.sql new file mode 100644 index 0000000..fd9d28a --- /dev/null +++ b/extensions/roaringbitmap/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION roaringbitmap; + +SELECT assert_true(rb_cardinality(rb_and(rb_build(ARRAY[1, 2, 3]), rb_build(ARRAY[2, 3, 4]))) = 2, 'roaringbitmap: intersection'); +SELECT assert_true(rb_cardinality(rb_or(rb_build(ARRAY[1, 2, 3]), rb_build(ARRAY[3, 4, 5]))) = 5, 'roaringbitmap: union'); diff --git a/extensions/rum/test.sql b/extensions/rum/test.sql new file mode 100644 index 0000000..5b7a866 --- /dev/null +++ b/extensions/rum/test.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION rum; + +CREATE TABLE posts (id int, body tsvector); +INSERT INTO posts VALUES + (1, to_tsvector('english', 'PostgreSQL is great')), + (2, to_tsvector('english', 'Full text search')), + (3, to_tsvector('english', 'RUM index ordering')); +CREATE INDEX ON posts USING rum (body rum_tsvector_ops); + +SET enable_seqscan = off; +SELECT assert_true( + (SELECT id FROM posts WHERE body @@ to_tsquery('english', 'search') ORDER BY body <=> to_tsquery('english', 'search') LIMIT 1) = 2, + 'rum: ranked search'); diff --git a/extensions/semver/test.sql b/extensions/semver/test.sql new file mode 100644 index 0000000..2be1013 --- /dev/null +++ b/extensions/semver/test.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION semver; + +SELECT assert_true('2.0.0'::semver > '1.10.0'::semver, 'semver: comparison'); +SELECT assert_true('1.10.0'::semver > '1.9.0'::semver, 'semver: numeric, not text, ordering'); diff --git a/extensions/set_user/server.conf b/extensions/set_user/server.conf new file mode 100644 index 0000000..b61f7f5 --- /dev/null +++ b/extensions/set_user/server.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'set_user' diff --git a/extensions/set_user/test.sql b/extensions/set_user/test.sql new file mode 100644 index 0000000..595f3cc --- /dev/null +++ b/extensions/set_user/test.sql @@ -0,0 +1,9 @@ +CREATE EXTENSION set_user; +CREATE ROLE set_user_alice NOSUPERUSER; + +SELECT set_user('set_user_alice'); +SELECT assert_true(current_user = 'set_user_alice', 'set_user: switched role'); +SELECT reset_user(); +SELECT assert_true(current_user = 'postgres', 'set_user: reset_user()'); + +DROP ROLE set_user_alice; diff --git a/extensions/tdigest/test.sql b/extensions/tdigest/test.sql new file mode 100644 index 0000000..fb604df --- /dev/null +++ b/extensions/tdigest/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION tdigest; + +SELECT assert_true(abs((SELECT tdigest_percentile(g, 100, 0.5) FROM generate_series(1, 1000) g) - 500) < 20, 'tdigest: median estimate'); diff --git a/extensions/tds_fdw/test.sql b/extensions/tds_fdw/test.sql new file mode 100644 index 0000000..c4a3d62 --- /dev/null +++ b/extensions/tds_fdw/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION tds_fdw; + +-- No SQL Server in tests: check that a server can be defined. +CREATE SERVER mssql FOREIGN DATA WRAPPER tds_fdw OPTIONS (servername '127.0.0.1', port '1433'); +SELECT assert_true((SELECT count(*) FROM pg_foreign_server WHERE srvname = 'mssql') = 1, 'tds_fdw: foreign server'); diff --git a/extensions/temporal_tables/test.sql b/extensions/temporal_tables/test.sql new file mode 100644 index 0000000..a97742d --- /dev/null +++ b/extensions/temporal_tables/test.sql @@ -0,0 +1,10 @@ +CREATE EXTENSION temporal_tables; + +CREATE TABLE employees (id int PRIMARY KEY, title text, sys_period tstzrange NOT NULL DEFAULT tstzrange(now(), NULL)); +CREATE TABLE employees_history (LIKE employees); +CREATE TRIGGER versioning BEFORE INSERT OR UPDATE OR DELETE ON employees + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'employees_history', true); + +INSERT INTO employees (id, title) VALUES (1, 'engineer'); +UPDATE employees SET title = 'manager' WHERE id = 1; +SELECT assert_true((SELECT title FROM employees_history) = 'engineer', 'temporal_tables: old version kept'); diff --git a/extensions/timescaledb/server.conf b/extensions/timescaledb/server.conf new file mode 100644 index 0000000..0101692 --- /dev/null +++ b/extensions/timescaledb/server.conf @@ -0,0 +1,2 @@ +shared_preload_libraries = 'timescaledb' +timescaledb.telemetry_level = off diff --git a/extensions/timescaledb/test.sql b/extensions/timescaledb/test.sql new file mode 100644 index 0000000..3834db7 --- /dev/null +++ b/extensions/timescaledb/test.sql @@ -0,0 +1,10 @@ +CREATE EXTENSION timescaledb; + +CREATE TABLE metrics (time timestamptz NOT NULL, value float8); +SELECT create_hypertable('metrics', by_range('time', interval '1 day')); +INSERT INTO metrics SELECT t, 1 FROM generate_series('2024-01-01'::timestamptz, '2024-01-03', interval '1 hour') t; + +SELECT assert_true((SELECT count(*) FROM timescaledb_information.chunks WHERE hypertable_name = 'metrics') = 3, 'timescaledb: one chunk per day'); +SELECT assert_true( + (SELECT count(*) FROM (SELECT time_bucket('1 day', time) FROM metrics GROUP BY 1) b) = 3, + 'timescaledb: time_bucket()'); diff --git a/extensions/timestamp9/test.sql b/extensions/timestamp9/test.sql new file mode 100644 index 0000000..0481f56 --- /dev/null +++ b/extensions/timestamp9/test.sql @@ -0,0 +1,5 @@ +CREATE EXTENSION timestamp9; + +SELECT assert_true( + timestamp9_to_timestamp(timestamp_to_timestamp9('2020-01-01 12:00:00'::timestamp)) = '2020-01-01 12:00:00'::timestamp, + 'timestamp9: round trip'); diff --git a/extensions/toastinfo/test.sql b/extensions/toastinfo/test.sql new file mode 100644 index 0000000..6b7b78e --- /dev/null +++ b/extensions/toastinfo/test.sql @@ -0,0 +1,12 @@ +CREATE EXTENSION toastinfo; + +CREATE TABLE blobs (kind text, v text); +INSERT INTO blobs VALUES + ('small', 'small'), + ('compressible', repeat('x', 100000)), + -- Random hex barely compresses, so it's moved out of line to TOAST. + ('random', (SELECT string_agg(md5(random()::text), '') FROM generate_series(1, 2000))); + +SELECT assert_true((SELECT pg_toastinfo(v)::text FROM blobs WHERE kind = 'small') = 'short inline varlena', 'toastinfo: inline value'); +SELECT assert_true((SELECT pg_toastinfo(v)::text FROM blobs WHERE kind = 'compressible') LIKE '%compressed%', 'toastinfo: compressed value'); +SELECT assert_true((SELECT pg_toastinfo(v)::text FROM blobs WHERE kind = 'random') LIKE 'toasted%', 'toastinfo: toasted value'); diff --git a/extensions/wal2json/server.conf b/extensions/wal2json/server.conf new file mode 100644 index 0000000..e666a0d --- /dev/null +++ b/extensions/wal2json/server.conf @@ -0,0 +1,3 @@ +wal_level = logical +# Recent minor releases only allow output plugins listed here. +output_plugin_libraries = 'wal2json' diff --git a/extensions/wal2json/test.sql b/extensions/wal2json/test.sql new file mode 100644 index 0000000..db36b9d --- /dev/null +++ b/extensions/wal2json/test.sql @@ -0,0 +1,9 @@ +-- An output plugin for logical decoding, not an extension. +CREATE TABLE changes (id int PRIMARY KEY); +SELECT pg_create_logical_replication_slot('wal2json_test', 'wal2json'); +INSERT INTO changes VALUES (1); + +SELECT assert_true( + (SELECT count(*) FROM pg_logical_slot_get_changes('wal2json_test', NULL, NULL) WHERE data LIKE '%"changes"%') > 0, + 'wal2json: insert decoded as JSON'); +SELECT pg_drop_replication_slot('wal2json_test'); diff --git a/extensions/wrappers/test.sql b/extensions/wrappers/test.sql new file mode 100644 index 0000000..8009494 --- /dev/null +++ b/extensions/wrappers/test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION wrappers; + +SELECT assert_true((SELECT count(*) FROM pg_proc WHERE proname LIKE '%\_fdw\_handler') > 0, 'wrappers: FDW handlers'); diff --git a/renovate.json b/renovate.json index 66e1c73..c734ce5 100644 --- a/renovate.json +++ b/renovate.json @@ -4,12 +4,31 @@ "customManagers": [ { "customType": "regex", - "description": "Extension versions (see the `# renovate:` comments in the Dockerfile)", + "description": "Extensions not from PGDG (see the `# renovate:` comments in the Dockerfile)", "managerFilePatterns": ["/^Dockerfile$/"], "matchStrings": [ "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: registryUrl=(?\\S+))?\\s+ARG \\S+_VERSION=(?\\S+)" ], "extractVersionTemplate": "^v?(?[^+]+)" + }, + { + "customType": "regex", + "description": "PGDG packages in extensions/pgdg.txt: `cron 1.6.8` is postgresql-18-cron, `pgaudit/16 16.1` is postgresql-16-pgaudit", + "managerFilePatterns": ["/^extensions/pgdg\\.txt$/"], + "matchStrings": ["(?m)^(?[a-z0-9-]+(?:/\\d+)?)[ \\t]+(?\\S+)$"], + "datasourceTemplate": "deb", + "packageNameTemplate": "{{#if (containsString depName '/')}}postgresql-{{{replace '^.*/' '' depName}}}-{{{replace '/.*$' '' depName}}}{{else}}postgresql-18-{{{depName}}}{{/if}}", + "registryUrlTemplate": "https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64", + "extractVersionTemplate": "^(?[^-+~]+)", + "versioningTemplate": "loose" + } + ], + "packageRules": [ + { + "description": "One weekly PR for all PGDG package updates", + "matchFileNames": ["extensions/pgdg.txt"], + "groupName": "PGDG extensions", + "schedule": ["before 6am on monday"] } ] } diff --git a/tests/run.sh b/tests/run.sh index 753e727..897b458 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -5,6 +5,20 @@ # Starts IMAGE and runs every extensions/*/test.sql against it, each one in a # fresh database. Fails if any test fails. # +# Each extensions// directory can also have: +# server.conf settings the extension needs (postgresql.conf syntax), e.g. +# shared_preload_libraries. They're applied to the test server. +# pg-versions the PostgreSQL majors the extension supports, e.g. "17 18". +# On other majors the test is reported as SKIP. +# database run the test in this existing database instead of a fresh +# one (e.g. "postgres", for extensions tied to pg_cron's). +# isolated run the test on a server of its own, with only its own +# server.conf. The file says why (e.g. it conflicts with +# another extension). +# +# Tests can call assert_true(condition, message), which raises an error with +# the message when the condition isn't true. +# # If SUMMARY_FILE is set, writes there the PostgreSQL version and the version # of every extension the tests created, one "name version" pair per line. @@ -16,48 +30,148 @@ CONTAINER="postgres-extensor-test-$$" trap 'docker rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT -docker run -d --name "$CONTAINER" -e POSTGRES_PASSWORD=postgres "$IMAGE" >/dev/null +major="$(docker image inspect "$IMAGE" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^PG_MAJOR=//p')" -# Connect over TCP: during first-boot initialisation the entrypoint runs a -# temporary server on the unix socket only, so this waits for the real one. -pg() { - docker exec -i -e PGPASSWORD=postgres "$CONTAINER" psql -h 127.0.0.1 -U postgres -X -q -v ON_ERROR_STOP=1 "$@" +supported() { + local versions="$1/pg-versions" + [ ! -e "$versions" ] || grep -qw "$major" "$versions" } -echo "Waiting for PostgreSQL..." -for _ in $(seq 60); do - docker exec "$CONTAINER" pg_isready -h 127.0.0.1 -U postgres -q && break - sleep 1 -done -if ! docker exec "$CONTAINER" pg_isready -h 127.0.0.1 -U postgres -q; then +# Starts the test server with the server.conf of the given extension +# directories. shared_preload_libraries entries are merged into one list; any +# other setting is passed as is. +start_server() { + local preload=() settings=() dir key value lib libs + for dir in "$@"; do + [ -e "$dir/server.conf" ] || continue + while IFS='=' read -r key value; do + key="$(echo "$key" | xargs)" + value="$(echo "$value" | xargs)" + value="${value#\'}" + value="${value%\'}" + case "$key" in + '' | \#*) ;; + shared_preload_libraries) + IFS=',' read -ra libs <<<"$value" + for lib in "${libs[@]}"; do + lib="$(echo "$lib" | xargs)" + [[ " ${preload[*]} " == *" $lib "* ]] || preload+=("$lib") + done ;; + *) settings+=(-c "$key=$value") ;; + esac + done <"$dir/server.conf" + done + if [ "${#preload[@]}" -gt 0 ]; then + settings+=(-c "shared_preload_libraries=$(IFS=,; echo "${preload[*]}")") + fi + + docker rm -f "$CONTAINER" >/dev/null 2>&1 || true + docker run -d --name "$CONTAINER" -e POSTGRES_PASSWORD=postgres "$IMAGE" postgres "${settings[@]}" >/dev/null + + # Connect over TCP: during first-boot initialisation the entrypoint runs a + # temporary server on the unix socket only, so this waits for the real one. + for _ in $(seq 60); do + docker exec "$CONTAINER" pg_isready -h 127.0.0.1 -U postgres -q && return 0 + sleep 1 + done docker logs "$CONTAINER" echo "PostgreSQL did not start" >&2 exit 1 -fi -pg -At -c 'SELECT version()' +} + +pg() { + docker exec -i -e PGPASSWORD=postgres "$CONTAINER" psql -h 127.0.0.1 -U postgres -X -q -v ON_ERROR_STOP=1 "$@" +} -summary="postgresql $(pg -At -c "SELECT split_part(current_setting('server_version'), ' ', 1)")"$'\n' +assert_function=" +CREATE OR REPLACE FUNCTION public.assert_true(condition boolean, message text) RETURNS void +LANGUAGE plpgsql AS \$\$ +BEGIN + IF condition IS NOT TRUE THEN + RAISE EXCEPTION 'assertion failed: %', message; + END IF; +END \$\$;" +declare -A versions=() +passed=0 failed=() -for sql in "$ROOT"/extensions/*/test.sql; do - name="$(basename "$(dirname "$sql")")" - db="test_${name//-/_}" - pg -c "CREATE DATABASE $db" >/dev/null - if output="$(pg -d "$db" <"$sql" 2>&1)"; then - versions="$(pg -d "$db" -At -F ' ' -c "SELECT extname, extversion FROM pg_extension WHERE extname <> 'plpgsql' ORDER BY extname")" - summary+="$versions"$'\n' - echo "PASS $name (${versions//$'\n'/, })" + +run_test() { + local dir="$1" name db output created ext version + name="$(basename "$dir")" + if [ -e "$dir/database" ]; then + db="$(xargs <"$dir/database")" + else + db="test_${name//-/_}" + fi + if output="$( { [ -e "$dir/database" ] || pg -c "CREATE DATABASE $db"; } 2>&1 >/dev/null && + (echo "$assert_function"; cat "$dir/test.sql") | pg -d "$db" 2>&1)"; then + created="$(pg -d "$db" -At -F ' ' -c "SELECT extname, extversion FROM pg_extension WHERE extname <> 'plpgsql' ORDER BY extname")" + while read -r ext version; do + [ -n "$ext" ] && versions[$ext]="$version" + done <<<"$created" + echo "PASS $name (${created//$'\n'/, })" + passed=$((passed + 1)) else echo "FAIL $name" echo "$output" | sed 's/^/ /' failed+=("$name") + wait_for_recovery fi +} + +# If a test crashed the server, PostgreSQL restarts and recovers on its own: +# wait for it so the remaining tests still run. +wait_for_recovery() { + for _ in $(seq 60); do + pg -At -c 'SELECT 1' >/dev/null 2>&1 && return 0 + sleep 1 + done + docker logs --tail 50 "$CONTAINER" + echo "PostgreSQL did not recover" >&2 + exit 1 +} + +shared=() +isolated=() +skipped=() +for dir in "$ROOT"/extensions/*/; do + dir="${dir%/}" + [ -e "$dir/test.sql" ] || continue + if ! supported "$dir"; then + echo "SKIP $(basename "$dir") (supports PostgreSQL $(xargs <"$dir/pg-versions"))" + skipped+=("$(basename "$dir")") + elif [ -e "$dir/isolated" ]; then + isolated+=("$dir") + else + shared+=("$dir") + fi +done + +echo "Starting PostgreSQL..." +start_server "${shared[@]}" +pg -At -c 'SELECT version()' +pg_version="$(pg -At -c "SELECT split_part(current_setting('server_version'), ' ', 1)")" +for dir in "${shared[@]}"; do + run_test "$dir" +done + +for dir in "${isolated[@]}"; do + echo "Starting PostgreSQL for $(basename "$dir") alone (see extensions/$(basename "$dir")/isolated)..." + start_server "$dir" + run_test "$dir" done if [ -n "${SUMMARY_FILE:-}" ]; then - printf '%s' "$summary" >"$SUMMARY_FILE" + { + echo "postgresql $pg_version" + for ext in "${!versions[@]}"; do + echo "$ext ${versions[$ext]}" + done | sort + } >"$SUMMARY_FILE" fi +echo "Passed: $passed, skipped: ${#skipped[@]}, failed: ${#failed[@]}" if [ "${#failed[@]}" -gt 0 ]; then echo "Failed: ${failed[*]}" >&2 exit 1