Compare commits

..
78 Commits
Author SHA1 Message Date
steve-chavez cd3cf9ed97 bump version to 12.2.12 2025-05-01 20:24:30 -05:00
steve-chavez 1f28efa9bd fix: don't enable admin server /config by default
This now requires setting `admin-server-config-enabled`.
2025-05-01 20:24:08 -05:00
steve-chavez 36eb72c2a0 bump version to 12.2.11 2025-04-21 17:08:00 -05:00
Taimoor ZaeemandSteve Chavez 38c596800a fix: regression with parameter charset=utf-8 in mediatype 2025-04-21 17:06:26 -05:00
steve-chavez a7f9181462 bump version to 12.2.10 2025-04-18 21:28:00 -05:00
Michal KleczekandSteve Chavez f68d5944e6 fix: purge JWT cache asynchronously in a separate thread
Otherwise performance was reduced unnecessarily.
2025-04-18 21:27:36 -05:00
Wolfgang Walther b454f29b2c bump version to 12.2.9 2025-04-16 20:39:19 +02:00
Taimoor ZaeemandWolfgang Walther a3cc762f17 fix: valid JWTs after jwt-secret is changed in a config reload 2025-04-16 18:26:42 +00:00
Taimoor ZaeemandWolfgang Walther 66cd4ca596 fix: regression that replaces an unknown media type with */* (#4013) 2025-04-14 18:00:21 +02:00
Taimoor ZaeemandWolfgang Walther 513704dc2b fix: parsing of the for parameter of plan media type (#4005) 2025-04-12 15:37:37 +00:00
Thilo HohltandWolfgang Walther 026d84a093 docs: add archtika to example apps section on ecosystem page (#4007) 2025-04-11 14:34:56 +02:00
Taimoor ZaeemandWolfgang Walther 106f193a56 docs: add note that ordering of columns is not enforced (#3999) 2025-04-10 14:44:03 +02:00
steve-chavezandWolfgang Walther 79fc43ec39 docs: redirect from broken #bulk-insert-default 2025-04-04 18:24:26 +02:00
Taimoor ZaeemandWolfgang Walther b187f09574 docs: mention that updates also supports specifying columns and missing pref 2025-04-04 18:24:25 +02:00
Taimoor ZaeemandWolfgang Walther 4f1dc72e40 docs: explain missing preference header 2025-04-04 18:24:23 +02:00
renovate[bot]andWolfgang Walther d70d5cb19d chore(deps): update peter-evans/dockerhub-description action to v4.0.2 2025-04-03 12:19:48 +00:00
renovate[bot]andWolfgang Walther 6e851cce59 chore(deps): update peter-evans/dockerhub-description action to v4.0.1 2025-04-01 20:25:14 +00:00
renovate[bot]andWolfgang Walther accf46d2ee chore(deps): update all dependencies 2025-03-27 19:51:24 +00:00
renovate[bot]andWolfgang Walther 852f833d56 chore(deps): update all dependencies 2025-03-27 19:32:19 +00:00
Wolfgang Walther 48e6267dde docs: Remove broken link 2025-03-26 17:00:31 +01:00
Wolfgang Walther ed86bbf3de docs: remove broken link 2025-03-14 23:21:41 +01:00
Wolfgang Walther 3ba4290ef0 chore: Fix style check 2025-02-22 17:01:27 +01:00
Wolfgang Walther 0be047a52f docs: Fix outdated links 2025-02-22 16:10:50 +01:00
Wolfgang Walther d2d3367046 ci: Fix cirrus FreeBSD builds
Apparently Cirrus removed the 14-1 image. When I firsted looked into
this some days ago, when the job started failing, the docs were not
updated, yet - so it wasn't clear. Now the docs mention freebsd-14-2
explicitly...
2025-02-22 14:22:29 +01:00
Laurence IslaandWolfgang Walther ba6f985dac docs: make the aggregate functions docs less verbose 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 7cb007567b docs: reduce verbosity of aggregate functions 2025-02-22 13:19:10 +01:00
Taimoor ZaeemandWolfgang Walther acb72b7c67 docs: add missing jwt claims and clock skew (#3908) 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 8649d27709 docs: correct package for installation under Nix 2025-02-22 13:19:10 +01:00
steve-chavez 4a3936f71c bump version to 12.2.8 2025-02-10 13:25:22 -05:00
M. Taimoor Zaeemandsteve-chavez 9b6b5d06ea fix: log 503 client error to stderr 2025-02-10 13:24:52 -05:00
Wolfgang Walther ec83d08be6 bump version to 12.2.7 2025-02-03 18:38:41 +01:00
Diogo BiazusandWolfgang Walther c026d0752f fix: Fix regression for schema cache reloading via NOTIFY on Windows
Upstream accidentally removed the fix, which was introduced for #2524. Fixed again.

(cherry picked from commit b285f5fba6)
2025-02-03 18:35:24 +01:00
steve-chavez dc96cdb0c3 bump version to 12.2.6 2025-01-29 15:22:22 -05:00
Taimoor Zaeemandsteve-chavez 89be285e6a fix: jwt cache is not purged (#3801) 2025-01-29 15:14:05 -05:00
renovate[bot]andWolfgang Walther c6b4fca160 chore(deps): update codecov/codecov-action action to v5.3.1 2025-01-25 12:01:21 +01:00
renovate[bot]andWolfgang Walther 31cccd3264 chore(deps): update codecov/codecov-action action to v5.3.0 2025-01-25 11:12:46 +01:00
Wolfgang Walther 0bec15fb71 bump version to 12.2.5 2025-01-20 18:24:51 +01:00
Wolfgang Walther cc22da5a02 fix: Make postgrest binary in arm64 docker image executable
This happened in 06aebfaa and caused the arm64 docker image to not start
up properly.

Resolves #3867
2025-01-20 18:24:06 +01:00
Wolfgang Walther 4d477d6e12 bump version to 12.2.4 2025-01-18 19:01:16 +01:00
Wolfgang Walther aff2a78aac Revert "ci: Remove brew install libpq for macos-14 stack build"
This partially reverts commit 53164453d8.
2025-01-18 19:01:16 +01:00
Wolfgang Walther 868f4a066b ci: Fix stack cache on Windows
Apparently the STACK_ROOT has been moved to C:\sr - for unknown reasons,
at least to me.

This should enable caching again and make the stack on windows builds
much faster than recently.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 2c04b98dd9 ci: Split ci into ci and release workflows
This is now possible, after we moved to the ARM build to the GitHub
runners.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 71c73b20ca ci: Remove left-over permissions setting from tag job
This has been replaced by using the SSH key.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 0160b6f370 ci: Display loadtest results in step summary
Much easier to implement and should be easier to find, too.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 038c05447a ci: Build the ubuntu-aarch64 binary with new ARM runners
The new GitHub arm runners are available, so we can use them to build
the ubuntu aarch64 binary instead of our custom machine.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 5d5ee71dce ci: Skip cachix push when no cachix token is set
This happens in forks.
2025-01-18 17:54:42 +01:00
Wolfgang Walther f09b4f4b7d ci: Remove brew install libpq for macos-14 stack build
When macos-14 was rolled out libpq was not installed, but by now it is
by default. Thus, we don't need to do that, it only creates a warning
annotation right now.
2025-01-18 17:54:42 +01:00
Wolfgang Walther d3f9de528c ci: Fix release name of x86-64 binaries
This should have been x86-64, only x64 is not a thing.
2025-01-18 17:54:42 +01:00
Wolfgang Walther c3f88d7a1d ci: Fix release name of macos binary
This is built on macos-14, which is running on new arm based hardware,
not the old x86_64 ones.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 164e6a601f ci: Update stack builder to ubuntu 24.04
Renovate doesn't seem to pick this up, because it's in a matrix
specification.
2025-01-18 17:54:42 +01:00
Andrei DziahelandWolfgang Walther c135a30515 ci: drop directories from windows release
Puts windows release in line with others which have the executable on the top level
2025-01-18 17:54:32 +01:00
renovate[bot]andWolfgang Walther 7515eb5490 chore(deps): update all dependencies 2025-01-17 20:59:24 +01:00
renovate[bot]andWolfgang Walther 629b57337c chore(deps): update haskell-actions/setup action to v2.7.8 2024-12-30 16:31:53 +01:00
M. Taimoor ZaeemandWolfgang Walther 9fcbf994cb fix: insert with missing=default uses column default before using domain default 2024-12-25 11:43:34 +01:00
renovate[bot]andWolfgang Walther 7b9027ea0a chore(deps): update all dependencies 2024-12-20 20:00:36 +01:00
renovate[bot]andWolfgang Walther 0a7ae1815c chore(deps): update dependency macos to v14 2024-12-18 13:52:31 +01:00
renovate[bot]andWolfgang Walther 06014047cb chore(deps): update actions/upload-artifact action to v4.5.0 2024-12-18 12:56:42 +01:00
renovate[bot]andWolfgang Walther 7feb755521 chore(deps): update all dependencies 2024-12-06 09:48:27 +01:00
renovate[bot]andWolfgang Walther a0e7c4f591 chore(deps): update ubuntu:noble docker digest to 80dd3c3 2024-12-04 08:45:16 +01:00
renovate[bot]andWolfgang Walther 572f24901e chore(deps): update codecov/codecov-action action to v5.0.7 2024-11-21 21:26:15 +01:00
renovate[bot]andWolfgang Walther 1e599a2319 chore(deps): update codecov/codecov-action action to v5.0.5 2024-11-20 19:36:28 +01:00
steve-chavezandWolfgang Walther 4e9601d155 fix: always show schema cache load time
It used to be that this was only enabled with log-level=debug.
But the default log-level is misleading, for example:

```
$ PGRST_DB_SCHEMAS="apflora" postgrest-with-postgresql-16  -f test/io/big_schema.sql postgrest-run

...
13/Nov/2024:22:08:20 -0500: Config reloaded
13/Nov/2024:22:08:20 -0500: Schema cache queried in 36.3 milliseconds
13/Nov/2024:22:08:20 -0500: Schema cache loaded 326 Relations, 305 Relationships, 7 Functions, 0 Domain Representations, 4 Media Type Handlers, 1194 Timezones
```

The "Schema cache loaded" can take a while to appear, yet the 22:08:20
time is the same. If we reveal the load time this is clarified:

```
13/Nov/2024:22:08:37 -0500: Schema cache loaded in 16770.1 milliseconds
```
2024-11-16 22:41:44 +01:00
renovate[bot]andWolfgang Walther 53ce2df33a chore(deps): update ubuntu:noble docker digest to 278628f 2024-11-16 22:41:14 +01:00
Laurence IslaandWolfgang Walther c7152787b9 docs: clarify usage of upsert with surrogate primary keys 2024-11-16 13:16:35 +01:00
renovate[bot]andWolfgang Walther 4d4f93da40 chore(deps): update codecov/codecov-action action to v5.0.2 2024-11-15 18:54:10 +01:00
renovate[bot]andWolfgang Walther cb22931015 chore(deps): update codecov/codecov-action action to v5 2024-11-14 19:39:58 +01:00
renovate[bot]andWolfgang Walther 4cc09c6b42 chore(deps): update all dependencies 2024-11-06 19:43:33 +01:00
Wolfgang WaltherandWolfgang Walther 3379ee3483 chore(deps): Update nixpkgs to unstable 2024-05-29
This makes actionlint happy about the new ubuntu runners.
2024-11-06 17:35:55 +01:00
Wolfgang Walther fc7f715aeb ci: Update cirrus' freebsd image to 14.1
This should fix CI which is failing lately like this:
https://cirrus-ci.com/task/4665005218463744

ld-elf.so.1: /lib/libc.so.7: version FBSD_1.8 required by
/usr/local/bin/stack not found
2024-10-26 14:52:13 +02:00
renovate[bot]andWolfgang Walther ec568efd06 chore(deps): update all dependencies 2024-10-24 05:10:55 +02:00
Wolfgang Walther 5b755b2877 docs: Remove broken link
https://github.com/PostgREST/postgrest/actions/runs/11136795565/job/30949162312
2024-10-04 16:02:33 +02:00
renovate[bot]andWolfgang Walther f1453f3c37 chore(deps): update ubuntu:noble docker digest to dfc1087 2024-09-18 20:01:57 +02:00
renovate[bot]andWolfgang Walther 9100ba3b70 chore(deps): update dependency urllib3 to v2.2.3 2024-09-12 21:01:08 +02:00
renovate[bot]andWolfgang Walther 6fe82d9e94 chore(deps): update actions/upload-artifact action to v4.4.0 2024-09-04 13:29:55 +02:00
Jason CloseandGitHub 1fff248c78 docs: rpc example for array of json objects
This change adds an explanation of how to handle an array of JSON objects within an RPC call.  To pass multiple objects, an array of JSON objects must be the JSON value, with the key being the json or jsonb variable name of the Postgres function.

For people who want to perform multiple tasks/inserts/updates within a single API call, this is a needed explanation for that use-case.
2024-08-23 13:00:19 -05:00
renovate[bot]andWolfgang Walther f5264c5aa3 chore(deps): update ubuntu:noble docker digest to 8a37d68 2024-08-18 14:04:26 +02:00
renovate[bot]andLaurence Isla 5de55ea141 chore(deps): update all dependencies 2024-08-13 13:25:12 -05:00
renovate[bot]andWolfgang Walther 1ef83d3732 chore(deps): update actions/upload-artifact action to v4.3.5 2024-08-02 21:07:44 +02:00
64 changed files with 906 additions and 763 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
freebsd_instance:
image_family: freebsd-14-0
image_family: freebsd-14-2
build_task:
# Don't change this name without adjusting .github/workflows/build.yaml
+5
View File
@@ -0,0 +1,5 @@
# TODO: Remove this once a new actionlint release has been cut
# and made its way to us through nixpkgs.
self-hosted-runner:
labels:
- ubuntu-24.04-arm
@@ -112,7 +112,7 @@ runs:
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
- name: Save artifact to GitHub Actions
if: steps.find-task.outputs.task_found
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }}
+2 -2
View File
@@ -19,14 +19,14 @@ inputs:
runs:
using: composite
steps:
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ runner.os }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
+3 -2
View File
@@ -11,15 +11,16 @@ inputs:
runs:
using: composite
steps:
- uses: nixbuild/nix-quick-install-action@60e9c39264d4714139af3cdf15f691b19eec3530 # v28
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
with:
nix_conf: |-
always-allow-substitutes = true
max-jobs = auto
- uses: cachix/cachix-action@ad2ddac53f961de1989924296a1f236fcfbaa4fc # v15
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: postgrest
authToken: ${{ inputs.authToken }}
skipPush: ${{ inputs.authToken == '' }}
- if: ${{ inputs.tools }}
run: nix-env -f default.nix -iA ${{ inputs.tools }}
shell: bash
-72
View File
@@ -1,72 +0,0 @@
#!/bin/bash
set -Eeuo pipefail
# This script builds PostgREST in a remote ARM server
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
[ -z "$2" ] && { echo "Missing 2nd argument: Build environment directory name"; exit 1; }
[ -z "$3" ] && { echo "Missing 3rd argument: GHC version"; exit 1; }
PGRST_GITHUB_COMMIT="$1"
SCRIPT_DIR="$2"
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
# latest is a shortcut documented on https://www.haskell.org/ghcup/guide/#tags-and-shortcuts
CABAL_VERSION="latest"
GHC_VERSION="$3"
install_packages() {
sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install -y git build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 llvm libnuma-dev zlib1g-dev libpq-dev jq gcc
sudo apt-get clean
}
install_ghcup() {
export BOOTSTRAP_HASKELL_NONINTERACTIVE=1
export BOOTSTRAP_HASKELL_MINIMAL=1
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
source ~/.ghcup/env
}
install_cabal() {
ghcup upgrade
ghcup install cabal $CABAL_VERSION
ghcup set cabal $CABAL_VERSION
}
install_ghc() {
ghcup upgrade
ghcup install ghc $GHC_VERSION
ghcup set ghc $GHC_VERSION
}
install_packages
# Add ghcup to the PATH for this session
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
ghcup --version || install_ghcup
ghcup set cabal $CABAL_VERSION || install_cabal
ghcup set ghc $GHC_VERSION || install_ghc
cd ~/$SCRIPT_DIR
# Clone the repository and build the project
git clone https://github.com/PostgREST/postgrest.git
cd postgrest
git checkout $PGRST_GITHUB_COMMIT
cabal v2-update && cabal v2-build
# Strip unused symbols from executable
PGRST_BIN=$(cabal exec which postgrest | tail -1)
strip $PGRST_BIN
# Copy the built binary to the Dockerfile directory
cp $PGRST_BIN ~/$DOCKER_BUILD_DIR
# Move and compress the built binary
mkdir -p ~/$SCRIPT_DIR/result
mv $PGRST_BIN ~/$SCRIPT_DIR/result
cd ~/$SCRIPT_DIR
tar -cJf result.tar.xz result
-50
View File
@@ -1,50 +0,0 @@
#!/bin/bash
set -Eeuo pipefail
# This script publishes the Docker ARM images to Docker Hub.
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
[ -z "$2" ] && { echo "Missing 2nd argument: Docker repo"; exit 1; }
[ -z "$3" ] && { echo "Missing 3rd argument: Docker username"; exit 1; }
[ -z "$4" ] && { echo "Missing 4th argument: Docker password"; exit 1; }
[ -z "$5" ] && { echo "Missing 5th argument: Build environment directory name"; exit 1; }
[ -z "$6" ] && { echo "Missing 6th argument: PostgREST version"; exit 1; }
PGRST_GITHUB_COMMIT="$1"
DOCKER_REPO="$2"
DOCKER_USER="$3"
DOCKER_PASS="$4"
SCRIPT_DIR="$5"
PGRST_VERSION="$6"
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
clean_env()
{
sudo docker logout
}
# Login to Docker
sudo docker logout
{ echo $DOCKER_PASS | sudo docker login -u $DOCKER_USER --password-stdin; } || { echo "Couldn't login to docker"; exit 1; }
trap clean_env sigint sigterm exit
# Move to the docker build environment
cd ~/$DOCKER_BUILD_DIR
# Push final images to Docker hub
# NOTE: This command publishes a separate ARM image because the builds cannot
# be added to the manifest if they are not in the registry beforehand.
# This image must be manually deleted from Docker Hub at the end of the process.
sudo docker buildx build --build-arg PGRST_GITHUB_COMMIT=$PGRST_GITHUB_COMMIT \
-t $DOCKER_REPO/postgrest:$PGRST_VERSION-arm \
--push .
# Add the arm images to the manifest
# NOTE: This assumes that there already is a `postgrest:<version>` image
# for the amd64 architecture pushed to Docker Hub
sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_VERSION $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
[ "$PGRST_VERSION" != "devel" ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
sudo docker logout
+30 -32
View File
@@ -30,10 +30,10 @@ concurrency:
jobs:
static:
name: Nix - Linux static
runs-on: ubuntu-22.04
name: Nix - Linux x86-64 static
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -42,27 +42,27 @@ jobs:
- name: Build static executable
run: nix-build -A postgrestStatic
- name: Save built executable as artifact
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: postgrest-linux-static-x64
name: postgrest-linux-static-x86-64
path: result/bin/postgrest
if-no-files-found: error
- name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
- name: Save built Docker image as artifact
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: postgrest-docker-x64
name: postgrest-docker-x86-64
path: postgrest-docker.tar.gz
if-no-files-found: error
macos:
name: Nix - MacOS
runs-on: macos-12
runs-on: macos-14
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -87,39 +87,38 @@ jobs:
fail-fast: false
matrix:
include:
- name: Linux
runs-on: ubuntu-22.04
- name: Linux aarch64
runs-on: ubuntu-24.04-arm
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
# no artifact for Linux, because we use the static build
artifact: postgrest-ubuntu-aarch64
deps: sudo apt-get update && sudo apt-get install libpq-dev
- name: MacOS
runs-on: macos-12
runs-on: macos-14
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-x64
artifact: postgrest-macos-aarch64
deps: brew link --force libpq
- name: Windows
runs-on: windows-2022
cache: |
~\AppData\Roaming\stack\pantry
~\AppData\Local\Programs\stack\pantry
~\AppData\Roaming\stack\snapshots
~\AppData\Local\Programs\stack\snapshots
~\AppData\Roaming\stack\stack.sqlite3
~\AppData\Local\Programs\stack\stack.sqlite3
C:\sr\pantry
C:\sr\snapshots
C:\sr\stack.sqlite3
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
artifact: postgrest-windows-x64
artifact: postgrest-windows-x86-64
name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: haskell-actions/setup@ec49483bfc012387b227434aba94f59a6ecd0900 # v2.7.5
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
with:
# This must match the version in stack.yaml's resolver
ghc-version: 9.6.5
@@ -147,8 +146,7 @@ jobs:
- name: Strip Executable
run: strip result/postgrest*
- name: Save built executable as artifact
if: matrix.artifact
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifact }}
path: |
@@ -159,15 +157,15 @@ jobs:
freebsd:
name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
task: Build FreeBSD (Stack)
download: bin
upload: postgrest-freebsd-x64
upload: postgrest-freebsd-x86-64
cabal:
@@ -175,11 +173,11 @@ jobs:
matrix:
ghc: ['9.6.4', '9.8.2']
fail-fast: false
name: Cabal - Linux GHC ${{ matrix.ghc }}
runs-on: ubuntu-22.04
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: haskell-actions/setup@ec49483bfc012387b227434aba94f59a6ecd0900 # v2.7.5
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
with:
ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal
+2 -2
View File
@@ -18,9 +18,9 @@ concurrency:
jobs:
lint-style:
name: Lint & Style
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+4 -296
View File
@@ -5,21 +5,10 @@ on:
branches:
- main
- v[0-9]+
tags:
- devel
- v*
concurrency:
# Terminate all previous runs of the same workflow for the same tag, but never
# cancel for the main / release branch pipelines.
group: ci-${{ (github.ref_type == 'tag' && github.ref) || github.run_id }}
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
cancel-in-progress: false
jobs:
check:
name: Check
if: startsWith(github.ref, 'refs/heads/')
uses: ./.github/workflows/check.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
@@ -27,7 +16,6 @@ jobs:
docs:
name: Docs
if: startsWith(github.ref, 'refs/heads/')
uses: ./.github/workflows/docs.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
@@ -35,7 +23,6 @@ jobs:
test:
name: Test
if: startsWith(github.ref, 'refs/heads/')
uses: ./.github/workflows/test.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
@@ -49,87 +36,21 @@ jobs:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
arm:
name: Build / Cabal - aarch64 GHC 9.4.8
if: vars.SSH_ARM_ENABLED
runs-on: ubuntu-22.04
outputs:
remotepath: ${{ steps.Remote-Dir.outputs.remotepath }}
env:
GITHUB_COMMIT: ${{ github.sha }}
GHC_VERSION: '9.4.8'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- id: Remote-Dir
name: Unique directory name for the remote build
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
- name: Copy script files to the remote server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
source: ".github/scripts/arm/*"
target: ${{ steps.Remote-Dir.outputs.remotepath }}
strip_components: 3
- name: Build ARM
uses: appleboy/ssh-action@master
env:
REMOTE_DIR: ${{ steps.Remote-Dir.outputs.remotepath }}
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
command_timeout: 120m
script_stop: true
envs: GITHUB_COMMIT,REMOTE_DIR,GHC_VERSION
script: bash ~/$REMOTE_DIR/build.sh "$GITHUB_COMMIT" "$REMOTE_DIR" "$GHC_VERSION"
- name: Download binaries from remote server
uses: nicklasfrahm/scp-action@main
with:
direction: download
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
source: "${{ steps.Remote-Dir.outputs.remotepath }}/result.tar.xz"
target: "result.tar.xz"
- name: Extract downloaded binaries
run: tar -xvf result.tar.xz && rm result.tar.xz
- name: Save aarch64 executable as artifact
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: postgrest-ubuntu-aarch64
path: result/postgrest
if-no-files-found: error
tag:
name: Release / Tag
name: Tag
concurrency:
# Never tag outdated commits on the main branch by skipping superseded commits
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
cancel-in-progress: false
if: |
vars.RELEASE_ENABLED &&
startsWith(github.ref, 'refs/heads/') &&
needs.docs.result == 'success' &&
needs.test.result == 'success' &&
needs.build.result == 'success' &&
(needs.arm.result == 'skipped' || success())
permissions:
contents: write
runs-on: ubuntu-22.04
if: vars.RELEASE_ENABLED
runs-on: ubuntu-24.04
needs:
- docs
- test
- build
- arm
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit
@@ -147,216 +68,3 @@ jobs:
git tag -f "devel"
git push -f origin "devel"
fi
prepare:
name: Release / Prepare
if: |
startsWith(github.ref, 'refs/tags/') &&
needs.build.result == 'success' &&
(needs.arm.result == 'skipped' || success())
runs-on: ubuntu-22.04
needs:
- build
- arm
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Check the version to be released
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [ "${GITHUB_REF_NAME}" != "devel" ] && [ "${GITHUB_REF_NAME}" != "v$cabal_version" ]; then
echo "Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
exit 1
fi
- name: Identify changes from CHANGELOG.md
run: |
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
echo "Getting unreleased changes..."
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
else
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
echo "Propper release, getting changes for version $version ..."
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
fi
echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md
- name: Save CHANGES.md as artifact
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: release-changes
path: CHANGES.md
if-no-files-found: error
github:
name: Release / GitHub
permissions:
contents: write
runs-on: ubuntu-22.04
needs:
- prepare
if: success() || needs.prepare.result == 'success'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Download all artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
path: artifacts
- name: Create release bundle with archives for all builds
run: |
find artifacts -type f -iname postgrest -exec chmod +x {} \;
mkdir -p release-bundle
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x64.tar.xz" \
-C artifacts/postgrest-linux-static-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x64.tar.xz" \
-C artifacts/postgrest-macos-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x64.tar.xz" \
-C artifacts/postgrest-freebsd-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
-C artifacts/postgrest-ubuntu-aarch64 postgrest
zip "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x64.zip" \
artifacts/postgrest-windows-x64/postgrest.exe
- name: Save release bundle
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: release-bundle
path: release-bundle
if-no-files-found: error
- name: Publish release on GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Releasing version ${GITHUB_REF_NAME} on GitHub..."
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
# To replace the existing release, we must first delete the old assets,
# then modify the release, then add the new assets.
gh release view devel --json assets \
| jq -r '.assets[] | .name' \
| xargs -rn1 \
gh release delete-asset -y devel
gh release edit devel \
-t devel \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
--prerelease
gh release upload --clobber devel release-bundle/*
else
gh release create "${GITHUB_REF_NAME}" \
-t "${GITHUB_REF_NAME}" \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
release-bundle/*
fi
docker:
name: Release / Docker Hub
runs-on: ubuntu-22.04
needs:
- prepare
if: |
vars.DOCKER_REPO && vars.DOCKER_USER &&
(success() || needs.prepare.result == 'success')
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
DOCKER_USER: ${{ vars.DOCKER_USER }}
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Download Docker image
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: postgrest-docker-x64
- name: Publish images on Docker Hub
run: |
docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
docker load -i postgrest-docker.tar.gz
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
# Only tag 'latest' for full releases
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
docker push "$DOCKER_REPO"/postgrest:latest
else
echo "Skipping push to 'latest' tag for pre-release..."
fi
docker-description:
name: Release / Docker Hub Description
runs-on: ubuntu-22.04
if: |
vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
repository: ${{ vars.DOCKER_REPO }}/postgrest
short-description: ${{ github.event.repository.description }}
readme-filepath: ./docker-hub-readme.md
docker-arm:
name: Release / Docker Hub Arm
runs-on: ubuntu-22.04
needs:
- arm
- docker
env:
GITHUB_COMMIT: ${{ github.sha }}
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
DOCKER_USER: ${{ vars.DOCKER_USER }}
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Publish images for ARM builds on Docker Hub
uses: appleboy/ssh-action@master
env:
REMOTE_DIR: ${{ needs.arm.outputs.remotepath }}
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
script_stop: true
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,GITHUB_REF_NAME
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$GITHUB_REF_NAME"
clean-arm:
name: Build / Cleanup
needs:
- arm
- docker-arm
if: ${{ always() && vars.SSH_ARM_ENABLED }}
runs-on: ubuntu-22.04
env:
REMOTE_DIR: ${{ needs.arm.outputs.remotepath }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Remove uploaded files from server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
envs: REMOTE_DIR
script: rm -rf $REMOTE_DIR
+6 -6
View File
@@ -25,9 +25,9 @@ concurrency:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -39,9 +39,9 @@ jobs:
spellcheck:
name: Spellcheck
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -56,9 +56,9 @@ jobs:
linkcheck:
name: Linkcheck
if: github.base_ref == 'main'
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+202
View File
@@ -0,0 +1,202 @@
name: Release
on:
push:
tags:
- devel
- v*
concurrency:
# Terminate all previous runs of the same workflow for the same tag.
group: release-${{ github.ref }}
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
cancel-in-progress: false
jobs:
build:
name: Build
uses: ./.github/workflows/build.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
prepare:
name: Prepare
runs-on: ubuntu-24.04
needs:
- build
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check the version to be released
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [ "${GITHUB_REF_NAME}" != "devel" ] && [ "${GITHUB_REF_NAME}" != "v$cabal_version" ]; then
echo "Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
exit 1
fi
- name: Identify changes from CHANGELOG.md
run: |
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
echo "Getting unreleased changes..."
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
else
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
echo "Propper release, getting changes for version $version ..."
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
fi
echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md
- name: Save CHANGES.md as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-changes
path: CHANGES.md
if-no-files-found: error
github:
name: GitHub
permissions:
contents: write
runs-on: ubuntu-24.04
needs:
- prepare
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download all artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
path: artifacts
- name: Create release bundle with archives for all builds
run: |
find artifacts -type f -iname postgrest -exec chmod +x {} \;
mkdir -p release-bundle
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
-C artifacts/postgrest-linux-static-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
-C artifacts/postgrest-macos-aarch64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
-C artifacts/postgrest-freebsd-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
-C artifacts/postgrest-ubuntu-aarch64 postgrest
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
artifacts/postgrest-windows-x86-64/postgrest.exe
- name: Save release bundle
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-bundle
path: release-bundle
if-no-files-found: error
- name: Publish release on GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Releasing version ${GITHUB_REF_NAME} on GitHub..."
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
# To replace the existing release, we must first delete the old assets,
# then modify the release, then add the new assets.
gh release view devel --json assets \
| jq -r '.assets[] | .name' \
| xargs -rn1 \
gh release delete-asset -y devel
gh release edit devel \
-t devel \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
--prerelease
gh release upload --clobber devel release-bundle/*
else
gh release create "${GITHUB_REF_NAME}" \
-t "${GITHUB_REF_NAME}" \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
release-bundle/*
fi
docker:
name: Docker Hub
runs-on: ubuntu-24.04-arm
needs:
- prepare
if: |
vars.DOCKER_REPO && vars.DOCKER_USER
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download x86-64 Docker image
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: postgrest-docker-x86-64
- name: Download aarch64 binary
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build aarch64 Docker image
run: |
# This only pushes the image via digest, not a tag. This will not appear
# in the image list on Docker Hub, yet. It will be later added to the main
# tag's manifest.
docker buildx build \
-t "$DOCKER_REPO/postgrest" \
--platform linux/arm64 \
--output push-by-digest=true,type=image,push=true \
--metadata-file metadata.json \
.
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
- name: Publish images on Docker Hub
run: |
docker load -i postgrest-docker.tar.gz
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker buildx imagetools create --append \
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
"$DOCKER_REPO/postgrest@$SHA256_ARM"
# Only tag 'latest' for full releases
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
docker push "$DOCKER_REPO"/postgrest:latest
docker buildx imagetools create --append \
-t "$DOCKER_REPO/postgrest:latest" \
"$DOCKER_REPO/postgrest@$SHA256_ARM"
else
echo "Skipping push to 'latest' tag for pre-release..."
fi
docker-description:
name: Docker Hub Description
runs-on: ubuntu-24.04
if: |
vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
repository: ${{ vars.DOCKER_REPO }}/postgrest
short-description: ${{ github.event.repository.description }}
readme-filepath: ./docker-hub-readme.md
-39
View File
@@ -1,39 +0,0 @@
name: Upload Reports
on:
workflow_run:
workflows:
- CI
- Test
branches-ignore:
- devel
- v[0-9]+
- v[0-9]+.[0-9]+.[0-9]+
types:
- completed
jobs:
loadtest:
name: Loadtest
permissions:
checks: write
runs-on: ubuntu-22.04
if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure'
steps:
- name: Download from Artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
name: loadtest.md
path: artifacts
- name: Upload to GitHub Checks
uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
sha: ${{ github.event.workflow_run.head_sha }}
name: Loadtest Results
conclusion: neutral
output: |
{"summary":""}
output_text_description_file: artifacts/loadtest.md
+10 -16
View File
@@ -32,14 +32,14 @@ concurrency:
jobs:
coverage:
name: Coverage
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
# Hack for enabling color output, see:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -49,7 +49,7 @@ jobs:
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
@@ -69,14 +69,14 @@ jobs:
matrix:
pgVersion: ["9_6", 10, 11, 12, 13, 14, 15, 16]
name: PG ${{ matrix.pgVersion }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
# Hack for enabling color output, see:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -98,9 +98,9 @@ jobs:
memory:
name: Memory
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -112,9 +112,9 @@ jobs:
loadtest:
name: Loadtest
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup Nix Environment
@@ -129,10 +129,4 @@ jobs:
- name: Run loadtest
run: |
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
postgrest-loadtest-report > loadtest/loadtest.md
- name: Upload report
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: loadtest.md
path: loadtest/loadtest.md
if-no-files-found: error
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
+57
View File
@@ -5,6 +5,63 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [12.2.12] - 2025-05-01
### Fixed
- #3956, Fix exposing admin server `/config` by default - @steve-chavez
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
## [12.2.11] - 2025-04-21
### Fixed
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
## [12.2.10] - 2025-04-18
### Fixed
- #3889, Fix: JWT cache purging on every request decreases performance - @mkleczek
## [12.2.9] - 2025-04-16
### Fixed
- #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem
- #4014, Fix JWT cache allows old tokens after the jwt-secret is changed in a config reload - @taimoorzaeem
## [12.2.8] - 2025-02-10
### Fixed
- #3841, Log `503` client error to stderr - @taimoorzaeem
## [12.2.7] - 2025-02-03
### Fixed
- #2524, Fix schema reloading notice on windows - @diogob
## [12.2.6] - 2025-01-29
### Fixed
- #3788, Fix jwt cache does not remove expired entries - @taimoorzaeem
## [12.2.5] - 2025-01-20
### Fixed
- #3867, Fix startup for arm64 docker image - @wolfgangwalther
## [12.2.4] - 2025-01-18
### Fixed
- #3779, Always log the schema cache load time - @steve-chavez
- #3706, Fix insert with `missing=default` uses default value of domain instead of column - @taimoorzaeem
## [12.2.3] - 2024-08-01
### Fixed
@@ -1,6 +1,8 @@
# PostgREST docker hub image
# PostgREST Docker Hub image for aarch64.
# The x86-64 is a single-static-binary image built via Nix, see:
# nix/tools/docker/README.md
FROM ubuntu:noble@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 AS postgrest
FROM ubuntu:noble@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782 AS postgrest
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
@@ -8,6 +10,7 @@ RUN apt-get update -y \
&& rm -rf /var/lib/apt/lists/*
COPY postgrest /usr/bin/postgrest
RUN chmod +x /usr/bin/postgrest
EXPOSE 3000
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2024-05-17T23:41:49Z
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
+9 -4
View File
@@ -292,12 +292,17 @@ def setup(app):
app.add_css_file("css/custom.css")
# taken from https://github.com/sphinx-doc/sphinx/blob/82dad44e5bd3776ecb6fd8ded656bc8151d0e63d/sphinx/util/requests.py#L42
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0"
user_agent = (
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
)
linkcheck_ignore = [
r"https://www.patreon.com/postgrest",
r"https://blog.frankel.ch/poor-man-api",
# Odd SSL error
r"https://www.dripdepot.com",
# New GitHub UI delays comment load, so anchor fails
r"https://github.com/.*#issuecomment",
# Random 500 Internal Server Error
r"https://jwt.io",
]
# sphinx-tabs configuration
+2 -4
View File
@@ -8,9 +8,6 @@ Community Tutorials
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `PostgREST + PostGIS API tutorial in 5 minutes <https://gis-ops.com/postgrest-postgis-api-tutorial-geospatial-api-in-5-minutes/>`_ -
In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`functions` interface.
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
@@ -37,6 +34,7 @@ Templates
Example Apps
------------
* `archtika <https://github.com/archtika/archtika>`_ - selfhosted CMS
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
@@ -90,7 +88,7 @@ Client-Side Libraries
* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go
* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript
* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin
* `postgrest-py <https://github.com/supabase-community/postgrest-py>`_ - Python
* `postgrest-py <https://github.com/supabase/postgrest-py>`_ - Python
* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust
* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift
* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.
+1 -1
View File
@@ -15,7 +15,7 @@ Roles for Each Web User
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
.. code-block:: postgres
+1 -3
View File
@@ -39,7 +39,7 @@ Sponsors
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
.. image:: ../static/gnuhost.png
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark
@@ -217,8 +217,6 @@ Here are some companies that use PostgREST in production.
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
* `OpenBooking <https://openbooking.ch>`_
* `Redsmin <https://www.redsmin.com>`_
* `Sompani <https://www.sompani.com>`_
* `Supabase <https://supabase.com>`_
.. Failing links
+1 -1
View File
@@ -1,6 +1,6 @@
Greenplum
#########
`Greenplum <https://greenplum.org/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
`Greenplum <https://blogs.vmware.com/tanzu/tanzu-greenplum/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
+1 -3
View File
@@ -12,10 +12,10 @@ booleans
BOM
Bytea
Cardano
casted
cd
centric
CLI
CMS
coercible
conf
Cloudflare
@@ -146,8 +146,6 @@ SHA
signup
SIGUSR
sl
spreaded
Spreaded
SQL
sql
SQLSTATE
+5 -1
View File
@@ -55,10 +55,12 @@ Metrics
Provides :ref:`metrics`.
.. _runtime_config:
Runtime Configuration
=====================
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`.
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. This requires setting :ref:`admin-server-config-enabled`.
.. code-block:: bash
@@ -72,6 +74,8 @@ Provides a ``config`` endpoint that returns the runtime :ref:`configuration`.
db-channel-enabled = false
...
.. _runtime_schema_cache:
Runtime Schema Cache
====================
+68 -103
View File
@@ -3,20 +3,20 @@
Aggregate Functions
###################
Aggregate functions allow you to summarize data by performing calculations across groups of rows. For instance, if you have an ``orders`` table that has an ``amount`` column, you could use an aggregate function to get the sum of the ``amount`` column, either for all rows, or for each group of rows that share specific values, for instance all rows that share the same ``order_date``.
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``.
Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
.. note::
Aggregate functions are *disabled* by default in PostgREST, as without appropriate safeguards, aggregate functions can create performance problems. See :ref:`db-aggregates-enabled` for further details.
Aggregate functions are *disabled* by default in PostgREST, because they can create performance problems without appropriate safeguards.
See :ref:`db-aggregates-enabled` for further details.
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``. Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
To use an aggregate function, you append the function to a value in the ``select`` parameter, like so:
To use an aggregate function, append it to a column in the ``select`` parameter, like so:
.. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum()"
With the above query, PostgREST will return a single row with a single column named ``sum`` that contains the sum of all the values in the ``amount`` column:
This will return a ``sum`` of all the values of the ``amount`` column in a single row:
.. code-block:: json
@@ -26,15 +26,29 @@ With the above query, PostgREST will return a single row with a single column na
}
]
You can use multiple aggregate functions by just adding more columns with aggregate functions to the ``select`` parameter.
You can ``select`` multiple aggregate functions at the same time (you may need to :ref:`rename them <renaming_columns>` to disambiguate).
To group by other columns, you simply add those columns to the ``select`` parameter. For instance:
.. code-block:: bash
curl "http://localhost:3000/orders?select=total_amount:amount.sum(),avg_amount:amount.avg(),total_quantity:quantity.sum()"
.. note::
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`.
However they are not compatible with :ref:`domain_reps` for the moment.
Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
Automatic ``GROUP BY``
======================
In SQL, a ``GROUP BY`` clause is required to aggregate the selected columns.
However, PostgREST handles grouping automatically if the columns are already present in the ``select`` parameter.
For instance:
.. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date"
This will return a row for each unique value in the ``order_date`` column, with the sum and average of the ``amount`` column for all rows that share the same ``order_date``:
This will get the sum and average of the amounts grouped by each unique value in the ``order_date`` column:
.. code-block:: json
@@ -51,67 +65,55 @@ This will return a row for each unique value in the ``order_date`` column, with
}
]
.. note::
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`. Please note at this time aggregate functions are not compatible with :ref:`domain_reps`. Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
The Case of ``count()``
===========================
The ``count()`` Aggregate
=========================
.. note::
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter. While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended.** Please use ``count()`` (with parentheses) instead.
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter.
While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended**.
Please use ``count()`` (with parentheses) instead.
``count()`` is treated specially, as it can be used without an associated column. Take for example the following query:
``count()`` is a special case because it can be used with or without an aggregated column. For example:
.. code-block:: bash
curl "http://localhost:3000/orders?select=count(),order_date"
This would return a row for each unique value in the ``order_date`` column, with the count of all rows that share the same ``order_date``:
curl "http://localhost:3000/orders?select=count(),observation_count:observation.count(),order_date"
.. code-block:: json
[
{
"count": 4,
"observation_count": 2,
"order_date": "2023-01-01"
},
{
"count": 2,
"observation_count": 1,
"order_date": "2023-01-02"
}
]
When ``count()`` is used with an associated column, its behavior is slightly different: It will return the count of all values that are not ``NULL``. This is due to how PostgreSQL itself implements the ``count()`` function.
Renaming and Casting
====================
Renaming Aggregates
-------------------
Just like with other columns, you can rename aggregated columns too. See :ref:`renaming_columns` for details.
Renaming columns is especially helpful in the context of aggregate functions, as by default a column with an aggregate function applied will take on the name of the applied aggregate function. You may want to provide a more semantically meaningful name or prevent collisions when using multiple aggregate functions of the same type.
Note that there is a difference between the result of ``count()`` and ``observation.count()``.
The former counts the whole row, while the latter counts the non ``NULL`` values of the ``observation`` column (both grouped by ``order_date``).
This is due to how PostgreSQL itself implements the ``count()`` function.
Casting Aggregates
------------------
==================
When applying an aggregate function to a column, you are able to cast both the value of the input to the aggregate function *and* the value of the output from the aggregate function. In both cases, the syntax works as described in :ref:`casting_columns`, with the only difference being the placement of the cast.
It is :ref:`possible to cast <casting_columns>` the aggregated column or the aggregate itself, or both at the same time.
Casting the Value of the Input
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Casting the Aggregated Column
-----------------------------
For instance, imagine that the ``orders`` table has a JSON column, ``order_details``, and this column contains a JSON object that has a key, ``tax_amount``. Let's say you want to get the sum of the tax amount for every order. You can use the ``->`` or ``->>`` operators to extract the value with this key (see :ref:`json_columns`), but these operators will return values of the types JSON and ``text`` respectively, and neither of these types can be used with ``sum()``.
Therefore, you will need to first cast the input value to a type that is compatible with ``sum()`` (e.g. ``numeric``). Casting the input value is done in exactly the same way as casting any other value:
For example, let's say that ``orders`` has an ``order_details`` :ref:`JSON column <json_columns>` with a ``tax_amount`` key.
We cannot sum ``tax_amount`` directly because using ``->`` or ``->>`` will return the data in ``json`` or ``text`` format.
So we need to cast it to a compatible type (e.g. ``numeric``) right before the aggregate function:
.. code-block:: bash
curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()"
With this, you will receive the sum of the casted ``tax_amount`` value:
.. code-block:: json
[
@@ -120,17 +122,15 @@ With this, you will receive the sum of the casted ``tax_amount`` value:
}
]
Casting the Value of the Output
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Casting the Aggregate
---------------------
Now let's return to an example involving the ``amount`` column of the ``orders`` table. Imagine that we want to get the rounded average of the ``amount`` column. One way to do this is to use the ``avg()`` aggregate function and then to cast the output value of the function to ``int``. To cast the value of the output of the function, we simply place the cast *after* the aggregate function:
For instance, if we wanted to round the average of the ``amount`` column, we could do so by casting ``avg()`` to an ``int``:
.. code-block:: bash
curl "http://localhost:3000/orders?select=amount.avg()::int"
You will then receive the rounded average as the result:
.. code-block:: json
[
@@ -139,27 +139,22 @@ You will then receive the rounded average as the result:
}
]
Of course, you can use both input and output casts at the same time, if you so desire.
Aggregates and Resource Embedding
=================================
You can group an aggregate function by an :ref:`embedded resource <resource_embedding>` and also use the aggregates inside them.
Using Aggregate Functions with Resource Embedding
=================================================
Grouping by an Embedded Resource
--------------------------------
Aggregate functions can be used in conjunction with :ref:`resource_embedding`. You can use embedded resources as grouping columns, use aggregate functions within the context of an embedded resource, or use columns from a spreaded resource as grouping columns or as inputs to aggregate functions.
Using Embedded Resources as Grouping Columns
--------------------------------------------
Using an embedded resource as a grouping column allows you to use data from an association to group the results of an aggregation.
For example, imagine that the ``orders`` table from the examples above is related to a ``customers`` table. If you want to get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, you can include the customer name, using the standard :ref:`resource_embedding` syntax, and perform a sum on the ``amount`` column.
Similar to grouping by columns, aggregate functions can also be grouped by embedded resources.
For example, let's say that the ``orders`` table is related to a ``customers`` table.
To get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, we would do the following:
.. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),customers(name)"
You will then get the summed amount, along with the embedded customer resource:
.. code-block:: json
[
@@ -177,15 +172,16 @@ You will then get the summed amount, along with the embedded customer resource:
}
]
.. note::
The previous example uses a has-one association to demonstrate this functionality, but you may also use has-many associations as grouping columns, although there are few obvious use cases for this.
The previous example uses a "to-one" relationship, but this can be done on "to-many" relationships as well (although there are few obvious use cases).
Using Aggregate Functions Within the Context of an Embedded Resource
--------------------------------------------------------------------
This also works in a similar way for :ref:`spread embedded resources <spread_embed>`.
For example, ``select=amount.sum(),...customers(name)`` would sum the ``amount`` grouped by the ``name`` column.
When embedding a resource, you can apply aggregate functions to columns from the associated resource to perform aggregations within the context of an embedded resource.
Using Aggregates Inside Embedded Resources
------------------------------------------
Continuing with the example relationship between ``orders`` and ``customers`` from the previous section, imagine that you want to fetch the ``name``, ``city``, and ``state`` for each customer, along with the sum of amount of the customer's orders, grouped by the order date. This can be done in the following way:
Using the relationship from the previous example, let's take all the ``customers`` and embed their ``orders``.
If we also want to get the total ``amount`` grouped by the ``order_date`` of the ``orders``, we would do the following:
.. code-block:: bash
@@ -226,51 +222,20 @@ Continuing with the example relationship between ``orders`` and ``customers`` fr
}
]
In this example, the ``amount`` column is summed and grouped by the ``order_date`` *within* the context of the embedded resource. That is, the ``name``, ``city``, and ``state`` from the ``customers`` table have no bearing on the aggregation performed in the context of the ``orders`` association; instead, each aggregation can be seen as being performed independently on just the orders belonging to a particular customer, using only the data from the embedded resource for both grouping and aggregation.
Note that the aggregate is done within the embedded resource ``orders``.
It is not affected by any of the columns from the top-level relationship ``customers``.
Using Columns from a Spreaded Resource
--------------------------------------
Using Aggregates in Spreads
~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you :ref:`spread an embedded resource <spread_embed>`, the columns from the spreaded resource are treated as if they were columns of the top-level resource, both when using them as grouping columns and when applying aggregate functions to them.
Grouping with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For instance, assume you want to sum the ``amount`` column from the ``orders`` table, using the ``city`` and ``state`` columns from the ``customers`` table as grouping columns. To achieve this, you may select these two columns from the ``customers`` table and spread them; they will then be used as grouping columns:
All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
.. code-block:: bash
curl "http://localhost:3000/orders?select=amount.sum(),...customers(city,state)
curl "http://localhost:3000/orders?select=order_date,...customers(subscription_date.max(),subscription_date.min())
The result will be the same as if ``city`` and ``state`` were columns from the ``orders`` table:
.. code-block:: json
[
{
"sum": 2000.29,
"city": "New York",
"state": "NY"
},
{
"sum": 9241.21,
"city": "Los Angeles",
"state": "CA"
}
]
Aggregate Functions with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now imagine that the ``customers`` table has a ``joined_date`` column that represents the date that the customer joined. You want to get both the most recent and the oldest ``joined_date`` for customers that placed an order on every distinct order date. This can be expressed as follows:
.. code-block:: bash
curl "http://localhost:3000/orders?select=order_date,...customers(joined_date.max(),joined_date.min())
As columns from a spreaded resource are treated as if they were columns from the top-level resource, the ``max()`` and ``min()`` are applied *within* the context of the top-level, rather than within the context of the embedded resource, as in the previous section.
The result will be the same as if the aggregations were applied to columns from the top-level:
This will take the ``max`` and ``min`` subscription date of every customer and group it by the ``order_date`` column:
.. code-block:: json
+34
View File
@@ -71,6 +71,40 @@ The function parameter names match the JSON object keys in the POST case, for th
.. _function_single_json:
Functions with an array of JSON objects
----------------------------------------------
If you want to pass multiple JSON objects to a Postgres function (an array of objects), you can create a function with a parameter of type ``json`` or ``jsonb``.
Within the curl request, this JSON must be embedded in an object where they key matches the same name as the function's ``json`` or ``jsonb`` parameter.
This will allow you to loop over the array of JSON objects within the Postgres function.
This practice may allow you to reduce the number of ``curl`` requests required to accomplish a task.
For instance, assume we have created this function in the database.
.. code-block:: postgres
CREATE FUNCTION update_data(p_json jsonb)
RETURNS void AS $$
DECLARE
json_item json;
BEGIN
FOR json_item IN SELECT jsonb_array_elements(p_json) LOOP
UPDATE data_table SET data_text_column = (json_item->>'data_text')::text
WHERE data_int_column = (json_item->>'data_int')::integer;
END LOOP;
END;
$$ LANGUAGE SQL IMMUTABLE;
A ``curl`` request using the POST method would look like the following:
.. code-block:: bash
curl "http://localhost:3000/rpc/update_data" \
-X POST -H "Content-Type: application/json" \
-d '{ "p_json": [ { "data_text": "one", "data_int": "1" }, { "data_text": "two", "data_int": "2" } ] }'
Functions with a single unnamed JSON parameter
----------------------------------------------
+42 -1
View File
@@ -12,7 +12,7 @@ The following preferences are supported.
- ``Prefer: return``. See :ref:`prefer_return`.
- ``Prefer: count``. See :ref:`prefer_count`.
- ``Prefer: resolution``. See :ref:`prefer_resolution`.
- ``Prefer: missing``. See :ref:`bulk_insert_default`.
- ``Prefer: missing``. See :ref:`prefer_missing`.
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
- ``Prefer: tx``. See :ref:`prefer_tx`.
- ``Prefer: params``. See :ref:`prefer_params`.
@@ -197,6 +197,47 @@ The ``tx`` preference can be set to specify if the :ref:`transaction <transactio
{"id": 35, "name": "Project X"}
.. _prefer_missing:
Missing
=======
When doing ``POST`` and ``PATCH`` requests, any missing columns in the payload will be inserted as ``null`` value by default. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.
Having:
.. code-block:: postgres
create table foo (
id bigint generated by default as identity primary key
, bar text
, baz int default 100
);
A request:
.. code-block:: bash
curl "http://localhost:3000/foo?columns=id,bar,baz" \
-H "Content-Type: application/json" \
-H "Prefer: missing=default, return=representation" \
-d @- << EOF
[
{ "bar": "val1" },
{ "bar": "val2", "baz": 15 }
]
EOF
Will result in:
.. code-block:: json
[
{ "id": 1, "bar": "val1", "baz": 100 },
{ "id": 2, "bar": "val2", "baz": 15 }
]
.. _prefer_max_affected:
Max Affected
@@ -16,6 +16,32 @@ Use the Accept request header to specify the acceptable format (or formats) for
curl "http://localhost:3000/people" \
-H "Accept: application/json"
.. note::
The ordering of columns in the response isn't guaranteed to align with the order specified in the ``select`` clause. For example, with resource embedding:
.. code-block:: bash
http://localhost:3000/films?select=directors(last_name,id),title
We may get:
.. code-block:: bash
[
{
"title": "title",
"directors": {
"id": 5,
"last_name": "name"
}
}
]
This is in line with the `JSON schema spec <https://json-schema.org/draft/2020-12/json-schema-core#name-instance-data-model>`_:
*"object: An unordered set of properties mapping a string to an instance"*
.. _builtin_media:
Builtin Media Type Handlers
+29 -45
View File
@@ -525,45 +525,6 @@ To bulk insert JSON post an array of objects having all-matching keys
]
EOF
.. _bulk_insert_default:
Bulk Insert with Default Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Any missing columns in the payload will be inserted as ``null`` values. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.
Having:
.. code-block:: postgres
create table foo (
id bigint generated by default as identity primary key
, bar text
, baz int default 100
);
A request:
.. code-block:: bash
curl "http://localhost:3000/foo?columns=id,bar,baz" \
-H "Content-Type: application/json" \
-H "Prefer: missing=default, return=representation" \
-d @- << EOF
[
{ "bar": "val1" },
{ "bar": "val2", "baz": 15 }
]
EOF
Will result in:
.. code-block:: json
[
{ "id": 1, "bar": "val1", "baz": 100 },
{ "id": 2, "bar": "val2", "baz": 15 }
]
.. _specify_columns:
@@ -606,7 +567,13 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s
-X PATCH -H "Content-Type: application/json" \
-d '{ "category": "child" }'
Updates also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:`v_filter`.
Updates also support:
- :ref:`prefer_return`
- :ref:`resource_embedding`
- :ref:`v_filter`
- :ref:`Missing Preference <prefer_missing>`
- :ref:`specify_columns`
.. warning::
@@ -623,18 +590,34 @@ You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge
.. code-block:: bash
curl "http://localhost:3000/employees" \
curl "http://localhost:3000/products" \
-X POST -H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates" \
-d @- << EOF
[
{ "id": 1, "name": "Old employee 1", "salary": 30000 },
{ "id": 2, "name": "Old employee 2", "salary": 42000 },
{ "id": 3, "name": "New employee 3", "salary": 50000 }
{ "sku": "CL2031", "name": "Existing T-shirt", "price": 35 },
{ "sku": "CL2040", "name": "Existing Hoodie", "price": 60 },
{ "sku": "AC1022", "name": "New Cap", "price": 30 }
]
EOF
By default, upsert operates based on the primary key columns, you must specify all of them. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. This works best when the primary key is natural, but it's also possible to use it if the primary key is surrogate (example: "id serial primary key"). For more details read `this issue <https://github.com/PostgREST/postgrest/issues/1118>`_.
By default, upsert operates based on the primary key columns, so you must specify all of them.
You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`.
Upsert works best when the primary key is natural (e.g. ``sku``).
However, it can work with surrogate primary keys (e.g. ``id serial primary key``), if you also do a :ref:`bulk_insert` with :ref:`prefer_missing`:
.. code-block:: bash
curl "http://localhost:3000/employees?colums=id,name,salary" \
-X POST -H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates, missing=default" \
-d @- << EOF
[
{ "id": 1, "name": "Existing employee 1", "salary": 30000 },
{ "id": 2, "name": "Existing employee 2", "salary": 42000 },
{ "name": "New employee 3", "salary": 50000 }
]
EOF
.. important::
After creating a table or changing its primary key, you must refresh PostgREST schema cache for upsert to work properly. To learn how to refresh the cache see :ref:`schema_reloading`.
@@ -739,6 +722,7 @@ Using ``offset`` to target a different subset of rows is also possible.
'#estimated-count': 'pagination_count.html#estimated-count',
'#prefer-return-headers-only': 'preferences.html#headers-only',
'#prefer-return-representation': 'preferences.html#full',
'#bulk-insert-default': 'preferences.html#prefer-missing',
};
let willRedirectTo = redirects[hash];
+9 -1
View File
@@ -159,7 +159,15 @@ You can specify the literal value as we saw earlier, or reference a filename to
JWT Claims Validation
~~~~~~~~~~~~~~~~~~~~~
PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens.
PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_:
- ``exp`` Expiration Time
- ``iat`` Issued At
- ``nbf`` Not Before
- ``aud`` Audience, see :ref:`jwt-aud`
.. note::
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
JWT Security
~~~~~~~~~~~~
+24
View File
@@ -161,6 +161,30 @@ admin-server-port
Specifies the port for the :ref:`admin_server`.
.. _admin-server-config-enabled:
admin-server-config-enabled
---------------------------
.. danger::
The ``/config`` endpoint contains sensitive information, don't enable this if you're exposing the Admin Server publicly.
To safely enable this you can use a proxy like :ref:`nginx` to:
- Ensure ``/config`` are only available to local networks.
- Only expose ``/live`` and ``/ready`` to public networks.
=============== =================================
**Type** Boolean
**Default** False
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_CONFIG_ENABLED
**In-Database** `n/a`
=============== =================================
Enables the admin server :ref:`runtime_config` and :ref:`runtime_schema_cache` endpoints.
.. _app.settings.*:
app.settings.*
+1 -1
View File
@@ -6,4 +6,4 @@ sphinx-rtd-dark-mode>=1.3.0
sphinx-tabs>=3.2.0
sphinx>=5.0.2
sphinxext-opengraph==0.9.1
urllib3==2.2.2
urllib3==2.2.3
+1 -1
View File
@@ -34,7 +34,7 @@
.. code:: bash
nix-env -i haskellPackages.postgrest
nix-env -i postgrest
.. group-tab:: Windows
+3 -3
View File
@@ -2,7 +2,7 @@
{
owner = "NixOS";
repo = "nixpkgs";
date = "2024-04-20";
rev = "92d295f588631b0db2da509f381b4fb1e74173c5";
tarballHash = "162w28y4i5c8g5qhjvs827qxphf2a8n4c8fwhcywzl1j1a35h2im";
date = "2024-05-29";
rev = "a15e8d1b3d9e6496c4a3214e2104f6d28dfa7df7";
tarballHash = "sha256:0r4a4165f4n1zlpnyjrdrwrg86n1b6g3axsdh3j5iizpmjrlxmd7";
}
+10 -3
View File
@@ -57,7 +57,14 @@ let
}
{ });
hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
hasql-pool = lib.dontCheck (prev.callHackageDirect
{
pkg = "hasql-pool";
ver = "1.0.1";
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
}
{ }
);
postgresql-libpq = lib.dontCheck
(prev.postgresql-libpq.override {
@@ -67,8 +74,8 @@ let
hasql-notifications = lib.dontCheck (prev.callHackageDirect
{
pkg = "hasql-notifications";
ver = "0.2.2.0";
sha256 = "sha256-73OQ9/su2qvO7HavF3xuuNWLXSXyB9reBUQDaHys06I=";
ver = "0.2.2.2";
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
}
{ }
);
+2 -2
View File
@@ -1,5 +1,5 @@
name: postgrest
version: 12.2.3
version: 12.2.12
synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security
@@ -110,7 +110,7 @@ library
, gitrev >= 1.2 && < 1.4
, hasql >= 1.6.1.1 && < 1.7
, hasql-dynamic-statements >= 0.3.1 && < 0.4
, hasql-notifications >= 0.2.2.0 && < 0.3
, hasql-notifications >= 0.2.2.2 && < 0.2.3
, hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && < 1.1
, heredoc >= 0.2 && < 0.3
+5 -2
View File
@@ -56,8 +56,11 @@ admin appState req respond = do
in
respond $ Wai.responseLBS status [] mempty
["config"] -> do
config <- AppState.getConfig appState
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
config@Config.AppConfig{configAdminServerConfigEnabled} <- AppState.getConfig appState
if configAdminServerConfigEnabled then
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
else
respond $ Wai.responseLBS HTTP.status404 [] mempty
["schema_cache"] -> do
sCache <- AppState.getSchemaCache appState
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
+40 -14
View File
@@ -5,6 +5,7 @@
module PostgREST.AppState
( AppState
, AuthResult(..)
, JwtCacheState(..)
, destroy
, getConfig
, getSchemaCache
@@ -13,7 +14,7 @@ module PostgREST.AppState
, getNextDelay
, getNextListenerDelay
, getTime
, getJwtCache
, getJwtCacheState
, getSocketREST
, getSocketAdmin
, init
@@ -83,6 +84,12 @@ data AuthResult = AuthResult
, authRole :: BS.ByteString
}
-- | JWT Cache and IO action that triggers purging old entries from the cache
data JwtCacheState = JwtCacheState
{ jwtCache :: C.Cache ByteString AuthResult
, purgeCache :: IO ()
}
data AppState = AppState
-- | Database connection pool
{ statePool :: SQL.Pool
@@ -107,7 +114,7 @@ data AppState = AppState
-- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int
-- | JWT Cache
, jwtCache :: C.Cache ByteString AuthResult
, jwtCacheState :: JwtCacheState
-- | Network socket for REST API
, stateSocketREST :: NS.Socket
-- | Network socket for the admin UI
@@ -139,6 +146,16 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
cache <- C.newCache Nothing
-- purgeExpired has O(n^2) complexity
-- so we wrap it in debounce to make sure it:
-- 1) is executed asynchronously
-- 2) only a single purge operation is running at a time
debounce <- mkDebounce defaultDebounceSettings
-- debounceFreq is set to default 1 second
{ debounceAction = C.purgeExpired cache
, debounceEdge = leadingEdge
}
appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -151,7 +168,7 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
<*> myThreadId
<*> newIORef 0
<*> newIORef 1
<*> C.newCache Nothing
<*> pure (JwtCacheState cache debounce)
<*> pure sock
<*> pure adminSock
<*> pure observer
@@ -265,8 +282,9 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _)) ->
pure ()
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
-- An error on the client-side, usually indicates problems wth connection
observer $ QueryErrorCodeHighObs err
)
return res
@@ -313,8 +331,8 @@ putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCache :: AppState -> C.Cache ByteString AuthResult
getJwtCache = jwtCache
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = jwtCacheState
getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST
@@ -438,12 +456,12 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
-- | Reads the in-db config and reads the config file again
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
readInDbConfig :: Bool -> AppState -> IO ()
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
AppConfig{..} <- getConfig appState
readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
conf <- getConfig appState
pgVer <- getPgVersion appState
dbSettings <-
if configDbConfig then do
qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements)
if configDbConfig conf then do
qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
case qDbSettings of
Left e -> do
observer $ ConfigReadErrorObs e
@@ -452,8 +470,8 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
else
pure mempty
(roleSettings, roleIsolationLvl) <-
if configDbConfig then do
rSettings <- usePool appState (queryRoleSettings pgVer configDbPreparedStatements)
if configDbConfig conf then do
rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))
case rSettings of
Left e -> do
observer $ QueryRoleSettingsErrorObs e
@@ -461,7 +479,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
Right x -> pure x
else
pure mempty
readAppConfig dbSettings configFilePath (Just configDbUri) roleSettings roleIsolationLvl >>= \case
readAppConfig dbSettings (configFilePath conf) (Just $ configDbUri conf) roleSettings roleIsolationLvl >>= \case
Left err ->
if startingUp then
panic err -- die on invalid config if the program is starting up
@@ -469,6 +487,14 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
observer $ ConfigInvalidObs err
Right newConf -> do
putConfig appState newConf
-- After the config has reloaded, jwt-secret might have changed, so
-- if it has changed, it is important to invalidate the jwt cache
-- entries, because they were cached using the old secret
if configJwtSecret conf == configJwtSecret newConf then
pass
else
C.purge jwtCache -- atomic O(1) operation
if startingUp then
pass
else
+29 -4
View File
@@ -44,8 +44,9 @@ import System.Clock (TimeSpec (..))
import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
getJwtCache, getTime)
import PostgREST.AppState (AppState, AuthResult (..),
JwtCacheState (..), getConfig,
getJwtCacheState, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Error (Error (..))
@@ -131,11 +132,35 @@ middleware appState app req respond = do
-- | Used to retrieve and insert JWT to JWT Cache
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
getJWTFromCache appState token maxLifetime parseJwt utc = do
checkCache <- C.lookup (getJwtCache appState) token
let JwtCacheState{..} = getJwtCacheState appState
checkCache <- C.lookup jwtCache token
authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
-- From comment:
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
--
-- We purge expired cache entries on a cache miss
-- The reasoning is that:
--
-- 1. We expect it to be rare (otherwise there is no point of the cache)
-- 2. It makes sure the cache is not growing (as inserting new entries
-- does garbage collection)
-- 3. Since this is time expiration based cache there is no real risk of
-- starvation - sooner or later we are going to have a cache miss.
(Right res, Nothing) -> do -- cache miss
let timeSpec = getTimeSpec res maxLifetime utc
-- insert new cache entry
C.insert' jwtCache timeSpec token res
-- Execute IO action to purge the cache
-- It is assumed this action returns immidiately
-- so that request processing is not blocked.
purgeCache
_ -> pure ()
return authResult
+3
View File
@@ -128,6 +128,9 @@ exampleConfigFile =
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|# admin-server-port = 3001
|
|## Whether to enable the /config endpoint of the admin server
|# admin-server-config-enabled = false
|
|## The database role to use when no client authentication is provided
|# db-anon-role = "anon"
|
+3
View File
@@ -110,6 +110,7 @@ data AppConfig = AppConfig
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int
, configAdminServerConfigEnabled :: Bool
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCSleep :: Maybe Int32
@@ -180,6 +181,7 @@ toText conf =
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-config-enabled", T.toLower . show . configAdminServerConfigEnabled)
]
-- quote all app.settings
@@ -286,6 +288,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> optInt "admin-server-port"
<*> (fromMaybe False <$> optBool "admin-server-config-enabled")
<*> pure roleSettings
<*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-sleep"
-3
View File
@@ -87,9 +87,6 @@ observationLogger loggerState logLevel obs = case obs of
o@(HasqlPoolObs _) -> do
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@(SchemaCacheLoadedObs _) -> do
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
PoolRequest ->
pure ()
PoolRequestFullfilled ->
+51 -28
View File
@@ -1,7 +1,7 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
module PostgREST.MediaType
( MediaType(..)
, MTVndPlanOption (..)
@@ -11,17 +11,16 @@ module PostgREST.MediaType
, decodeMediaType
) where
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Text.ParserCombinators.Parsec as P
import Data.Map (fromList, (!?))
import Data.Text.Encoding (decodeLatin1)
import Network.HTTP.Types.Header (Header, hContentType)
import Data.Map (fromList, (!?))
import qualified Data.Text as T (break, drop, dropWhile,
dropWhileEnd, null, splitOn,
toLower)
import Data.Text.Encoding (decodeLatin1)
import Protolude
import Protolude
-- | Enumeration of currently supported media types
data MediaType
@@ -104,6 +103,9 @@ toMimePlanFormat PlanText = "text"
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
-- MTVndPlan MTApplicationJSON PlanText []
--
-- >>> decodeMediaType "application/vnd.pgrst.plan ; for=\"text/xml\" ; options=analyze"
-- MTVndPlan MTTextXML PlanText [PlanAnalyze]
--
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- MTVndPlan MTTextCSV PlanJSON []
--
@@ -150,7 +152,10 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
("*","*",_) -> MTAny
_ -> MTOther mt'
where
(mainType, subType, params') = tokenizeMediaType mt'
mediaTypeOrError = P.parse tokenizeMediaType "parsec: tokenizeMediaType failed" $ T.unpack mt'
(mainType, subType, params') = case mediaTypeOrError of
Right mt'' -> mt''
Left _ -> (mt',"",[])
params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321
getPlan fmt = MTVndPlan mtFor fmt $
[PlanAnalyze | inOpts "analyze" ] ++
@@ -166,21 +171,39 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON
-- | Split a Media Type string into components
-- >>> tokenizeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- ("application","vnd.pgrst.plan+json",[("for","text/csv")])
-- >>> tokenizeMediaType "*/*"
-- ("*","*",[])
-- >>> tokenizeMediaType "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
-- ("application","vnd.pgrst.plan",[("wat","application/json"),("text/csv\"","")])
tokenizeMediaType :: Text -> (Text, Text, [(Text, Text)])
tokenizeMediaType t = (mainType, subType, params)
where
(mainType, rest) = T.break (== '/') t
(subType, restParams) = T.break (== ';') $ T.drop 1 rest
params =
let rp = T.drop 1 restParams
in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value
param p =
let (k, v) = T.break (== '=') p
in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values
dropAround p = T.dropWhile p . T.dropWhileEnd p
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- Right ("application","vnd.pgrst.plan+json",[("for","text/csv")])
--
-- >>> P.parse tokenizeMediaType "" "*/*"
-- Right ("*","*",[])
--
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
-- Right ("application","vnd.pgrst.plan",[("wat","application/json;text/csv")])
--
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
-- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
-- TODO: Improve mediatype parser as per RFC 2045 https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
tokenizeMediaType = do
mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
P.char '/'
subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
params <- P.many pSemicolonSeparatedKeyVals
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon, discard input after that because it has already failed or we have hit EOF
return (T.pack mainType, T.pack subType, params)
where
pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal
where
pKeyVal :: P.Parser (Text, Text)
pKeyVal = do
key <- P.many1 (P.alphaNum <|> P.oneOf "-")
P.spaces
P.char '='
P.spaces
val <- P.try pQuoted <|> P.try pUnQuoted
return (T.pack key, T.pack val)
where
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|-")
pQuoted = P.char '\"' *> P.manyTill P.anyChar (P.char '\"')
+3 -3
View File
@@ -818,20 +818,20 @@ tablesSqlQuery pgVer =
columnDefault -- typbasetype and typdefaultbin handles `CREATE DOMAIN .. DEFAULT val`, attidentity/attgenerated handles generated columns, pg_get_expr gets the default of a column
| pgVer >= pgVersion120 = [q|
CASE
WHEN t.typbasetype != 0 THEN pg_get_expr(t.typdefaultbin, 0)
WHEN (t.typbasetype != 0) AND (ad.adbin IS NULL) THEN pg_get_expr(t.typdefaultbin, 0)
WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
WHEN a.attgenerated = 's' THEN null
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
END|]
| pgVer >= pgVersion100 = [q|
CASE
WHEN t.typbasetype != 0 THEN pg_get_expr(t.typdefaultbin, 0)
WHEN (t.typbasetype != 0) AND (ad.adbin IS NULL) THEN pg_get_expr(t.typdefaultbin, 0)
WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
END|]
| otherwise = [q|
CASE
WHEN t.typbasetype != 0 THEN pg_get_expr(t.typdefaultbin, 0)
WHEN (t.typbasetype != 0) AND (ad.adbin IS NULL) THEN pg_get_expr(t.typdefaultbin, 0)
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
END|]
+1 -1
View File
@@ -19,6 +19,6 @@ nix:
extra-deps:
- configurator-pg-0.2.10
- fuzzyset-0.2.4
- hasql-notifications-0.2.2.0
- hasql-notifications-0.2.2.2
- hasql-pool-1.0.1
- postgresql-libpq-0.10.1.0
+3 -3
View File
@@ -19,12 +19,12 @@ packages:
original:
hackage: fuzzyset-0.2.4
- completed:
hackage: hasql-notifications-0.2.2.0@sha256:a4e591ef3f06647b056567d3b66948c4a85371f05deb5434edb6ce190f7c845d,2021
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
pantry-tree:
sha256: bd7192a5e82ef6dbac711c3433408a0330c8db1cd3482be1ccd4fbd0a63bc2f6
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
size: 452
original:
hackage: hasql-notifications-0.2.2.0
hackage: hasql-notifications-0.2.2.2
- completed:
hackage: hasql-pool-1.0.1@sha256:3cfb4c7153a6c536ac7e126c17723e6d26ee03794954deed2d72bcc826d05a40,2302
pantry-tree:
+1 -1
View File
@@ -11,5 +11,5 @@ nix:
extra-deps:
- fuzzyset-0.2.4
- hasql-notifications-0.2.2.0
- hasql-notifications-0.2.2.2
- hasql-pool-1.0.1
+3 -3
View File
@@ -12,12 +12,12 @@ packages:
original:
hackage: fuzzyset-0.2.4
- completed:
hackage: hasql-notifications-0.2.2.0@sha256:a4e591ef3f06647b056567d3b66948c4a85371f05deb5434edb6ce190f7c845d,2021
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
pantry-tree:
sha256: bd7192a5e82ef6dbac711c3433408a0330c8db1cd3482be1ccd4fbd0a63bc2f6
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
size: 452
original:
hackage: hasql-notifications-0.2.2.0
hackage: hasql-notifications-0.2.2.2
- completed:
hackage: hasql-pool-1.0.1@sha256:3cfb4c7153a6c536ac7e126c17723e6d26ee03794954deed2d72bcc826d05a40,2302
pantry-tree:
+1
View File
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
admin-server-config-enabled = false
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
admin-server-config-enabled = false
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
admin-server-config-enabled = false
+1
View File
@@ -36,3 +36,4 @@ server-timing-enabled = false
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
admin-server-config-enabled = false
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test"
app.settings.test2 = "test"
@@ -36,5 +36,6 @@ server-timing-enabled = false
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test"
app.settings.test2 = "test"
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test"
app.settings.test2 = "test"
+1
View File
@@ -36,4 +36,5 @@ server-timing-enabled = false
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
admin-server-config-enabled = false
app.settings.test = "Bool False"
+1
View File
@@ -39,3 +39,4 @@ PGRST_SERVER_TIMING_ENABLED: true
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_CONFIG_ENABLED: true
+1
View File
@@ -36,5 +36,6 @@ server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
admin-server-config-enabled = true
app.settings.test = "test"
app.settings.test2 = "test"
+111
View File
@@ -673,6 +673,15 @@ def test_admin_config(defaultenv):
"Should get a success response from the admin server containing current configuration"
with run(env=defaultenv) as postgrest:
response = postgrest.admin.get("/config")
assert response.status_code == 404
env = {
**defaultenv,
"PGRST_ADMIN_SERVER_CONFIG_ENABLED": "true",
}
with run(env=env) as postgrest:
response = postgrest.admin.get("/config")
print(response.text)
assert response.status_code == 200
@@ -1596,3 +1605,105 @@ def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):
response = metapostgrest.session.post("/rpc/reset_db_schemas_config")
assert response.text == ""
assert response.status_code == 204
def test_jwt_cache_purges_expired_entries(defaultenv):
"test expired cache entries are purged on cache miss"
# The verification of actual cache size reduction is done manually, see https://github.com/PostgREST/postgrest/pull/3801#issuecomment-2620776041
# This test is written for code coverage of purgeExpired function
relativeSeconds = lambda sec: int(
(datetime.now(timezone.utc) + timedelta(seconds=sec)).timestamp()
)
headers = lambda sec: jwtauthheader(
{"role": "postgrest_test_author", "exp": relativeSeconds(sec)},
SECRET,
)
env = {
**defaultenv,
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
"PGRST_JWT_SECRET": SECRET,
"PGRST_DB_CONFIG": "false",
}
with run(env=env) as postgrest:
# Generate two unique JWT tokens
# The 1 second sleep is needed for it generate a unique token
hdrs1 = headers(5)
postgrest.session.get("/authors_only", headers=hdrs1)
time.sleep(1)
hdrs2 = headers(5)
postgrest.session.get("/authors_only", headers=hdrs2)
# Wait 5 seconds for the tokens to expire
time.sleep(5)
hdrs3 = headers(5)
# Make another request which should cause a cache miss and so
# the purgeExpired function will be triggered.
#
# This should remove the 2 expired tokens but adds another to cache
response = postgrest.session.get("/authors_only", headers=hdrs3)
assert response.status_code == 200
def test_pgrst_log_503_client_error_to_stderr(defaultenv):
"PostgREST should log 503 errors to stderr"
env = {
**defaultenv,
"PGAPPNAME": "test-io",
}
with run(env=env) as postgrest:
postgrest.session.get("/rpc/terminate_pgrst?appname=test-io")
output = postgrest.read_stdout(nlines=6)
log_message = '{"code":"PGRST001","details":"no connection to the server\\n","hint":null,"message":"Database client error. Retrying the connection."}\n'
assert any(log_message in line for line in output)
def test_invalidate_jwt_cache_when_secret_changes(tmp_path, defaultenv):
"JWT cache should be emptied after jwt-secret is changed in a config reload"
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
external_secret_file = tmp_path / "jwt-secret-config"
external_secret_file.write_text(SECRET)
env = {
**defaultenv,
"PGRST_JWT_SECRET": f"@{external_secret_file}",
"PGRST_DB_CHANNEL_ENABLED": "true",
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400", # enable cache
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous", # required for NOTIFY
}
with run(env=env) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200 # jwt gets cached
# change external file
external_secret_file.write_text("invalid" * 5)
# reload config and external file with NOTIFY
# jwt-cache should get empty
response = postgrest.session.post("/rpc/reload_pgrst_config")
assert response.text == ""
assert response.status_code == 204
sleep_until_postgrest_config_reload()
# now the request should fail because the cached token is removed
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 401
+2 -2
View File
@@ -103,8 +103,8 @@ postJsonArrayTest(){
echo "Running memory usage tests.."
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "27M"
jsonKeyTest "1M" "POST" "/leak?columns=blob" "20M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "20M"
jsonKeyTest "1M" "POST" "/leak?columns=blob" "21M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "21M"
jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M"
jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M"
@@ -375,3 +375,31 @@ spec = describe "custom media types" $ do
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/octet-stream"]
}
context "media type parser fails" $ do
it "sends media type as is" $
request methodGet "/items" (acceptHdrs "undefined") ""
`shouldRespondWith`
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: undefined"} |]
{ matchStatus = 406 }
context "media type parser allowed characters" $ do
it "regression test allowing charset=utf-8" $
request methodPost "/rpc/overloaded_default"
[("Content-Type", "application/json; charset=utf-8")]
[json|{"must_param":1}|]
`shouldRespondWith`
[json|{"val":1}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "handle unrecognized parameters leniently" $ do
request methodPost "/rpc/overloaded_default"
[("Content-Type", "application/json; $$ unrecognized-chars=ignored $$")]
[json|{"must_param":1}|]
`shouldRespondWith`
[json|{"val":1}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
+9
View File
@@ -579,6 +579,15 @@ spec actualPgVersion = do
, matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"]
}
it "inserts a COLUMN default before a DOMAIN default with missing=default" $
request methodPost "/evil_friends_with_column_default?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
[json| { "name": "Demon" } |]
`shouldRespondWith`
[json| [{"id": 420, "name": "Demon"}] |]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"]
}
it "inserts json that has duplicate keys" $ do
request methodPost "/tbl_w_json" [("Prefer", "return=representation")]
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
+1
View File
@@ -151,6 +151,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerPort = Nothing
, configAdminServerConfigEnabled = False
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCSleep = Nothing
+5
View File
@@ -3319,6 +3319,11 @@ create table evil_friends(
, name text
);
create table evil_friends_with_column_default(
id devil_int default 420
, name text
);
create table bets (
id int
, data_json json