Compare commits

...
Author SHA1 Message Date
Taimoor Zaeem 02d83d1c01 test(io): move remaining tests in test_io.py to their modules
We had just 3 tests remaining in test_io.py. This commit moves them to
their modules. So we have:

* test_graceful_shutdown.py

* test_zero_downtime.py

* test_pg_internal.py

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-13 03:42:29 +05:00
Taimoor Zaeem e89e0bc255 test(io): move config related behavior tests to test_config.py
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-13 03:42:29 +05:00
Taimoor Zaeem 9e20e5df90 test(io): move logs and observations tests to test_log.py
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-13 03:42:29 +05:00
Taimoor Zaeem 2122dcef97 test(io): move connection related tests to test_connection.py
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-13 03:42:29 +05:00
Taimoor Zaeem d84d00be8e test(io): move reloading related tests to test_reloading.py
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-13 03:42:29 +05:00
Taimoor ZaeemandSteve Chavez 3a034fcc2a chore: add changelog entry for v16.1
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-12 10:14:34 -05:00
Michał KłeczekandSteve Chavez a2a592a617 fix: JWT validation uses wrong current time due to a bug in auto-update
Upgrade auto-update to 0.2.7 which contains a fix to a bug causing some threads not seeing updates to the cached values.
2026-08-10 17:20:56 -05:00
Taimoor ZaeemandWolfgang Walther 89fe63fce0 nix(release): automate updating source file links in the docs
Closes #5133.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-10 19:14:04 +00:00
steve-chavez f21949a517 chore(changelog): missed entry for ghcr 2026-08-07 19:15:50 -05:00
steve-chavez 6ead34e70b chore(changelog): add integrations section
Also clarify some parts.
2026-08-07 16:17:56 -05:00
steve-chavezandWolfgang Walther e3206262d2 chore(changelog): make it in postgres style
This is to make it easier for users to understand the changes,
much like PostgreSQL does for its changelog: https://www.postgresql.org/docs/19/release-19.html#RELEASE-19-SERVER
(see how they classify "Optimizer", "General Performance", etc).

We classify features, fixes and deprecation in subsections.
Breaking changes are put inside a "Migration to v16" guide.
2026-08-07 19:59:15 +00:00
Taimoor Zaeem a3f36d6184 test: fix failing test of legacy target names warning
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-07 22:47:09 +05:00
renovate[bot]andWolfgang Walther 6cc1698c05 chore(deps): update vmactions/freebsd-vm action to v1.5.3 2026-08-07 07:43:06 +00:00
Wolfgang Walther 77d6055683 chore: bump version to 17 2026-08-07 09:33:46 +02:00
Wolfgang Walther 05d1e0f83d chore: bump version to 16.0 2026-08-07 09:33:45 +02:00
Wolfgang Walther 6fdc230637 nix: remove outdated code / comment in release script 2026-08-07 09:33:29 +02:00
Wolfgang Walther a841dc9c39 chore: ignore stackoverflow in linkcheck
Seems like stackoverflow started blocking GHA runners as well.
2026-08-07 09:14:33 +02:00
steve-chavez 18371ec372 chore(changelog): clarified JSON Path feature 2026-08-06 18:36:16 -05:00
steve-chavez 6951e1301d docs: keep working when scache reload fails
Docs for 54430c3a2c
2026-08-06 17:54:23 -05:00
steve-chavez 75891975f5 docs: fix wrong ref in admin 2026-08-06 17:24:56 -05:00
steve-chavez 142b54158e docs: graceful shutdown docs
Considered the explanations and use cases on:

- https://github.com/yesodweb/wai/issues/853
- https://github.com/PostgREST/postgrest/pull/4580
2026-08-06 17:24:53 -05:00
steve-chavez 876dfad9d9 chore(changelog): move fixed entry to added
Graceful shutdown is really a feature
2026-08-06 17:19:45 -05:00
steve-chavez 2ad6d239a0 docs: add new http server page 2026-08-06 17:19:45 -05:00
Taimoor Zaeem 426e15bbb4 chore(cabal): update haskell language edition to GHC2021
This includes a good set of default language extensions that are
often used by us. It frees us of explicitly importing common extensions.

Ref: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/control.html#extension-GHC2021

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-05 12:38:29 +05:00
steve-chavez 7e8212870d docs: release cycle 2026-08-04 19:49:48 -05:00
renovate[bot]andWolfgang Walther 6a9c99d54e chore(deps): update haskell-actions/setup action to v2.12.0 2026-08-04 21:05:25 +00:00
steve-chavez e44178445d docs: timezone offsets 2026-08-04 13:41:40 -05:00
steve-chavez 59a644bb99 test: prefer timezone offsets 2026-08-04 13:41:40 -05:00
steve-chavez 932c4f6328 perf: no caching for prefer timezone
`Prefer: timezone` no longer requires the schema cache.
Previously this required caching `pg_timezone_names` which was slow in some systems.

Closes https://github.com/PostgREST/postgrest/issues/5100 and
https://github.com/PostgREST/postgrest/issues/4751.
2026-08-04 13:41:40 -05:00
steve-chavez a41396c425 Revert "add: config db-timezone-enabled for optional querying of timezones"
This reverts commit d6c338d588.
2026-08-04 13:41:40 -05:00
steve-chavez ca4a6d9e99 Revert "add: configs as json GUC for db-root-spec"
This reverts commit f34ca15e84.
2026-08-04 00:50:18 -05:00
steve-chavez f34ca15e84 add: configs as json GUC for db-root-spec
These are needed for a complete OpenAPI spec
2026-08-03 23:41:03 -05:00
Wolfgang Walther f84c44dafa nix: add postgrest-with-oriole-18 2026-08-02 14:51:50 +00:00
Wolfgang Walther 549ab36c30 docs: add NixOS integration section
Just a very basic example of how to use the NixOS module that Nixpkgs
provides.
2026-08-02 14:51:50 +00:00
Wolfgang Walther 42754cb6c9 chore(deps): update nixpkgs pin
Nixpkgs has removed support for x86_64-darwin, so removing that from
CI's flake check.

A x86_64-darwin binary is still built via Stack.
2026-08-02 14:51:50 +00:00
Wolfgang Walther 57e8d20811 nix: show stderr output for postgrest-style
Hiding this will also hide errors when they happen...

In my case, my nix-shell's TMPDIR was too long, so black was failing
silently because it couldn't create a unix socket for its forkserver.
2026-08-02 14:51:50 +00:00
Taimoor ZaeemandWolfgang Walther 8ede8fb0dc test(io): move settings related tests to test_settings.py
Towards #4946.

Moves app and pg settings related tests from test_io.py to
test_settings.py.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-08-02 11:53:32 +00:00
Joel JacobsonandWolfgang Walther 131df9b578 perf: use pg_basetype on PG 17+ for schema cache 2026-08-02 11:47:26 +00:00
Taimoor Zaeem 6a7ad33524 fix: db-channel-enabled not reloadable on config reload
Fixes #4894.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-31 21:39:27 +05:00
Taimoor Zaeem b174295bf2 refactor(chore): rename conf to oldConf in readInDbConfig function
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-31 21:39:27 +05:00
renovate[bot]andWolfgang Walther 6304265cd9 chore(deps): update docker/login-action action to v4.6.0 2026-07-30 20:41:39 +00:00
Taimoor Zaeem cb9b00d374 docs: update link of listener in architecture page
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-30 11:23:56 +05:00
Taimoor Zaeem 59081195f0 chore: add changelog entry for v14.16
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-28 23:31:31 +05:00
Taimoor Zaeem 022f0faa38 add: make config log-level reloadable
Closes #5113.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-28 11:33:33 +05:00
Taimoor Zaeem f6d34fd4fc refactor: move reloading related logic to Reload.hs
This commit moves, schema cache reload, config reload and listener
reload logic to `Reload.hs` module.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-25 14:39:48 +05:00
Taimoor Zaeem e32e2b07b5 refactor: move connection pool functions to Pool.hs
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-25 14:39:48 +05:00
Taimoor Zaeem a108d66968 refactor: move AppState type and update functions to AppState/Types.hs
`AppState.hs` is imported by many modules. Some of those modules contain
important functions that need to be imported back to `AppState.hs`, causing
circular dependency problem.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-25 14:39:48 +05:00
renovate[bot]andWolfgang Walther e820cc7370 chore(deps): update docker/login-action action to v4.5.1 2026-07-24 19:26:16 +00:00
renovate[bot]andWolfgang Walther 74c8a3580d chore(deps): update vmactions/freebsd-vm action to v1.5.2 2026-07-24 18:51:13 +00:00
steve-chavez 03c67f3410 test: move psql_as_superuser as util and reuse it 2026-07-24 13:44:58 -05:00
renovate[bot]andWolfgang Walther 1d195bf865 chore(deps): update docker/login-action action to v4.5.0 2026-07-23 20:11:53 +00:00
renovate[bot]andWolfgang Walther 473734f717 chore(deps): update actions/checkout action to v7.0.1 2026-07-23 20:11:28 +00:00
Taimoor Zaeem 178b1d3e38 fix: kill postgrest main thread on admin server crash
Fixes #5096.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-21 20:53:40 +05:00
Taimoor Zaeem 080abcb695 chore: increase columns length in .stylish-haskell.yaml
This gives the import statements more space, essentially, it collectively
removes 90+ unneeded lines in the imports statements.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-19 20:56:12 +05:00
Taimoor ZaeemandWolfgang Walther 8faf9c0519 test: resolve pytest warning when running io tests
When running `postgrest-test-io`, pytest raises a `InsecureKeyLengthWarning`
for a test. To clear that warning, this commit increases the key length to
more than 32 characters.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-17 17:42:51 +00:00
Taimoor Zaeem f81dd29cd1 chore: build haskell source modules in parallel
`postgrest-build` now builds twice as fast as compared to previous
build time. This can be tested by running `rm -rf dist-newstyle/` and
then run `postgrest-build`.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-16 13:34:59 +05:00
Michał KłeczekandSteve Chavez c297d051dc add: use SO_REUSEPORT on platform supporting it 2026-07-15 11:51:21 -05:00
Wolfgang Walther de19b04fe4 amend(aa7d442d): fix docs link to new Main.hs location 2026-07-15 08:55:19 +02:00
Taimoor Zaeem 51e9fd6efd fix: schema cache dump missing RPC transaction isolation level
Fixes #5079.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-14 23:13:21 +05:00
Taimoor Zaeem 63a2267f53 chore: add changelog entry for v14.15
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-14 21:04:15 +05:00
Wolfgang Walther aa7d442d32 chore: move executable code to src/
src/ now contains all source code - in subdirectories, according to the
.cabal component they belong to. This will allow us to put vendored
libraries in the same place - and later split our own code into multiple
components/libraries as well.
2026-07-14 06:58:15 +00:00
Wolfgang Walther 44e15e4e9b chore: run doctests in parallel
Runs the doctests much faster, which is potentially useful in
combination with postgrest-watch for local development.

This implies that doctests run on compiled code, not in a GHCi session,
which has some implications:
- Only exported functions can be tested.
- Imports need to be made explicit in doctests themselves.

On the flipside, this would allow us to potentially include doctest
results in code coverage, I believe.

This change is a requirement to vendor hasql, which otherwise breaks the
existing doctests: hasql contains a .hsc file, which *needs* to be
compiled - not interpreted - to make the tests work.
2026-07-14 06:55:32 +00:00
Wolfgang Walther aa6c19ebda test: remove useless doctest in Network.hs
This essentially just tests upstream's show instance.
2026-07-14 06:55:32 +00:00
Taimoor ZaeemandWolfgang Walther 8776ece7d5 test(io): move admin server tests to test_admin.py
Towards #4946.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-11 19:20:59 +00:00
steve-chavez 205135853d amend: update cabal index so cabal build succeeds 2026-07-11 13:24:09 -05:00
steve-chavez a71ab36096 amend: use warp 3.4.14 for 3522917
Use a release instead of a git commit
2026-07-11 13:12:03 -05:00
steve-chavezandLaurence Isla 490d1dc5d3 add: config to emit warning for legacy target names
Adds the `url_use_legacy_target_names` config.

Enabled (default):
* It allows using the resource name in filters,
  orders or limits when it has an alias, e.g.
  `table?select=alias:target(*)&target.id=eq.1`
* Logs a WARNING with a hint to use the alias
* Returns a Warning header in the response

Disabled:
* It returns an error, only the alias is allowed
* No warnings returned

This feature is deprecated
2026-07-11 02:15:07 +00:00
steve-chavez 2fa8de4e52 chore: add changelog entry for 5077 2026-07-10 19:54:11 -05:00
steve-chavez 3522917021 fix: admin server dies silently by upgrading warp
Uses warp main branch https://github.com/yesodweb/wai/commit/ad041216b643f69a2a9c87cbf4c2988aa4633dd5
since there's no release yet.
2026-07-10 17:56:15 -05:00
Michał KłeczekandSteve Chavez 4fc47754c2 refactor: do not open a TCP connection in liveness check
This change introduces a new way to perform liveness check - instead of trying to open a TCP connection to main server, we check if main socket is listening and if main server accept loop thread is alive.

Opening a TCP connection in liveness check was problematic because:
* it used available file descriptors which might have been a problem under load
* made liveness check unreliable when multiple PostgREST instances are available on the same port (eg. using SO_REUSEPORT)
2026-07-10 12:45:59 -05:00
Taimoor Zaeem 0bda2bcdff refactor: remove redundant destroyPool function
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-08 21:09:20 +05:00
Taimoor Zaeem c03ac09dbb test: move prepared statements config related tests to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-07 23:32:40 +05:00
Taimoor Zaeem 44edf962d8 test: move db-pre-config with pg reserved word func to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-07 23:32:40 +05:00
Taimoor Zaeem 6670b1a4b6 test(io): add test to db-pre-config with non-existent function
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-07 23:32:40 +05:00
Taimoor Zaeem c73c676849 test: move error verbosity test from io tests to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-07 23:32:40 +05:00
steve-chavez 6191243c64 ci: add git to freebsd
Otherwise when using a git commit on stack the build fails because it
lacks the git dependency.
2026-07-06 21:50:26 -05:00
Michał KłeczekandSteve Chavez c5c9dc33c9 refactor: make sure proper sockets cleanup is performed in App.run
Right now code in App.run does not properly use bracket/finally to close sockets and clean-up mainSocketRef. This is not a big problem at the moment because the application is going to exit enyway but introducting proper resource handling will make future refactorings safer.
2026-07-06 10:59:05 -05:00
Michał KłeczekandSteve Chavez 56df1dc532 refactor: encapsulate application termination
This change makes AppState and Listener modules independent from the way how application is terminated. It removes stateMainThreadId from AppState and introduces stateKillApp. It also removes exported  function AppState.getMainThreadId replacing it with more general killApp.
2026-07-06 10:29:17 -05:00
Michał KłeczekandSteve Chavez 06bda07db1 refactor: encapsulate how main application liveness check is done
This change makes Admin module independent from how liveness check is performed. It moves reachMainApp function to App module and makes Admin.runAdmin, instead of action to get the main socket, take an IO action checking liveness as a parameter.

Thanks to this, any change in how liveness check is performed and, more importantly, what data it uses, does not require changing multiple modules (Admin and App).
2026-07-05 15:22:37 -05:00
Wolfgang Walther 0bd2821937 chore(deps): update hackage freeze, stackage snapshot and nixpkgs pin
This brings in PostgreSQL 19 beta 1 to start testing against.
2026-07-05 12:50:14 +00:00
renovate[bot]andWolfgang Walther 936d9df13d chore(deps): update docker/login-action action to v4.4.0 2026-07-05 12:49:52 +00:00
Taimoor Zaeem 75b315d077 nix: provide default file name when generating module graph
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-05 00:44:05 +05:00
renovate[bot]andWolfgang Walther 76c1ec5b06 chore(deps): update docker/setup-buildx-action action to v4.2.0 2026-07-02 20:08:24 +00:00
renovate[bot]andWolfgang Walther a836fb5e37 chore(deps): update docker/login-action action to v4.3.0 2026-07-02 08:56:54 +00:00
ncikandWolfgang Walther 69d21a82c9 docs: fix example jwt expiration epoch
Issue: the example epoch date is wrapped in parentheses. This causes the server to return PGRST303: "The JWT 'exp' claim must be a number"

Fix: remove parentheses
2026-07-01 19:22:59 +00:00
steve-chavez 412c6a1d26 chore: mentions docs on CONTRIBUTING 2026-06-30 10:06:45 -05:00
renovate[bot]andWolfgang Walther 5a987c06fe chore(deps): update korthout/backport-action action to v4.6 2026-06-30 06:24:53 +00:00
Taimoor ZaeemandSteve Chavez af17a75ae1 chore: add changelog entry for v14.14
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-29 16:42:14 -05:00
renovate[bot]andWolfgang Walther 9f3dc46502 chore(deps): update vmactions/freebsd-vm action to v1.5.0 2026-06-29 17:46:01 +00:00
Taimoor ZaeemandSteve Chavez b967a5033a fix(admin): log admin server exceptions and close admin socket
Admin server was crashing silently and requests hang indefinitely.
With this fix, we are now logging the admin server exceptions and also
close the socket afterwards so admin request don't hang.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-29 12:43:54 -05:00
Taimoor Zaeem 18646a6c03 chore: enable GHC NumericUnderscores language extension
The `_` character between numeric literals improve their readablity.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-28 01:01:12 +05:00
Wolfgang Walther 84e2a0c829 nix(loadtest): generate targets at nix build time
Instead of generating them at loadtest runtime, we generate these
dynamic targets once via Nix. Because we're using libfaketime, we're not
bound to doing it at the same time as running the tests.

This allows us to run each loadtest right after each other, with minimal
other stuff going on inbetween. It's also a tiny bit more reproducible,
because the loadtests against both branches run with exactly the same
set of targets, where they did not before.
2026-06-27 18:01:50 +00:00
Wolfgang Walther f1649bf6ee nix(loadtest): run all target branches almost at the same time
Instead of building, running, building, running, ... we now build all
executables once ahead of time and then run all loadtests right after
each other. This can sometimes reduce noise when load on the GHA runner
varies over time.

Since this requires us to move building into the loadtest-against
script, it also allows to go back to have the regular postgrest-loadtest
command default to building with cabal for faster local iteration.
2026-06-27 18:01:50 +00:00
Wolfgang Walther bc5d7a0e92 nix: remove postgrest-git-hooks
I'm not sure whether anyone is actually using them, but they never
worked really well, I believe. We should add something based on an
existing pre-commit hook manager, not that brittle custom thing we have.

Allows me to remove postgrest-with-git in the next commit instead of
duplicating it.
2026-06-27 18:01:50 +00:00
Wolfgang Walther d5875c1554 nix(loadtest): refactor loadtest-against
No need to duplicate this code, all of it is right there.
2026-06-27 18:01:50 +00:00
Wolfgang Walther 576805b7b0 nix: remove hint about installing tools locally from README
This is a heavily discouraged way of using these tools - they will
become outdated almost immediately and would need to be reinstalled on
every change to main.
2026-06-27 18:01:30 +00:00
Wolfgang Walther 2b463ab1c6 nix: remove docker tools note from README
This is outdated, we don't have that docker argument anymore - docker
tools are exposed by default, when on Linux.
2026-06-27 18:01:30 +00:00
Wolfgang Walther 6f899ee493 nix: reduce list of commands in README
This list is chronically outdated and hard to maintain with the two
column layout. Simplify and reduce, not important to keep up-to-date to
bring the point across.
2026-06-27 18:01:30 +00:00
Wolfgang Walther 556a5dad47 chore: remove eol-whitespace in postgrest.cabal 2026-06-27 19:20:24 +02:00
renovate[bot]andWolfgang Walther cbaf5ac8fc chore(deps): update vmactions/freebsd-vm action to v1.4.9 2026-06-27 09:41:20 +00:00
renovate[bot]andWolfgang Walther 76261d5bfc chore(deps): update actions/cache action to v6.1.0 2026-06-26 19:48:22 +00:00
Wolfgang Walther dcc99040d6 nix(loadtest): run more requests in jwt-cache test
We don't need to restrict us to run every request once. Instead we can
run as many as possible within 60s by removing the `-lazy` flag.

This also allows us to generate fewer targets, because vegeta will do
the repetition for us.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 07a9752866 nix(loadtest): remove jwt-cache-worst
This test is not really functional (anymore?). It depends on some tokens
expiring - but they don't, really. This is because:
1) Before merging the RSA and HS variants earlier in this patch series,
they ran roughly 15s and 34s respectively in CI.
2) Since PostgREST takes a clock skew of up to 30 seconds into account,
there would be *at most* 4s of expiring tokens in a test run - and
*only* in the HS case, not for RSA.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 7cafa67f0a nix(loadtest): use faketime in jwt-cache test
While we didn't have much problem in this test, it's surely not a
problem either to make the test more reproducible with a fixed base
timestamp. This gives us a tiny bit less of a diff on the next change.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 3909e85f67 nix(loadtest): remove jwt test
This test is not very interesting, really: It's hard to imagine to be
able to produce a regression that would slow down the no-cache case, but
would *not* show when a cache is used.
2026-06-26 19:33:51 +00:00
Wolfgang Walther e83a25a698 nix(loadtest): remove genKeyMaterials
Merge the same code into genTargets, which removes the need to save the
private key to disk and read it again.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 32706284f0 nix(loadtest): refactor key selection
Just a bit simpler.
2026-06-26 19:33:51 +00:00
Wolfgang Walther a079d23de1 nix(loadtest): remove useless try/except blocks
Rethrowing the same error, but without stacktrace is not really useful,
but only makes the code (and debugging!) harder than it should be.
2026-06-26 19:33:51 +00:00
Wolfgang Walther d470213857 nix(loadtest): remove pyjwt dependency
Let's use the same dependency when creating key material and when
creating tokens, no need to carry multiple different dependencies and
converting keys between them.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 1446f11222 nix(loadtest): refactor genTargets argument
Instead of passing a path to the output target file, just pass the
directory in which to create the file. This allows the script to create
multiple files later.
2026-06-26 19:33:51 +00:00
Wolfgang Walther ef72787907 nix(loadtest): remove left-over run_command helper
When we changed the test structure to use libfaketime, we moved away
from generating targets as part of the chained command running
with-pg/with-pgrst. This function was left-over from that, the
genTargets script does not need to relay its remaining arguments
arguments anymore.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 66d40c0159 nix(loadtest): merge jwt-rsa-* and jwt-hs-* tests
Instead of creating separate test suites for the key type, the PostgREST
instance now accepts both keys via a JWKSet and the targets are
generated 50/50 for both.

The results are still reported seperately by using a different URL,
which shows up as separate rows in the results.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 2d340e8a8c nix(loadtest): refactor generate_jwt -> generate_target
This makes the next change easier to do, but should be easier to review
step-by-step.
2026-06-26 19:33:51 +00:00
Wolfgang Walther 94be1b7250 nix(loadtest): remove --rsa option from genTargets
The public key is not needed for target generation, only the private
key, so it's enough to pass that.
2026-06-26 19:33:51 +00:00
Wolfgang Walther b32c477b1d nix(loadtest): remove --method argument for JWT loadtests
This is not used by CI and I doubt it's actually used by anyone locally.
2026-06-26 19:33:51 +00:00
Wolfgang Walther e5eb98c8a0 nix(loadtest): fix postgrest-loadtest-report locally
The script currently depends on results from "head" and "main" to be
present to calculate the change ratio. Fallback nicely in case these are
not available, which allows using the same to report URL-by-URL stats
locally.
2026-06-26 19:33:51 +00:00
Taimoor ZaeemandWolfgang Walther bb63c3fade change: use RFC 9535 syntax for jwt-role-claim-key config
BREAKING CHANGE

Breaks the string comparison operators implemented in #3813. Those can
be replaced with regex searches using JSON Path `search()` function.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-26 18:32:05 +00:00
Wolfgang Walther a0bb87d693 nix(loadtest): fix cpu/mem monitoring for tests using libfaketime
Instead of starting a separate process, which would make the pid we
store the one from `faketime` instead of `postgrest`, we can also just
start libfaketime directly via LD_PRELOAD.
2026-06-26 18:30:25 +00:00
Wolfgang Walther ba9336f402 nix: change reference from master to main in README 2026-06-26 18:16:36 +02:00
Wolfgang Walther 5feb55a95b nix: remove PGRST_DELAY AND PGDELAY from README
These had been removed a while ago.
2026-06-26 18:16:25 +02:00
Taimoor Zaeem 67956b3cb1 refactor: log server listening observations after listening socket is ready
Correct behavior is to log this after the listening socket is ready.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-26 16:01:55 +05:00
renovate[bot]andWolfgang Walther a6840f9bf9 chore(deps): update actions/cache action to v6 2026-06-25 20:10:47 +00:00
Taimoor ZaeemandSteve Chavez 081c4c6d70 test(spec): sort imports in SpecHelper.hs
The imports were scattered all around. This commit
sorts and group them.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-25 11:21:28 -05:00
Taimoor ZaeemandSteve Chavez b5f10be167 test: move jwt cache tests from io tests to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-25 11:21:28 -05:00
Taimoor ZaeemandSteve Chavez 0bc9fe813f test: move jwt iat claim test from io tests to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-25 11:21:28 -05:00
Taimoor ZaeemandSteve Chavez feb4853273 test(io): add test for admin server listening observation
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-25 10:39:16 -05:00
Taimoor Zaeem 9e0f9bc4ee refactor: correct admin server port setting
For admin server, port server-setting was set to the same port as
app server port. This doesn't have any effect because it is overridden
by the already initialized admin server socket, but it's better to
explicitly correct it to avoid anything unexpected.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-23 23:05:02 +05:00
Taimoor Zaeem f5fd2e71b6 test: move jwt error tests from io tests to spec tests
Towards #4946.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-22 23:52:45 +05:00
Taimoor Zaeem ee30bd03ac test(io): remove needless server-timing config coverage tests
These 2 tests don't test any specific behavior - they were added
only to please code coverage.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-22 23:52:45 +05:00
Taimoor Zaeem 1a35af5024 test(spec): rename misleading function name in ErrorSpec.hs
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-22 23:52:45 +05:00
steve-chavez ded9be6774 nix(loadtest): add comment on loadtest-against 2026-06-22 12:55:56 -05:00
Wolfgang Walther ffe4cc6f42 nix(loadtest): allow running multiple postgrest-loadtest in same working directory
There is no need to store the out-links to the various build artifacts
in the current working directory while running postgrest-with-pg, which
is used by postgrest-loadtest. Instead reference the nix store paths
directly.

Resolves #5028
2026-06-21 12:56:42 +02:00
renovate[bot]andWolfgang Walther 79e9e77e46 chore(deps): update vmactions/freebsd-vm action to v1.4.8 2026-06-20 21:57:24 +00:00
renovate[bot]andWolfgang Walther 87b480bc0a chore(deps): update actions/checkout action to v7 2026-06-19 19:09:44 +00:00
renovate[bot]andWolfgang Walther 336d32d1ce chore(deps): update nixbuild/nix-quick-install-action action to v35 2026-06-19 19:09:02 +00:00
renovate[bot]andWolfgang Walther 9aef6f30a8 chore(deps): update vmactions/freebsd-vm action to v1.4.7 2026-06-19 19:08:17 +00:00
Taimoor ZaeemandSteve Chavez 242e5ba87e chore(cabal): explain performance related build flags
These flags were added in 9f97147cc3. Adding
a comment to clarify the use.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-17 14:15:45 -05:00
Michał KłeczekandTaimoor Zaeem 9f97147cc3 perf: add ghc 9.12 options -fexpose-overloaded-unfoldings -fspecialise-aggressively
Before 9.12.1 it was necessary to mark functions as INLINABLE or INLINE to make GHC consider cross-module specialization of polymorphic functions.
9.12 added a new -fexpose-overloaded-unfoldings flag that exposes optimized polymorphic functions in interface files. -fspecialise-aggressively then makes GHC apply aggresive specialization.
2026-06-17 13:13:48 +05:00
Taimoor ZaeemandSteve Chavez 9048332b0e test(io): clarify test names
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-16 13:13:51 -05:00
Taimoor ZaeemandSteve Chavez b6c12abf0e test: move http header related tests from io tests to spec tests
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-16 13:13:51 -05:00
Michał KłeczekandSteve Chavez 8fa26ee865 fix: Start listening after schema cache load
This change ensures PostgREST starts listening on a server socket only after it loaded the schema cache and is ready to handle requests. It is no longer going to return 503 errors during startup until the schema cache is loaded.
2026-06-16 12:30:51 -05:00
Michał KłeczekandWolfgang Walther fae6253932 refactor: make nextListenerDelay local to Listener module
Right now stateNextListenerDelay is only used in Listener module.
This change is a small refactoring moving nextListenerDelay to Listener to increase cohesion and decrease coupling.
2026-06-14 18:39:59 +00:00
Wolfgang Walther 2976eb047b add: support running admin server on unix socket
This is useful when multiple instances run on the same machine, for
example behind a proxy. Unix sockets for web and admin servers can then
be put in the same folder for each instance.

Can be helpful when writing tests as well.
2026-06-14 17:18:37 +00:00
Wolfgang Walther f24bc7092f test(io): fix UnboundLocalError
The `if response` check a few rows down would not actually work without
initializing the variable first. It'd throw:

```
E       UnboundLocalError: cannot access local variable 'response' where
it is not associated with a value
```
2026-06-14 17:18:37 +00:00
Wolfgang Walther 6d87fc5879 test(io): move some tests to unix sockets
No need to run these with TCP and freeport, I believe.
2026-06-14 17:18:37 +00:00
Michał KłeczekandSteve Chavez 54430c3a2c fix: Do not clear the schema cache during retries
retryingSchemaCacheLoad should not clear existing schema cache upon failure - there is no reason to do that. If there is a communication issue with the database server or db is down, clients are going to get 502 anyway. If it was a glitch when loading the schema cache - the clients are going to use old (stale) schema cache for some time until next retry re-loads it successfully.
2026-06-11 13:14:37 -05:00
Taimoor Zaeem e9a016670f Revert "add: string slicing operator for jwt-role-claim-key"
This reverts commit fe0386e9c4.

As discussed in https://github.com/PostgREST/postgrest/pull/4984#issuecomment-4652725178.
2026-06-11 23:07:05 +05:00
steve-chavez 1ebf480258 amend: required membership for postgrest_test_anonymous
Previous test correction required GRANTing membership for
postgrest_test_anonymous.
2026-06-11 12:00:02 -05:00
steve-chavez e7c8d70333 test: correct test_admin_ready_includes_schema_cache_state
There was an oversight on d556cea, `timeout_authenticator` doesn't have
any privileges on the tables so it shouldn't be making requests to the
API server or it will end up with 401 responses.
2026-06-11 11:51:14 -05:00
Wolfgang Walther 9097752888 ci(release): lowercase ghcr repo
ghcr does not support uppercase letters, which was not a problem when
testing in my fork. But the PostgREST org's name obviously has uppercase
letters in it, so we need to lowercase these for the ghcr namespace.
2026-06-10 14:18:12 +02:00
Wolfgang Walther 62503466a0 Revert "ci(ci): avoid running build jobs twice on pushes to main"
This reverts commit 840d3f99ed.

This was a nice idea in theory, but in practice this means that the
build jobs do not run at all on the default branch anymore. This means
the caches they push to won't be used by any other jobs, neither on the
tag pipelines, nor on PRs. This in turn makes all of these jobs really
slow.

Not helpful!
2026-06-10 14:04:59 +02:00
Wolfgang Walther 1560078ce2 ci: push container images to ghcr.io
Resolves #2836
2026-06-10 13:17:37 +02:00
Taimoor ZaeemandWolfgang Walther 8bde0ad474 test: move cors related tests from io tests to spec tests
Towards #4946.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-10 10:27:49 +00:00
Taimoor ZaeemandWolfgang Walther 21433d10c2 refactor(config): use correct data type for server-cors-allowed-origins
Replaces the `Maybe [Text]` with `[Text]`. The `Maybe` is unnecessary
because we handle `Just []` and `Nothing` the same way.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-09 12:35:47 +00:00
Wolfgang Walther 044d623424 ci(test): fail loadtest correctly
Previously this would only fail if *each* row had `:x:` in it, which was
never the case, because the header never has it. It is supposed to fail
when *at least one* row has an .
2026-06-09 12:35:25 +00:00
renovate[bot]andWolfgang Walther 0b5655b9d3 chore(deps): update codecov/codecov-action action to v7 2026-06-07 11:01:07 +00:00
steve-chavez 4df0654998 docs: add how-to for Query Identifier 2026-06-06 06:47:33 -05:00
steve-chavez ad790baefb test: explain verbose reveals query identifier
This also requires enabling shared_preload_libraries=pg_stat_statements.
Really we only need compute_query_id=on, but pg_stat_statements is
useful for other tests and it already enables compute_query_id (since
it's `auto` by default).
2026-06-06 06:47:33 -05:00
Wolfgang Walther cb6fc55a2d chore(deps): update nixpkgs, stackage and hackage pins 2026-06-05 20:29:48 +00:00
Wolfgang Walther b114bb9395 nix: replace silver-searcher with fd
The former has been removed from Nixpkgs and won't be available on the
next bump.
2026-06-05 20:29:48 +00:00
Taimoor ZaeemandSteve Chavez da8738b88e chore: add changelog entry for v14.13
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-05 13:30:59 -05:00
renovate[bot]andWolfgang Walther 3ca876da62 chore(deps): update actions/checkout action to v6.0.3 2026-06-04 09:09:49 +00:00
Laurence IslaandSteve Chavez 416a15480f fix: request failures when work_mem is set on a role 2026-06-03 13:45:34 -05:00
Wolfgang Walther a149405d11 ci: remove deprecation warning
app-id was replaced with client-id
2026-06-03 10:09:38 +02:00
netandWolfgang Walther 3d596e2dbc fix: don't mention retrying in the database connection error message
The PGRST000 database connection error message was "Database connection
error. Retrying the connection.", but reconnection attempts are already
logged separately by the reconnection observation, and on fatal errors
(e.g. authentication failure) PostgREST does not retry at all. Drop the
"Retrying the connection." part, leaving "Database connection error.".
2026-06-03 08:04:40 +00:00
Pedro Gil MoraandWolfgang Walther fae58174fa docs: Add efcore-postgrest library to client-side libraries 2026-06-02 08:19:45 +00:00
Wolfgang Walther 840d3f99ed ci(ci): avoid running build jobs twice on pushes to main
It's enough to run the build jobs in the release pipeline, no need to
run them on the push event as well.
2026-06-02 10:08:16 +02:00
Wolfgang Walther 871aa058f8 ci(release): only update docker description from devel release
This condition was accidentally removed when merging 4 release jobs into
2.
2026-06-02 10:07:37 +02:00
Wolfgang Walther 8552c66c1e ci: Use FreeBSD VM instead of cirrus 2026-06-02 07:36:59 +00:00
netandWolfgang Walther 097a1de67d chore: sort config dump alphabetically
The config dump in Config.hs listed db-config, db-pre-config and the
db-pool-* settings out of alphabetical order, while the rest of the dump
was sorted. Now the whole dump is consistently alphabetical and update
the expected IO test configs to match.
2026-06-02 07:32:11 +00:00
Wolfgang Walther ed4b68c364 test(spec): remove obsolete extra search path from postgis tests
This was brought over in the last commit, because the postgis was
re-using the same config as another test. Now it has a separate config,
so we can reduce it a bit.
2026-06-02 06:49:15 +00:00
Wolfgang Walther 268ab00ed9 test(spec): inline config into test suite
Previously, information about each test-suite was repeated in 3 separate
places:
- as a label and as implicit knowledge in the test-suite itself,
- as a comment in Main.hs, and
- as a configuration in SpecHelper.hs.

With this change, there will be a single source of truth in the test
suite itself. This will allow a single test-suite to easily test
multiple different configurations.
2026-06-02 06:49:15 +00:00
Wolfgang Walther 7803960cd1 test(spec): fix test labels 2026-06-02 06:49:15 +00:00
Wolfgang Walther 0e46474cb2 test(spec): fix typo in comment 2026-06-02 06:49:15 +00:00
Wolfgang Walther 7c806a2bf9 test(spec): move two tests with basic config into list
Everything that uses withApp is in that list.
2026-06-02 06:49:15 +00:00
Wolfgang Walther c2d32be3c5 test(spec): remove obsolete ANALYZE calls
These are now part of the with- helpers.
2026-06-02 06:49:15 +00:00
Michał KłeczekandTaimoor Zaeem 13c0e7061e refactor: simplify control flow in App.postgrest
Currently, authentication and response execution each unwrap ExceptT with separate runExceptT calls, which split the main request flow across nested pattern matching and Either handling. Control flow is complex and difficult to understand.

The goal of this change is to make request execution as sequential
monadic code with clear error handling.

To implement that, request handling is now run in ExceptT over WriterT (Last ByteString) IO monad stack. Auth role is written after authentication succeeds and further returned along the response. Thanks to it response observation generation is centralized at the end of request handling.

It was necessary to abstract monad stack in getAuthResult, lookupJwtCache, postgrestResponse, and withTiming to enable introduction of WriterT.
2026-06-02 11:28:19 +05:00
Wolfgang Walther 1d6e0bd35f nix(loadtest): report percentage change
This reports the percentage change between the current head branch and
the main branch, which is exactly the number we'll want to make our
decisions on "success or fail" on.

CI failures will initially be reported for regressions of 5% or more on
an individual number.
2026-06-01 20:49:35 +00:00
Wolfgang Walther ff92846e64 nix(loadtest): report multiple percentiles at once
We can look at the data over time and see which percentile works best
for our purpose.
2026-06-01 20:49:35 +00:00
Wolfgang Walther 640039a180 nix(loadtest): move errors into mixed
Because we separate loadtest results per URL now, we can move the error
tests into the regular mixed bag of loadtests - we will be able to tell
from the misspelled URLs when we hit a regression in that area.

We should be able to do similar things for JWT tests, but we'll need
more infrastructure here.
2026-06-01 20:49:35 +00:00
Wolfgang Walther 59dffc258a nix(loadtest): report latency instead of rate
We previously used "rate", i.e. number of requests per second, as the
primary metric to judge loadtest results. However, this has always been
varying from run to run quite a bit, especially in CI where other jobs
possibly run on the same VM host.

The run-to-run variance has massively increased after splitting the
results up per request. Example run in CI with rate on the PR
introducing this change (on which we would expect no change at all):

| rate [1/s]                         |   main |   head |    Δ |
|:-----------------------------------|-------:|-------:|-----:|
| /                                  |    9.4 |    9.5 |   1% |
| /actors                            |  870.4 | 1023.0 |  18% |
| /actors?actor=eq.1                 |  188.5 |  198.6 |   5% |
| /actors?actor=eq.1&columns=name    |  197.3 |  167.1 | -15% |
| /actors?select=*,roles(*,films(*)) |  153.9 |  144.9 |  -6% |
| /films?columns=id,title            |  157.9 |  182.6 |  16% |
| /films?columns=id,title,year,...   |   87.0 |   87.1 |   0% |
| /roles                             |  204.5 |  267.3 |  31% |
| /rpc/call_me                       |  231.3 |  208.8 | -10% |
| /rpc/call_me?name=John             |  212.2 |  201.7 |  -5% |

From the data we can easily tell that the very reason that rate as a
paramter has only worked, so far, because the data was *heavily*
dominated by the requests on the root endpoint for OpenAPI. The longer
duration makes the request much less vulnerable for concurrent activity.
For all other requests its essentially not possible to judge the effect
of a PR this way.

One way to counter this would be to massively increase the time the
loadtest runs. More samples will result in a smoother average. However,
that's not practical for usability of CI. In the original PR #1812 I
already evaluated using the *minimum latency* as the most reliable
criteriumi, but this has never really caught on. The theory behind this
is: The variation in timings between requests is happening because of
concurrent activity, priority chosen by the scheduler, availability of
resources and such - all factors *outside* our control, and *irrelevant*
to the Haskell code we're writing.

Using the minimum latency is an estimation of how fast the code can run
*in the best case*. This might not be a number relevant for production,
but it's much more directly related to the code we write.

Here's to show how variation becomes *much* smaller with minimum latency
as the parameter:

| min latency [μs]                   |     main |   head |    Δ |
|:-----------------------------------|---------:|-------:|-----:|
| /                                  |   1275.3 | 1263.6 |  -1% |
| /actors                            |     10.0 |    9.9 |  -1% |
| /actors?actor=eq.1                 |     50.7 |   48.3 |  -5% |
| /actors?actor=eq.1&columns=name    |     54.1 |   54.0 |   0% |
| /actors?select=*,roles(*,films(*)) |     63.2 |   61.9 |  -2% |
| /films?columns=id,title            |     51.1 |   50.7 |  -1% |
| /films?columns=id,title,year,...   |    121.9 |  121.8 |   0% |
| /roles                             |     42.9 |   42.6 |  -1% |
| /rpc/call_me                       |     45.6 |   45.4 |   0% |
| /rpc/call_me?name=John             |     44.4 |   44.2 |   0% |

Since we're separating results per request now, we can only sensibly
focus on *one* parameter - otherwise this would get really clunky
UI-wise. Especially for automated CI failures, minimum latency is the
logical choice.

This commit starts using minimum latency, i.e. P0, but any percentile
should be an improvement over the status quo. A later commit will change
to a different P-value.
2026-06-01 20:49:35 +00:00
Wolfgang Walther 8a9f98371e nix(loadtest): group results by status, method and URL
Different requests hit different code paths and perform very
differently. By looking at each request type separately, we should be
able to get a much better idea of what kind of change in performance
we're looking at and where the root cause might be.

It will hopefully also allow us to migrate some of the other test-cases
into the main loadtest.
2026-06-01 20:49:35 +00:00
Wolfgang Walther 5cdf2e0569 nix(loadtest): remove noise from report
Ultimately, we only look at the `rate` column, so we can just as well
remove all other columns.

This makes the next step, when we split results by request type, much
less noisy.
2026-06-01 20:49:35 +00:00
Taimoor ZaeemandSteve Chavez 669fc9ede8 docs: restructure PostGIS page
Restructure PostGIS page with order:

- `Accept: application/geo+json` request example
- Generated column example
- String representation example

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-01 12:59:15 -05:00
Michał KłeczekandSteve Chavez 0df56f9ea8 refactor: assemble main transaction session in MainTx
This change makes the API surface between MainTx and App smaller.

Currently, App reconstructs a database transaction by unpacking the isolation
level, transaction mode, DbHandler, and transaction runner returned by MainTx.
That exposes MainTx internals at the call site even though MainTx already owns
query setup, execution, decoding, and rollback behavior.

The goal is to keep transaction assembly in MainTx while App remains responsible
for pool execution, database error mapping, and response orchestration. DbTx now
carries the assembled SQL session, and App passes that session directly to the
connection pool.
2026-05-31 15:52:56 -05:00
Michał KłeczekandSteve Chavez 56ad5a9324 refactor: get rid of dbQueryTimings from SchemaCache
Schema cache query timings are only needed immediately after a schema-cache reload to emit SchemaCacheQueriedObs. Storing them inside SchemaCache makes the cache carry transient observability data that is not part of the cached schema state and is never used by request handling.

This change makes querySchemaCache to return query timings in a tuple in parallel to SchemaCache and removes dbQueryTimings field.
2026-05-31 14:20:35 -05:00
Taimoor ZaeemandSteve Chavez 9162cea7ba test(io): remove unused config variables from config test
Config variables are tested already via reading the config files in
the `configs/` directory.

If more are to be tested, it should be done via adding a file in
`configs/` and compare it with its associated file in `configs/expected/`.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-30 09:51:26 -05:00
Taimoor ZaeemandSteve Chavez e1d547623e nix(docs): print message to console on script runs
It's not immediately visible that these scripts are run when
doing `postgrest-docs-check`.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-27 15:23:59 -05:00
Michał KłeczekandSteve Chavez 998d232ac0 docs: PGRST107 error should map to 406 not to 415 2026-05-27 14:37:23 -05:00
Michał KłeczekandWolfgang Walther 1a6ba2072c refactor: Simplify App.initServerSocket
This change gets rid of unnecessary explicit bindRandomPortTCP in initServerSocket. Returned port value was ignored in removed code anyway as assigned port retrieval from an open socket is handled elsewhere.
2026-05-24 23:56:05 +02:00
renovate[bot]andWolfgang Walther ae00c04faf chore(deps): update docker/login-action action to v4.2.0 2026-05-22 20:54:17 +00:00
renovate[bot]andWolfgang Walther e0e14a1fe4 chore(deps): update docker/setup-buildx-action action to v4.1.0 2026-05-22 20:52:03 +00:00
Taimoor ZaeemandSteve Chavez 901624a891 chore: add changelog entry for v14.12
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-21 10:26:17 -05:00
Michał KłeczekandSteve Chavez 913fe001e5 refactor(remove): schema cache load delay config
Remove internal schema cache load and relationship load sleep settings plus
the delay wrappers they enabled. Drop IO tests that depended on the removed
settings.
2026-05-19 10:59:27 -05:00
Michał KłeczekandSteve Chavez 86d6ed10bb test: cover stale schema cache database error
Add an IO test that drops a table while schema cache reload is delayed. It verifies the stale cache path returns PostgreSQL 42P01 and the refreshed cache returns PGRST205.
2026-05-19 09:11:04 -05:00
renovate[bot]andWolfgang Walther 55f9c45899 chore(deps): update codecov/codecov-action action to v6.0.1 2026-05-19 07:04:42 +00:00
Michał KłeczekandTaimoor Zaeem 07938aaab2 amend: Move #4622 to proper Changelog section 2026-05-19 10:55:53 +05:00
Wolfgang Walther 61da5dd88b nix(loadtest): prevent timing errors for worst-case JWT test
Instead of taking wild guesses at the runtime of the target generation
itself, we're just making sure to reset the system time to a fixed value
when we ultimately start PostgREST. This allows us to create the right
JWT expiry values ahead of time.
2026-05-18 16:23:37 +00:00
Wolfgang Walther d9f73ac71b nix(loadtest): remove duplicate line 2026-05-18 16:23:37 +00:00
Taimoor ZaeemandWolfgang Walther e6d8d95c0a chore: remove unplanned TODO in Logger.hs
We decided against doing this in #4913, therefore removing it. With that,
also mentioning that control flow never reaches there to avoid confusion.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-18 16:23:08 +00:00
Wolfgang Walther 9d67febb93 ci(release): merge 4 jobs into 2
No need to do this in as many jobs. Splitting all the github stuff from
docker stuff into two jobs is enough. This still allows to conditionally
enable docker jobs in contributors repos, depending on whether the
relevant docker credentials are provided - but avoids using too many
concurrent runners.
2026-05-18 16:22:49 +00:00
steve-chavez 6220ab3f31 test: adjust test_positive_pool_metric
Adjusted so it waits for liveness instead of requiring sleeping
2026-05-18 10:46:02 -05:00
steve-chavez 1eba9855c1 test: change wait_for_readiness to an enum
This so we can wait for both readiness and liveness
2026-05-18 10:45:36 -05:00
Michał KłeczekandSteve Chavez a297391071 fix: implement connection tracking in metrics
Right now metrics observation handler does not track database connections but updates a single Gauge based on HasqlPoolObs events. This is problematic because Hasql pool reports various connection events in multiple phases. The connection state machine is not simple and to precisely report the number of connections in various states, it is necessary to track their lifecycles.

This change adds a ConnTrack data structure and logic to track database connections lifecycles. At the moment it supports "connected" and "inUse" connection counts precisely. The "pgrst_db_pool_available" metric is implemented on top of ConnTrack instead of a simple Gauge.
2026-05-18 10:09:00 -05:00
Wolfgang Walther 4f9bc89ab2 nix(loadtest): remove outdated PGRST_DB_URI default
This was required for v9 and earlier, but these don't build with the
current nix invocation anymore anyway. Even loadtesting against v10 does
not work, because `--version` is used in one of the wait scripts and
this was only added in v11.2.

So no need to pretend we'd support comparing against older versions.
2026-05-18 15:18:05 +02:00
Taimoor ZaeemandWolfgang Walther 084a8eca55 test(io): move utility functions to util.py
The functions `drain_stdout` and `match_log` should be in `util.py`
so they can be reused in other modules.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-18 12:54:08 +00:00
Wolfgang Walther ed344c795a ci(release): run docker job on regular VM runner
Tried to run it on a -slim runner in fb24b6b3, but the result is that no
docker socket is available. This jobs needs one of the big runners.
2026-05-18 14:46:21 +02:00
Wolfgang Walther c335da9334 ci: avoid running pg18 tests twice
We already test PG18, the default, with the coverage job, so why run it
twice?
2026-05-18 14:06:16 +02:00
Wolfgang Walther fb24b6b37f ci: use -slim runner for small jobs
No need to spin up full VM runners for small automation tasks, when we
can use single-CPU runners in containers instead.

https://docs.github.com/en/actions/reference/runners/github-hosted-runners#single-cpu-runners

(some of this will potentially not work, because dependencies in the
slim image might not be available - however, it makes no sense to create
this as a PR, because all jobs touched here run on branches only. Thus
pushing directly to main)
2026-05-18 14:05:41 +02:00
steve-chavez 8868d13214 test: adjust sleep in test_positive_pool_metric
Otherwise it's not enough time for the pool metric to
reach a negative value.
2026-05-17 16:39:11 -05:00
Wolfgang Walther c7260bddab test: consistently analyze tables exactly once
Disabling the autovacuum daemon should also help reproducibility in
theory, although I don't know of any cases where we hit a problem with
that.

VACUUM changes the order of rows that PostgreSQL returns for some table
without explicit ordering, thus doing the latter to make it consistently
reproducible.

After ANALYZE estimates are 100% exact for the moment, so some requests
which returned 206 Partial Response now return 200 instead. The fact
that PostgREST returns 206 on an unfiltered endpoint can probably be
considered a bug.
2026-05-17 20:03:15 +00:00
Wolfgang Walther abe3e511bf nix(postgrest-with-pg-...): use postgresql.conf instead of CLI arguments
This is easier to reason about and easier to extend. Also removes a tiny
bit of repetition.
2026-05-17 20:03:15 +00:00
steve-chavez 70327cf869 test: negative pgrst_db_pool_available in metrics
Proves the failure on https://github.com/PostgREST/postgrest/issues/4622.

This doesn't require additional test infra, only nginx. Taking advantage
of the `stream {}` context which is also compatible with unix socket
besides TCP.
2026-05-17 12:58:58 -05:00
Wolfgang Walther a9713208e4 ci: ignore loadtest failures for jwt-rsa-cache-worst 2026-05-15 23:25:38 +02:00
Wolfgang Walther 2783ec7b51 ci: run other loadtests to completion when one of them fails
The most likely offender to fail early is jwt-rsa-cache-worst - which
then cancels *all* loadtests at once. This is unnecessary churn for CI.
Instead we can re-run only that one test later.
2026-05-15 23:21:54 +02:00
Taimoor Zaeem ab43ee1fbb docs: add redirect of the moved postgis section
In 055921ea, we missed redirecting the old link to the new one.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-15 23:01:36 +05:00
renovate[bot]andWolfgang Walther 2ab2ea2c6c chore(deps): update dependency macos to v26 2026-05-14 19:45:32 +00:00
Aiden ParkandWolfgang Walther 703a5535cf chore: remove duplicated words in Listener and Plan comments
Signed-off-by: Aiden Park <275402320+vip892766gma@users.noreply.github.com>
2026-05-14 14:23:09 +00:00
renovate[bot]andWolfgang Walther 187ea6d546 chore(deps): update actions/create-github-app-token action to v3.2.0 2026-05-13 21:01:47 +00:00
Taimoor Zaeem 055921eaf5 docs: move PostGIS under Integrations
For better visibility.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-13 12:41:44 +05:00
Taimoor ZaeemandWolfgang Walther f90d7d7296 ci: catch linkcheck failure
The scheduled linkcheck job fails without notification to maintainers.
To catch the failure, a github workflow step has been added which
comments on issue #4106 so that the maintainers get notified.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-12 17:27:45 +00:00
renovate[bot]andWolfgang Walther 27aea13798 chore(deps): update cachix/cachix-action digest to 5f2d7c5 2026-05-12 13:10:57 +00:00
renovate[bot]andWolfgang Walther d75beed5dd chore(deps): update korthout/backport-action digest to 6606540 2026-05-12 13:03:16 +00:00
Wolfgang Walther 243bbb1b61 test: make some tests more stable
These fail with orioledb, because it randomly returns a different order.
Of course, that's allowed - ordering is not guaranteed unless specified.
We should write our tests with more explicit ordering in general, even
if multiple major versions show the same behavior.
2026-05-08 18:03:54 +02:00
Taimoor ZaeemandSteve Chavez 98f8e52b46 refactor: remove auth and logging middleware
This commit removes auth middleware for it hides
side effects and obscures logic. The auth operations
are now done in its own stage in the request-response
cycle.

It also removes the logging middleware because now
we instead use observation module to log the response.
2026-05-07 11:47:17 -05:00
Wolfgang Walther 0bba1d265a ci: test NixOS' VM test for PostgREST
This adds a CI job to run our latest postgrest version against the NixOS
VM test currently available in Nixpkgs. Now, this will not always be in
sync, so has the potential to be failing. However, since the Nixpkgs VM
test is really simple, this should only happen when we introduce a
breaking change on a very fundamental level. The failing test will then
be resolved once the new version is available in Nixpkgs and we have
updated our lock file.

This is essentially just a sanity check to make sure we're not breaking
something fundamentally - and if we do, it's a head up for me to adjust
the Nixpkgs tests accordingly. Those might otherwise break unnoticed
since Nixpkgs does not have a good notification system for such
breakages in place.

We do use the chance to run the static executable in this test, which
was previously not tested at all.

It also gives us a first test whether NixOS VM tests work well in GitHub
Actions.
2026-05-07 07:10:56 +00:00
Taimoor ZaeemandWolfgang Walther 96f12f83f0 ci: cancel previous runs for tag pipelines
Reverts the change in ad5bb38d70.

https://github.com/orgs/community/discussions/13015 is now solved.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-07 07:08:41 +00:00
steve-chavez 02a1de2afe test: OPTIONS produce logs but not CORS preflight 2026-05-06 17:21:31 -05:00
renovate[bot]andWolfgang Walther beafd0b53f chore(deps): update korthout/backport-action digest to bf97bcf 2026-05-06 19:47:13 +00:00
Taimoor Zaeem 4c04572783 nix(chore): add commit prefix in release commit message
We should follow our commit convention.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-06 23:26:06 +05:00
steve-chavez 6b3ec5d9e0 test: empty db-anon-role lacks role in logs 2026-05-05 19:29:59 -05:00
steve-chavez 6804ce3cbe test: empty schema cache error has role in logs 2026-05-05 19:29:59 -05:00
steve-chavez 42e53e84e1 test: invalid rpc call contains role in logs 2026-05-05 19:29:59 -05:00
steve-chavez 8105375b64 test: expired JWT shows empty role in logs 2026-05-05 19:29:59 -05:00
steve-chavez e39740cca3 nix: allow empty PGRST_DB_ANON_ROLE on postgrest-run
So we can do `PGRST_DB_ANON_ROLE="" postgrest-run` since empty string has
meaning. argbash conflates both unset and empty string so this requires
a workaround.
2026-05-05 17:32:50 -05:00
Taimoor ZaeemandWolfgang Walther c8e7f11688 chore: add changelog entry for v14.11
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-05 15:53:15 +00:00
Taimoor Zaeem fd3f9378ac fix: functions count in startup logs not counting overloaded functions
Renames the "Functions" in the logs to "RPCs". This clarifies that
we log number of callable functions and not the number of SQL objects.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-04 20:42:20 +05:00
Michał KłeczekandWolfgang Walther 85d91e5c0a add: Publish GHC runtime metrics to Prometheus metrics endpoint 2026-05-03 11:41:02 +00:00
Laurence IslaandGitHub c09394517d fix: call pg_notification_queue_usage() automatically when the LISTEN channel bug is detected (#4858)
There's a PostgreSQL bug that doesn't let any listener to register in the DB:
https://www.postgresql.org/message-id/flat/CAK98qZ3wZLE-RZJN_Y%2BTFjiTRPPFPBwNBpBi5K5CU8hUHkzDpw%40mail.gmail.com
The only workaround is to advance the async notification queue tail,
which can be done by executing:  "SELECT pg_notification_queue_usage();".
Before we just logged a HINT with this suggestion, but now we call that function directly
and then let the listener to automatically recover.

No automated tests were added here as it would be too complex and this is a PostgreSQL bug. But this was manually tested following the steps on https://github.com/PostgREST/postgrest/pull/4581#issuecomment-3690610592
2026-04-30 18:22:59 -05:00
Michal KleczekandGitHub 04a0e041e4 fix: Restore showing LISTEN pgrst in pg_stat_activity 2026-04-30 17:21:12 -05:00
251 changed files with 7240 additions and 8022 deletions
-42
View File
@@ -1,42 +0,0 @@
freebsd_instance:
image_family: freebsd-14-3
build_task:
# Don't change this name without adjusting .github/workflows/build.yaml
name: Build FreeBSD (Stack)
install_script: pkg install -y postgresql16-client hs-stack git
only_if: |
$CIRRUS_TAG != '' || $CIRRUS_BRANCH == 'main' || $CIRRUS_BRANCH =~ 'v*' ||
changesInclude(
'.github/workflows/build.yaml',
'.github/actions/artifact-from-cirrus/**',
'.cirrus.yml',
'postgrest.cabal',
'stack.yaml*',
'**.hs'
)
stack_cache:
folders: /.stack
fingerprint_script:
- echo $CIRRUS_OS
- stack --version
- md5sum postgrest.cabal
- md5sum stack.yaml.lock
stack_work_cache:
folders: .stack-work
fingerprint_script:
- echo $CIRRUS_OS
- stack --version
- md5sum postgrest.cabal
- md5sum stack.yaml.lock
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
build_script: |
stack build -j 1 --local-bin-path . --copy-bins
strip postgrest
bin_artifacts:
path: postgrest
@@ -1,119 +0,0 @@
name: Artifact from Cirrus
description: Waits for a specific Cirrus CI run to complete, then downloads the artifact and uploads it to the current workflow. This will silently succeed if Cirrus CI did not schedule a task within 2 minutes.
inputs:
download:
description: Name of Artifact to download from Cirrus CI
required: true
task:
description: Name of Cirrus Task
required: true
token:
description: GitHub Token
required: true
upload:
description: Name of Artifact to upload on GitHub Actions
required: true
runs:
using: composite
steps:
- shell: bash
run: echo "GH_TOKEN=${{ inputs.token }}" >> "$GITHUB_ENV"
- name: Wait for Check Suite to be created
id: check-suite
env:
# GITHUB_SHA does weird things for pull request, so we roll our own:
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
shell: bash
run: |
get_check_runs_url() {
gh api "repos/{owner}/{repo}/commits/${COMMIT}/check-suites" \
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
}
for _ in $(seq 1 12); do
check_runs_url="$(get_check_runs_url)"
if [ -z "$check_runs_url" ]; then
echo "Cirrus CI task has not started, yet. Waiting..."
sleep 10
else
echo "check_runs_url=$check_runs_url" >> "$GITHUB_OUTPUT"
exit 0
fi
done
>&2 echo "Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?"
- name: Find task by name
id: find-task
if: steps.check-suite.outputs.check_runs_url
shell: bash
run: |
get_number_of_tasks() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -r '.check_runs | map(select(.name == "${{ inputs.task }}")) | length'
}
tasks="$(get_number_of_tasks)"
case "$tasks" in
0)
echo "Task not found, assuming it's skipped intentionally..."
exit 0
;;
1)
echo "task_found=1" >> "$GITHUB_OUTPUT"
exit 0
;;
*)
>&2 echo "More than 1 task with the same name found. Don't know what to do..."
exit 1
;;
esac
- name: Wait for Cirrus CI to complete task
if: steps.find-task.outputs.task_found
shell: bash
run: |
get_conclusion() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -r '.check_runs[] | select(.name == "${{ inputs.task }}" and .status == "completed") | .conclusion'
}
while true; do
conclusion="$(get_conclusion)"
if [ -z "$conclusion" ]; then
echo "Cirrus CI task has not completed, yet. Waiting..."
sleep 30
else
if [ "$conclusion" == "success" ]; then
break
else
exit 1
fi
fi
done
- name: Download artifact from Cirrus CI
if: steps.find-task.outputs.task_found
id: download
shell: bash
run: |
get_external_id() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -er '.check_runs[] | select(.name == "${{ inputs.task }}") | .external_id'
}
archive="$(mktemp)"
artifacts="$(mktemp -d)"
until curl --no-progress-meter --fail -o "${archive}" \
"https://api.cirrus-ci.com/v1/artifact/task/$(get_external_id)/${{ inputs.download }}.zip"
do
# This happens when a tag is pushed on the same commit. In this case the
# job is immediately marked as "completed" for us, so we end up here after a few
# seconds - but the actual Cirrus CI task is still running and didn't produce its artifact, yet.
echo "Artifact not found on Cirrus CI, yet. Waiting..."
sleep 30
done
unzip "${archive}" -d "${artifacts}"
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
- name: Save artifact to GitHub Actions
if: steps.find-task.outputs.task_found
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }}
if-no-files-found: error
+2 -2
View File
@@ -18,14 +18,14 @@ inputs:
runs:
using: composite
steps:
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
+35
View File
@@ -0,0 +1,35 @@
name: Run anywhere
description: Runs the same code either in a VM or on the bare machine
inputs:
vm:
description: Which VM to run on.
envs:
description: List of relevant environment variables, which might need to be copied into the VM.
prepare:
description: Code to run in a prepare step, e.g. installing dependencies.
run:
description: Code to run as the main action.
required: true
runs:
using: composite
steps:
- if: ${{ inputs.vm == 'freebsd' }}
uses: vmactions/freebsd-vm@83b151f58c6047089f4c80eb5ba2039d158ce093 # v1.5.3
with:
envs: ${{ inputs.envs }}
prepare: ${{ inputs.prepare }}
# Work around https://github.com/vmactions/freebsd-vm/issues/59
run: |
pw user add -n action -m
su action -c '${{ inputs.run }}'
- if: ${{ inputs.vm == '' }}
name: Prepare
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
run: ${{ inputs.prepare }}
- if: ${{ inputs.vm == '' }}
name: Run
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
run: ${{ inputs.run }}
+2 -2
View File
@@ -11,12 +11,12 @@ inputs:
runs:
using: composite
steps:
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
- uses: nixbuild/nix-quick-install-action@9f63be77f412a248c9d9a65a4c82cf066cdf8f0c # v35
with:
nix_conf: |-
always-allow-substitutes = true
max-jobs = auto
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
with:
name: postgrest
authToken: ${{ inputs.authToken }}
+6 -5
View File
@@ -9,7 +9,7 @@ on:
jobs:
backport:
name: Backport
runs-on: ubuntu-24.04
runs-on: ubuntu-slim
# It triggers only when PR is already merged on either:
#
# - The merge event itself (action != labeled) or
@@ -28,9 +28,9 @@ jobs:
# This actions creates the github token using the postgrest app secrets
- name: Create Github App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
@@ -38,14 +38,15 @@ jobs:
# This is required for backport action to cherry-pick the PR
- name: Fetch PR ref
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
allow-unsafe-pr-checkout: true
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ steps.app-token.outputs.token }}
# Backport action that creates the PR with given settings
- name: Create backport PR
uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5
uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6
with:
github_token: ${{ steps.app-token.outputs.token }}
pull_description: 'Backport for #${pull_number}.'
+34 -42
View File
@@ -44,7 +44,7 @@ jobs:
name: Nix - ${{ matrix.name }} static
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -68,12 +68,17 @@ jobs:
path: postgrest-docker-${{ matrix.artifact }}.tar.gz
if-no-files-found: error
- name: Test static executable with NixOS' VM test
# GHA's ARM runner does not support KVM
if: runner.arch == 'X64'
run: nix-build -A nixpkgs-nixos-test
macos:
name: Nix - MacOS
runs-on: macos-15
runs-on: macos-26
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -93,64 +98,64 @@ jobs:
fail-fast: false
matrix:
include:
- name: FreeBSD x86-64
runs-on: ubuntu-24.04
vm: freebsd
artifact: postgrest-freebsd-x86-64
deps: pkg install -y git postgresql16-client hs-stack
- name: MacOS aarch64
runs-on: macos-14
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-aarch64
deps: brew link --force libpq
- name: MacOS x86-64
runs-on: macos-15-intel
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-x86-64
deps: brew link --force libpq
- name: Windows
runs-on: windows-2022
cache: |
C:\sr\pantry
C:\sr\snapshots
C:\sr\stack.sqlite3
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
artifact: postgrest-windows-x86-64
name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
env:
# Putting .stack in the working directory helps with moving this in and out of the FreeBSD VM.
STACK_ROOT: ${{ github.workspace }}/.stack
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- if: ${{ !matrix.vm }}
uses: haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d # v2.12.0
with:
# This must match the version in stack.yaml's resolver
ghc-version: 9.10.3
enable-stack: true
stack-no-global: true
stack-setup-ghc: true
- name: Cache ~/.stack
- name: Cache .stack
uses: ./.github/actions/cache-on-main
with:
path: ${{ matrix.cache }}
prefix: stack
path: .stack
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
- name: Cache .stack-work
uses: ./.github/actions/cache-on-main
with:
path: .stack-work
save-prs: true
prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
- name: Install dependencies
if: matrix.deps
run: ${{ matrix.deps }}
- name: Build with Stack
run: stack build --lock-file error-on-write --local-bin-path result --copy-bins
- name: Strip Executable
run: strip result/postgrest*
uses: ./.github/actions/run-anywhere
with:
vm: ${{ matrix.vm }}
envs: STACK_ROOT
prepare: ${{ matrix.deps }}
run: |
stack build --lock-file error-on-write --local-bin-path result --copy-bins
strip result/postgrest*
- name: Save built executable as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -161,19 +166,6 @@ jobs:
if-no-files-found: error
freebsd:
name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
task: Build FreeBSD (Stack)
download: bin
upload: postgrest-freebsd-x86-64
cabal:
strategy:
matrix:
@@ -182,8 +174,8 @@ jobs:
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d # v2.12.0
with:
ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
name: Lint & Style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -36,7 +36,7 @@ jobs:
name: Commit
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
- name: Setup Nix Environment
+3 -4
View File
@@ -41,16 +41,15 @@ jobs:
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
cancel-in-progress: true
if: vars.RELEASE_ENABLED
runs-on: ubuntu-24.04
runs-on: ubuntu-slim
needs:
- docs
- test
- build
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
name: Build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -42,7 +42,7 @@ jobs:
name: Spellcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
+27 -2
View File
@@ -7,12 +7,37 @@ on:
jobs:
linkcheck:
name: Linkcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: docs.linkcheck.bin
- run: postgrest-docs-linkcheck
- name: Run Linkcheck
id: linkcheck
run: postgrest-docs-linkcheck
# This actions creates the github token using the postgrest app secrets
- name: Create Github App Token (Runs only on linkcheck failure)
id: app-token
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }} # only create the token on linkcheck failure
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
permission-issues: write # required for commenting on issues
- name: Notify on linkcheck failure by commenting on GH Issue 4106
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }}
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: 4106
body: |
**Linkcheck Job Failed!**
A broken link was detected in the docs. Please check the [failed run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
+66 -37
View File
@@ -9,8 +9,7 @@ on:
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
cancel-in-progress: true
jobs:
build:
@@ -20,13 +19,15 @@ jobs:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
prepare:
name: Prepare
runs-on: ubuntu-24.04
github:
name: GitHub
permissions:
contents: write
runs-on: ubuntu-slim
needs:
- build
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check the version to be released
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
@@ -48,23 +49,7 @@ jobs:
echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md
- name: Save CHANGES.md as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -116,14 +101,14 @@ jobs:
gh release edit devel \
-t devel \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
-F 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 \
-F CHANGES.md \
release-bundle/*
fi
@@ -132,13 +117,13 @@ jobs:
name: Docker Hub
runs-on: ubuntu-24.04-arm
needs:
- prepare
- github
if: |
vars.DOCKER_REPO && vars.DOCKER_USER
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download aarch64 Docker image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -147,8 +132,8 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-docker-x86-64
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
@@ -178,16 +163,9 @@ jobs:
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
if: github.ref == 'refs/tags/devel'
name: Docker Hub Description
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
@@ -195,3 +173,54 @@ jobs:
short-description: ${{ github.event.repository.description }}
readme-filepath: ./docker-hub-readme.md
ghcr:
name: GitHub Container Registry
runs-on: ubuntu-24.04-arm
needs:
- github
permissions:
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download aarch64 Docker image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-docker-aarch64
- name: Download x86-64 Docker image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-docker-x86-64
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish images on Docker Hub
run: |
docker load -i postgrest-docker-aarch64.tar.gz
docker tag postgrest:latest "ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-arm64"
docker push "ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-arm64"
docker load -i postgrest-docker-x86-64.tar.gz
docker tag postgrest:latest "ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-amd64"
docker push "ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-amd64"
docker manifest create "ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}" \
"ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-arm64" \
"ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-amd64"
docker manifest push "ghcr.io/${GITHUB_REPOSITORY,,}:${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 manifest create "ghcr.io/${GITHUB_REPOSITORY,,}:latest" \
"ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-arm64" \
"ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_REF_NAME}-linux-amd64"
docker manifest push "ghcr.io/${GITHUB_REPOSITORY,,}:latest"
else
echo "Skipping push to 'latest' tag for pre-release..."
fi
+51 -18
View File
@@ -25,6 +25,10 @@ on:
- test/**
- '!**.md'
defaults:
run:
shell: bash
concurrency:
# Terminate all previous runs of the same workflow for pull requests
group: test-${{ github.head_ref || github.run_id }}
@@ -40,7 +44,7 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -52,14 +56,14 @@ jobs:
- name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
- name: Run doctests
if: always()
run: postgrest-test-doctests
run: nix-shell --run postgrest-test-doctests
- name: Check the spec tests for idempotence
if: always()
@@ -70,8 +74,9 @@ jobs:
strategy:
fail-fast: false
matrix:
pgVersion: [14, 15, 16, 17, 18]
name: PG ${{ matrix.pgVersion }}
# Latest version is tested via `coverage` above.
pgVersion: [pg-14, pg-15, pg-16, pg-17, oriole-18, pg-18]
name: ${{ matrix.pgVersion }}
runs-on: ubuntu-24.04
defaults:
run:
@@ -79,37 +84,37 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.${{ matrix.pgVersion }}.bin cabalTools.update.bin
- run: postgrest-cabal-update
- name: Run spec tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec
run: postgrest-with-${{ matrix.pgVersion }} postgrest-test-spec
- name: Run observability tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
run: postgrest-with-${{ matrix.pgVersion }} postgrest-test-observability
- name: Run IO tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv
run: postgrest-with-${{ matrix.pgVersion }} postgrest-test-io -vv
- name: Run IO tests on a big schema
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
run: postgrest-with-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
memory:
name: Memory
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -124,19 +129,20 @@ jobs:
loadtest:
strategy:
fail-fast: false
matrix:
kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
kind: ['mixed', 'jwt-cache']
name: Loadtest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: loadtest.loadtestAgainst.bin loadtest.report.bin cabalTools.update.bin
tools: loadtest.loadtestAgainst.bin loadtest.report.bin loadtest.report-load.bin cabalTools.update.bin
- run: postgrest-cabal-update
@@ -150,21 +156,48 @@ jobs:
latest_tag=$(git tag --merged HEAD --sort=-creatordate "v*" | head -n1)
fi
postgrest-loadtest-against -k ${{ matrix.kind }} "$TARGET_BRANCH" "$latest_tag"
postgrest-loadtest-report -g ${{ matrix.kind }} >> "$GITHUB_STEP_SUMMARY"
- name: Report P50
# This step checks whether any red cross indicators (:x:) are present in the step summary.
# The loadtest reporter writes them when any of individual steps fails the performance
# regression threshold.
run: |
! (postgrest-loadtest-report -g ${{ matrix.kind }} -p 50 \
| tee "$GITHUB_STEP_SUMMARY" \
| grep ':x:')
- name: Report P0
if: always()
run: |
postgrest-loadtest-report -g ${{ matrix.kind }} -p 0 >> "$GITHUB_STEP_SUMMARY"
- name: Report P90
if: always()
run: |
postgrest-loadtest-report -g ${{ matrix.kind }} -p 90 >> "$GITHUB_STEP_SUMMARY"
- name: Report P95
if: always()
run: |
postgrest-loadtest-report -g ${{ matrix.kind }} -p 95 >> "$GITHUB_STEP_SUMMARY"
- name: Report CPU/MEM
if: always()
run: |
postgrest-loadtest-report-load -g ${{ matrix.kind }} >> "$GITHUB_STEP_SUMMARY"
flake:
strategy:
fail-fast: false
matrix:
runs-on:
- macos-15-intel # x86_64-darwin
- macos-14 # aarch64-darwin
- ubuntu-24.04 # x86_64-linux
- ubuntu-24.04-arm # aarch64-linux
name: Flake Check
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Nix Environment
+5 -1
View File
@@ -25,5 +25,9 @@ loadtest
.history
.docs-build
gen_targets.http
gen_jwk.json
gen_jwks.json
gen_private.json
.pytest_cache
.ruff_cache
postgrest-module-graph.png
.ghc.environment.*
+1 -1
View File
@@ -200,7 +200,7 @@ steps:
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account. Default: 80.
columns: 70
columns: 80
# By default, line endings are converted according to the OS. You can override
# preferred format here.
+136 -22
View File
@@ -4,34 +4,148 @@ All notable changes to this project will be documented in this file. From versio
## Unreleased
### Added
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
+ Removed unnecessary double count when building the `Content-Range`.
- Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
- Add `Vary` header to responses by @develop7 in #4609
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
## [16.1] - 2026-08-10
### Fixed
- Shutdown should wait for in flight requests by @mkleczek in #4702
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
- JWT validation uses wrong current time due to a bug in auto-update by @mkleczek in #5159
### Changed
## [16.0] - 2026-08-07
### Changes
#### HTTP Server
- [Graceful shutdown](https://docs.postgrest.org/en/v16/references/http_server.html#graceful-shutdown) by @mkleczek, @Vlix in #4702
- [server-reuseport](https://docs.postgrest.org/en/v16/references/configuration.html#server-reuseport) allows starting multiple PostgREST instances using the same port on supported platforms by @mkleczek in #4703, #4694
#### Performance
- Optimize schema cache domain type resolution by using [pg_basetype](https://www.postgresql.org/docs/current/functions-info.html#FUNCTIONS-INFO-CATALOG) on PostgreSQL 17+ by @joelonsql in #4567
- [Prefer: count=exact](https://docs.postgrest.org/en/v16/references/api/pagination_count.html#exact-count) no longer does a double count on requests that do not use ranges or `db-max-rows` by @laurenceisla in #3957
- [Prefer: timezone](https://docs.postgrest.org/en/v16/references/api/preferences.html#prefer-timezone) no longer requires the schema cache by @steve-chavez in #5100
+ Previously this required caching [pg_timezone_names](https://www.postgresql.org/docs/current/view-pg-timezone-names.html) which was slow in some systems
#### Integrations
- PostgREST is now tested to work with [OrioleDB](https://github.com/orioledb/orioledb/) in #4845 by @wolfgangwalther
+ See [our guide for running OrioleDB on NixOS](https://docs.postgrest.org/en/v16/integrations/nixos.html)
#### JWT
- [JWT Role Extraction](https://docs.postgrest.org/en/v16/references/auth.html#jwt-role-extract) is now more flexible, supporting the standard JSON Path defined in RFC 9535 by @taimoorzaeem in #4984
#### API
- [Prefer: timezone](https://docs.postgrest.org/en/v16/references/api/preferences.html#timezone) now supports numeric offsets like `05:00` or `-4` by @steve-chavez in #5100
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
+ You need to set [url-use-legacy-target-names](https://docs.postgrest.org/en/v16/references/configuration.html#url-use-legacy-target-names) to `false`.
- Deprecate filters, orders and limits with the name of an embedded table when it has an alias by @steve-chavez, @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` will not be possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
+ You will see a warning in the logs and a `Warning` header on the client response when this happens.
+ You can disable this behavior now by setting `url-use-legacy-target-names = false`.
- Add `Vary` header to responses by @develop7 in #4609
- Fix automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
#### Observability
- [GHC runtime metrics](https://docs.postgrest.org/en/v16/references/observability.html#ghc-runtime-metrics) by @mkleczek in #4862
- [client-error-verbosity](https://docs.postgrest.org/en/v16/references/configuration.html#client-error-verbosity) to customize responses error verbosity by @taimoorzaeem in #4088, #3980, #3824
- [log-level](https://docs.postgrest.org/en/v16/references/configuration.html#log-level) config is now reloadable by @taimoorzaeem in #5113
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
#### Admin Server
- [admin-server-unix-socket](https://docs.postgrest.org/en/v16/references/configuration.html#admin-server-unix-socket)/[admin-server-unix-socket-mode](https://docs.postgrest.org/en/v16/references/configuration.html#admin-server-unix-socket-mode) to run the admin server on a unix socket by @wolfgangwalther in #5003
- Fix responding with `Something went wrong` on Admin server when under EMFILE by @mkleczek in #5077
#### Deployment
- Make executable for aarch64-linux static instead of Ubuntu-based by @wolfgangwalther in #4193
- Docker image for aarch64-linux is now built from scratch instead of being Ubuntu-based by @wolfgangwalther in #4193
- Besides Docker Hub, docker images are now published to Github Container Registry by @wolfgangwalther in #2836
#### Schema Cache
- Fix requests failing when the schema cache fails to reload, when this happens PostgREST will continue serving requests in "best effort" by @mkleczek in #4873 #4869
- Fix reporting 503s errors unnecessarily while the schema cache is loading at startup by @mkleczek in #4880
- Fix schema cache dump missing RPC transaction isolation level by @taimoorzaeem in #5079
#### Listener
- Fix config `db-channel-enabled` not reloading by @taimoorzaeem in #4894
### Migration to v16
- Drop support for PostgreSQL EOL version 13 by @wolfgangwalther in #4193
- All responses now include a `Vary` header by @develop7 in #4609
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
+ PostgreSQL 13 end of life was on 2025 ([ref](https://www.postgresql.org/support/versioning/))
+ Upgrade your PostgreSQL version to at least 14 to use this new PostgREST version.
- Fail at startup when `db-schemas` contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
+ Previously it failed at runtime with `PGRST205` on requests related to these schemas.
+ Remove `pg_catalog` and `information_schema` from `db-schemas`.
- `Prefer: timezone` no longer complies with `handling=lenient` and instead always fails by @steve-chavez in #5128
+ Supporting this required caching `pg_timezone_names`, which was expensive.
+ Ensure your requests always have a valid timezone.
- `jwt-role-claim-key` no longer uses the JSPath DSL and instead uses JSON Path by @taimoorzaeem in #4984
+ Now all config values must start with `$` character.
Example: `.roles.read` -> `$.roles.read`
+ Keys with special characters, with the exception of `_` char must be quoted.
Example: `.roles.write-role` -> `$.roles["write-role"]`
+ String comparison operators (`^==`, `==^` and `*==`) are replaced with regular expression search.
Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]`
+ Update the `jwt-role-claim-key` value accoring to the above rules. Also see the syntax reference: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti).
## [14.16] - 2026-07-27
### Fixed
- Fix admin server crashing without a way to recover by @taimoorzaeem in #5096
## [14.15] - 2026-07-13
### Fixed
- Fix admin server dying silently by @Vlix, @mkleczek, @steve-chavez in #5012
## [14.14] - 2026-06-29
### Fixed
- Fix admin server not logging cause of failure by @taimoorzaeem in #5012
## [14.13] - 2026-06-04
### Fixed
- Fix connection retrying message in `PGRST000` error by @netqo in #4980
+ Remove redundant "Retrying the connection." from message because it is logged separately
- Fix request failures when `work_mem` is set on a role by @laurenceisla in #4955
## [14.12] - 2026-05-20
### Fixed
- Fix race condition in pool_available metric causing negative values during network instability by @mkleczek in #4622
## [14.11] - 2026-05-04
### Fixed
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
- Restore Listener query shape so it can be found in `pg_stat_activity` by @mkleczek in #4857 #4859
- The LISTEN channel now automatically recovers when it stops working due to a PostgreSQL bug @laurenceisla in #3147
- Fix misleading "Functions" name on schema cache summary in startup logs by @taimoorzaeem in #4821
## [14.10] - 2026-04-16
+2
View File
@@ -40,6 +40,8 @@ Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/ni
* All fixes or features must have a test proving the improvement.
* All features must document the new behavior. Critical fixes that introduce new behavior must be documented too.
* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](https://github.com/jaspervdj/stylish-haskell)
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
+3
View File
@@ -2,3 +2,6 @@ packages: postgrest.cabal
tests: true
allow-newer:
hasql:postgresql-libpq
-- https://github.com/martijnbastiaan/doctest-parallel/blob/main/example/README.md#cabalproject
write-ghc-environment-files: always
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2026-04-18T18:42:36Z
index-state: hackage.haskell.org 2026-08-10T16:58:32Z
+23 -1
View File
@@ -52,11 +52,20 @@ let
postgresqlVersions =
[
{ name = "pg-19"; postgresql = pkgs.postgresql_19.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-18"; postgresql = pkgs.postgresql_18.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{
name = "oriole-18";
postgresql = pkgs.orioledb.withPackages (p: [ p.postgis p.pg_safeupdate ]);
config = "
default_table_access_method = 'orioledb'
shared_preload_libraries = 'orioledb, pg_stat_statements'
";
}
];
haskellPackages = pkgs.haskell.packages."${compiler}";
@@ -77,6 +86,16 @@ let
"-f dev --test-show-detail=direct";
inherit (pkgs.haskell) lib;
nixos-lib = import (pkgs.path + "/nixos/lib") { };
runTest = postgrest: test: (nixos-lib.runTest {
hostPkgs = pkgs;
# Replace the top-level `pkgs.postgrest` attribute with our current version on this branch.
defaults.nixpkgs.overlays = [ (_: _: { inherit postgrest; }) ];
# Speeds up evaluation a little bit; documentation is really not required for tests.
defaults.documentation.enable = pkgs.lib.mkDefault false;
imports = [ test ];
}).config.result;
in
rec {
inherit nixpkgs pkgs;
@@ -120,7 +139,7 @@ rec {
# Development tools.
devTools =
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie; };
# Documentation tools.
docs =
@@ -162,4 +181,7 @@ rec {
# Docker images and loading script.
docker =
pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };
# NixOS VM tests
nixpkgs-nixos-test = runTest postgrestStatic (pkgs.path + "/nixos/tests/postgrest.nix");
}
+2 -2
View File
@@ -87,8 +87,8 @@ url of Authorization is [[../explanations/db_authz.html]]
url of CLI is [[../references/cli.html#cli]]
url of "Connection Pool" is [[../references/connection_pool.html]]
url of Config is [[../references/configuration.html#configuration]]
url of HTTPADMIN is [[../explanations/architecture.html#http]]
url of HTTPAPI is [[../explanations/architecture.html#http]]
url of HTTPADMIN is [[../references/http_server.html]]
url of HTTPAPI is [[../references/http_server.html]]
url of Listener is [[../references/listener.html#listener]]
url of Proxy is [[../explanations/nginx.html]]
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

+1
View File
@@ -300,6 +300,7 @@ linkcheck_ignore = [
r"https://www.patreon.com/postgrest",
r"https://blog.frankel.ch/poor-man-api",
r"https://www.cybertec-postgresql.com/.*",
r"https://stackoverflow.com/.*",
# Odd SSL error
r"https://www.dripdepot.com",
r"https://www.euronodes.com",
+1
View File
@@ -83,6 +83,7 @@ Extensions
Client-Side Libraries
---------------------
* `efcore-postgrest <https://github.com/pedro-gilmora/EF.PostgREST.Provider>`_ - C#
* `postgrest-csharp <https://github.com/supabase-community/postgrest-csharp>`_ - C#
* `postgrest-dart <https://github.com/supabase/postgrest-dart>`_ - Dart
* `postgrest-ex <https://github.com/supabase-community/postgrest-ex>`_ - Elixir
+11 -16
View File
@@ -31,65 +31,60 @@ This section talks briefly about various important modules.
Main
----
The starting point of the program is `Main.hs <https://github.com/PostgREST/postgrest/blob/main/main/Main.hs>`_.
The starting point of the program is `Main.hs <https://github.com/PostgREST/postgrest/blob/main/src/executable/Main.hs>`_.
CLI
---
Main then calls `CLI.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/CLI.hs>`_, which is in charge of :ref:`cli`.
Main then calls `CLI.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/CLI.hs>`_, which is in charge of :ref:`cli`.
App
---
`App.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/App.hs>`_ is then in charge of composing the different modules.
`App.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/App.hs>`_ is then in charge of composing the different modules.
Auth
----
`Auth.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Auth.hs>`_ is in charge of :ref:`authn`.
`Auth.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Auth.hs>`_ is in charge of :ref:`authn`.
Api Request
-----------
`ApiRequest.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/ApiRequest.hs>`_ is in charge of parsing the URL query string (following PostgREST syntax), the request headers, and the request body.
`ApiRequest.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/ApiRequest.hs>`_ is in charge of parsing the URL query string (following PostgREST syntax), the request headers, and the request body.
A request might be rejected at this level if it's invalid. For example when providing an unknown media type to PostgREST or using an unknown HTTP method.
Plan
----
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
Query
-----
`Query.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Query.hs>`_ generates the SQL queries (parametrized and prepared) required to satisfy the user request.
`Query.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Query.hs>`_ generates the SQL queries (parametrized and prepared) required to satisfy the user request.
Only at this stage a connection from the pool might be used.
Schema Cache
------------
`SchemaCache.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/SchemaCache.hs>`_ is in charge of :ref:`schema_cache`.
`SchemaCache.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/SchemaCache.hs>`_ is in charge of :ref:`schema_cache`.
Config
------
`Config.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Config.hs>`_ is in charge of :ref:`configuration`.
`Config.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Config.hs>`_ is in charge of :ref:`configuration`.
Admin
-----
`Admin.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.
HTTP
----
The HTTP server is provided by `Warp <https://aosabook.org/en/posa/warp.html>`_.
`Admin.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.
Listener
--------
`Listener.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Listener.hs>`_ is in charge of the :ref:`listener`.
`Reload.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/AppState/Reload.hs>`_ is in charge of the :ref:`listener`.
@@ -0,0 +1,62 @@
.. _debugging_performance_pg_stat_statements:
Debugging Performance with pg_stat_statements
=============================================
This how-to shows how to get a query identifier through PostgREST and then use it to inspect the same query in ``pg_stat_statements``.
.. important::
- :ref:`db-plan-enabled` must be enabled in PostgREST.
- PostgreSQL 14 or newer with ``pg_stat_statements`` available.
Get the Query Identifier from PostgREST
---------------------------------------
Request the plan in JSON format with the ``verbose`` option:
.. code-block:: bash
curl "http://localhost:3000/projects?select=id,name&order=id" \
-H "Accept: application/vnd.pgrst.plan+json; options=verbose"
The response will contain a top-level ``Query Identifier`` field:
.. code-block:: json
[
{
"Plan": {
"Node Type": "Aggregate"
},
"Query Identifier": -432192689578025496
}
]
Look up the query in pg_stat_statements
---------------------------------------
Use that identifier against ``pg_stat_statements``:
.. code-block:: postgres
select
calls,
total_exec_time,
mean_exec_time,
rows,
query
from pg_stat_statements
where queryid = -432192689578025496;
.. csv-table::
:header: "calls", "total_exec_time", "mean_exec_time", "rows", "query"
"13", "0.6355850000000001", "0.04889115384615385", "13", "WITH pgrst_source AS (...)"
This lets you correlate a PostgREST request with PostgreSQL runtime statistics such as:
- how often the query ran
- total and average execution time
- how many rows it produced
- the normalized SQL text recorded by PostgreSQL
@@ -318,144 +318,6 @@ You can insert a new product using a JSON object for the ``extra_info`` column:
To query and filter the data see :ref:`json_columns` for a complete reference.
.. _ww_postgis:
PostGIS
-------
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
.. code-block:: postgres
-- Activate the postgis module in the current database
create extension if not exists postgis;
create table coverage (
id int primary key,
name text unique,
area geometry
);
To add areas in polygon format, you can use string representation:
.. code-block:: bash
curl "http://localhost:3000/coverage" \
-X POST -H "Content-Type: application/json" \
-d @- << EOF
[
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
]
EOF
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
.. code-block:: bash
curl "http://localhost:3000/coverage" \
-H "Accept: application/geo+json"
.. code-block:: json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[1,0],[1,1],[0,1],[0,0]]
]
},
"properties": {
"id": 1,
"name": "small"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[10,0],[10,10],[0,10],[0,0]]
]
},
"properties": {
"id": 2,
"name": "big"
}
}
]
}
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
.. code-block:: postgres
alter table coverage
add square_units double precision generated always as ( st_area(area) ) stored;
In the case that you are using older PostGIS versions, then creating a function is your best option:
.. code-block:: postgres
create or replace function coverage_geo_collection() returns json as $$
select
json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', st_AsGeoJSON(c.area)::json,
'properties', json_build_object('id', c.id, 'name', c.name)
)
)
)
from coverage c;
$$ language sql;
Now this query will return the same results:
.. code-block:: bash
curl "http://localhost:3000/rpc/coverage_geo_collection"
.. code-block:: json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[1,0],[1,1],[0,1],[0,0]]
]
},
"properties": {
"id": 1,
"name": "small"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[10,0],[10,10],[0,10],[0,0]]
]
},
"properties": {
"id": 2,
"name": "big"
}
}
]
}
Ranges
------
@@ -609,3 +471,20 @@ You can use other comparative filters and also all the `PostgreSQL special date/
"due_date": "2022-02-27T06:00:00-05:00"
}
]
.. raw:: html
<script type="text/javascript">
let hash = window.location.hash;
const redirects = {
// PostGIS
'#postgis': '../integrations/postgis.html#postgis',
};
let willRedirectTo = redirects[hash];
if (willRedirectTo) {
window.location.href = willRedirectTo;
}
</script>
+6 -3
View File
@@ -119,11 +119,14 @@ Releases
PostgREST follows ``MAJOR.PATCH`` two-part versioning:
- ``MAJOR``: feature release, may deprecate or remove things.
- ``PATCH``: fix/security release only; no features, no behavior changes.
- ``PATCH``: fix/security release only, no features and no behavior changes.
Starting from ``v14.0``, only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
MAJOR releases are published twice a year, with their scope and target dates tracked through `GitHub milestones <https://github.com/PostgREST/postgrest/milestones>`_.
PATCH releases are published on an as-needed basis.
All the releases are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
Starting from ``v14.0``, only even-numbered MAJOR versions are released, reserving odd-numbered MAJOR versions for development.
All releases are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_, along with the corresponding upgrade guides.
Tutorials
---------
+36
View File
@@ -0,0 +1,36 @@
NixOS
=====
Nixpkgs contains a `NixOS module to run PostgREST <https://search.nixos.org/options?channel=unstable&query=services.postgrest&type=options>`_, which can be enabled with ``services.postgrest.enable = true``.
A PostgreSQL server can be enabled on the same machine with ``services.postgresql.enable = true``. Connections will use the name of the system user as user and database names by default, in this case ``postgrest``.
A minimal example could look like this:
.. code-block:: nix
{
pkgs,
...
}:
{
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "init.sql" ''
CREATE ROLE postgrest LOGIN NOINHERIT;
CREATE ROLE anon ROLE postgrest;
'';
};
services.postgrest = {
enable = true;
settings.db-anon-role = "anon";
settings.db-uri.dbname = "postgres";
};
}
This will expose the PostgREST server on localhost on the NixOS machine and allow anonymous access.
.. tip::
NixOS also allows to quickly spin up different PostgreSQL versions or even forks this way. For example, to test the current beta version of `OrioleDB <https://www.orioledb.com>`_, use ``services.postgresql.package = pkgs.orioledb``.
+154
View File
@@ -0,0 +1,154 @@
.. _ww_postgis:
PostGIS
=======
To work with `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography``, you'll need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first.
.. code-block:: postgres
-- Activate the postgis module in the current database
create extension if not exists postgis;
create table coverage (
id int primary key,
name text unique,
area geometry
);
insert into coverage (id, name, area) values
(1, 'small', ST_GeomFromText('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',4326)),
(2, 'big', ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', 4326);
.. _application/geo+json:
``application/geo+json``
------------------------
PostgREST supports the `standard <https://www.iana.org/assignments/media-types/application/geo+json>`_ ``application/geo+json`` media type which can be used to get the output in `GeoJSON <https://geojson.org/>`_ format. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
.. code-block:: bash
curl "http://localhost:3000/coverage" \
-H "Accept: application/geo+json"
.. code-block:: json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[1,0],[1,1],[0,1],[0,0]]
]
},
"properties": {
"id": 1,
"name": "small"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[10,0],[10,10],[0,10],[0,0]]
]
},
"properties": {
"id": 2,
"name": "big"
}
}
]
}
Using generated columns
-----------------------
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
.. code-block:: postgres
alter table coverage
add square_units double precision generated always as ( st_area(area) ) stored;
In the case that you are using older PostGIS versions, then creating a function is your best option:
.. code-block:: postgres
create or replace function coverage_geo_collection() returns json as $$
select
json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', st_AsGeoJSON(c.area)::json,
'properties', json_build_object('id', c.id, 'name', c.name)
)
)
)
from coverage c;
$$ language sql;
Now this query will return the same results:
.. code-block:: bash
curl "http://localhost:3000/rpc/coverage_geo_collection"
.. code-block:: json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[1,0],[1,1],[0,1],[0,0]]
]
},
"properties": {
"id": 1,
"name": "small"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[0,0],[10,0],[10,10],[0,10],[0,0]]
]
},
"properties": {
"id": 2,
"name": "big"
}
}
]
}
Using string representation
---------------------------
To insert areas in polygon format, you can use string representation:
.. code-block:: bash
curl "http://localhost:3000/coverage" \
-X POST -H "Content-Type: application/json" \
-d @- << EOF
[
{ "id": 3, "name": "strip", "area": "SRID=4326;POLYGON((0 0, 50 0, 50 2, 0 2, 0 0))" },
{ "id": 4, "name": "diamond", "area": "SRID=4326;POLYGON((5 0, 10 5, 5 10, 0 5, 5 0))" }
]
EOF
PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type.
+8 -3
View File
@@ -1,5 +1,6 @@
personal_ws-1.1 en 0 utf-8
api
autoscaling
API's
APIs
APISIX
@@ -30,11 +31,11 @@ CSV
durations
DDL
DOM
DSL
DevOps
Dramatiq
dockerize
enum
ECS
Enums
Entra
eq
@@ -45,7 +46,9 @@ filename
FreeBSD
fts
fullstack
GC
GeoJSON
GHC
Github
Google
grantor
@@ -74,7 +77,6 @@ isdistinct
JS
js
JSON
JSPath
JWK
JWT
jwt
@@ -97,6 +99,7 @@ namespaced
Nanos
neq
nginx
NixOS
nixpkgs
npm
nxl
@@ -142,11 +145,13 @@ Redux
refactor
reloadable
Reloadable
reuseport
requester's
RESTful
RLS
RPC
RSA
RTS
safeupdate
savepoint
schemas
@@ -200,4 +205,4 @@ webuser
wfts
www
debouncing
deduplicates
deduplicates
+3 -2
View File
@@ -3,7 +3,9 @@
Admin Server
############
PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port`.
PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port` or :ref:`admin-server-unix-socket`.
Multiple PostgREST instances can share the same public API host and port when :ref:`server-reuseport` is enabled. Admin ports are not shared: give each instance a different :ref:`admin-server-port`, otherwise the new instance will fail to start.
.. _health_check:
@@ -72,5 +74,4 @@ Provides the ``schema_cache`` endpoint that prints the runtime :ref:`schema_cach
"dbRepresentations": ["..."],
"dbRoutines": ["..."],
"dbTables": ["..."],
"dbTimezones": ["..."]
}
+29 -28
View File
@@ -62,8 +62,12 @@ The server ignores unrecognized or unfulfillable preferences by default. You can
Timezone
========
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all time zones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
.. important::
``handling=lenient`` is ignored for ``timezone``. Invalid time zones always return an error.
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_.
It accepts all time zones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ and numeric offsets.
.. code-block:: bash
@@ -84,43 +88,40 @@ The ``timezone`` preference allows you to change the `PostgreSQL timezone <https
{"t":"2023-10-18T09:37:59.611-07:00"}
]
For an invalid time zone, PostgREST returns values with the default time zone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).
Offsets are also accepted:
.. code-block:: bash
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: timezone=05:30"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Preference-Applied: timezone=05:30
.. code-block:: json
[
{"t":"2023-10-18T17:07:59.611+05:30"},
{"t":"2023-10-18T19:07:59.611+05:30"},
{"t":"2023-10-18T21:07:59.611+05:30"}
]
You can also use negative offsets like ``-03:00``.
For an invalid time zone, PostgREST returns a database error.
.. code-block:: bash
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: timezone=Jupiter/Red_Spot"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
.. code-block:: json
[
{"t":"2023-10-18T12:37:59.611+00:00"},
{"t":"2023-10-18T14:37:59.611+00:00"},
{"t":"2023-10-18T16:37:59.611+00:00"}
]
Note that there's no ``Preference-Applied`` in the response.
However, with ``handling=strict``, an invalid time zone preference will throw an :ref:`error <pgrst122>`.
.. code-block:: bash
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: handling=strict, timezone=Jupiter/Red_Spot"
.. code-block:: http
HTTP/1.1 400 Bad Request
.. note::
This feature requires querying `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`.
.. _prefer_return:
Return Representation
@@ -51,7 +51,7 @@ Builtin handlers are offered for common standard media types.
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`functions`.
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
* ``application/geo+json``, see :ref:`ww_postgis`.
* ``application/geo+json``, see :ref:`application/geo+json`.
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
The following vendor media types handlers are also supported.
+11 -36
View File
@@ -224,56 +224,31 @@ It's recommended to leave the JWT cache enabled as our load tests indicate ~20%
JWT Role Extraction
-------------------
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
A JSON Path (`RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_) can be specified for the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
- ``==`` selects the first array element that exactly matches the right operand
- ``!=`` selects the first array element that does not match the right operand
- ``^==`` selects the first array element that starts with the right operand
- ``==^`` selects the first array element that ends with the right operand
- ``*==`` selects the first array element that contains the right operand
The selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:
- ``[a:b]`` take slice from index ``a`` up to ``b``
- ``[a:]`` take slice from index ``a`` to end
- ``[:b]`` take slice from start to index ``b``
- ``[:]`` select everything, no slicing
.. important::
Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.
You can quickly try out JSON Path by visiting https://serdejsonpath.live.
Usage examples:
.. code:: bash
# {"postgrest":{"roles": ["other", "author"]}}
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
jwt-role-claim-key = ".postgrest.roles[1]"
jwt-role-claim-key = "$$.postgrest.roles[1]"
# {"https://www.example.com/role": { "key": "author" }}
# non-alphanumerical characters can go inside quotes(escaped in the config value)
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
# non-alphanumerical characters can go inside single quotes
jwt-role-claim-key = "$$['https://www.example.com/role'].key"
# {"postgrest":{"roles": ["other", "author"]}}
# `@` represents the current element in the array
# all the these match the string "author"
jwt-role-claim-key = ".postgrest.roles[?(@ == \"author\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ != \"other\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ^== \"aut\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
# {"postgrest":{"wlcg": ["/groupa", "/groupb/"]}}
# skip the "/" character using slice operator
jwt-role-claim-key = ".postgrest.wlcg[0][1:]"
jwt-role-claim-key = ".postgrest.wlcg[1][1:-1]"
# filter based on equality or regular expression
jwt-role-claim-key = "$$.postgrest.roles[?(@ == 'author')]"
jwt-role-claim-key = "$$.postgrest.roles[?search(@, '^au')]"
.. note::
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
- If JSON Path query returns multiple values, the first one gets selected.
- Only when using the :ref:`file_config`, all ``$`` characters in the value must be escaped with an additional ``$`` char. For :ref:`env_variables_config` and :ref:`in_db_config`, only use a single ``$`` char.
- In our implementation, only the `search()` function from `JSON Path Functions <https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions>`_ is available for filtering.
JWT Security
------------
+124 -17
View File
@@ -176,6 +176,46 @@ admin-server-port
Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.
.. _admin-server-unix-socket:
admin-server-unix-socket
------------------------
=============== =================================
**Type** String
**Default** `n/a`
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_UNIX_SOCKET
**In-Database** `n/a`
=============== =================================
`Unix domain socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ where to bind the :ref:`admin_server`.
If specified, this takes precedence over :ref:`admin-server-port`. Example:
.. code:: bash
admin-server-unix-socket = "/tmp/pgrst-admin.sock"
.. _admin-server-unix-socket-mode:
admin-server-unix-socket-mode
-----------------------------
=============== ===================================
**Type** String
**Default** 660
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_UNIX_SOCKET_MODE
**In-Database** `n/a`
=============== ===================================
`Unix file mode <https://en.wikipedia.org/wiki/File_system_permissions>`_ to be set for the socket specified in :ref:`admin-server-unix-socket`
Needs to be a valid octal between 600 and 777.
.. code:: bash
admin-server-unix-socket-mode = "660"
.. _app.settings.*:
app.settings.*
@@ -540,21 +580,6 @@ db-schemas
The list of database schemas to expose to clients. See :ref:`schemas`.
.. _db-timezone-enabled:
db-timezone-enabled
-------------------
=============== =================================
**Type** Boolean
**Default** True
**Reloadable** Y
**Environment** PGRST_DB_TIMEZONE_ENABLED
**In-Database** pgrst.db_timezone_enabled
=============== =================================
Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``.
.. _db-tx-end:
db-tx-end
@@ -654,7 +679,7 @@ jwt-role-claim-key
=============== =================================
**Type** String
**Default** .role
**Default** $.role
**Reloadable** Y
**Environment** PGRST_JWT_ROLE_CLAIM_KEY
**In-Database** pgrst.jwt_role_claim_key
@@ -664,6 +689,10 @@ jwt-role-claim-key
See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
.. warning::
Only when using :ref:`file_config`, the ``$`` char needs to be escaped, so use ``$$`` and PostgREST will interpret it as a single ``$`` character.
.. _jwt-secret:
jwt-secret
@@ -723,7 +752,7 @@ log-level
=============== =================================
**Type** String
**Default** error
**Reloadable** N
**Reloadable** Y
**Environment** PGRST_LOG_LEVEL
**In-Database** `n/a`
=============== =================================
@@ -899,6 +928,50 @@ server-port
The TCP port to bind the web server. Use ``0`` to automatically assign a port.
.. _server-reuseport:
server-reuseport
----------------
=============== =================================
**Type** Bool
**Default** false
**Reloadable** N
**Environment** PGRST_SERVER_REUSEPORT
**In-Database** `n/a`
=============== =================================
Enables ``SO_REUSEPORT`` on the TCP server socket. This allows multiple
PostgREST processes to bind to the same :ref:`server-host` and
:ref:`server-port` when the operating system supports it.
For example, two PostgREST processes can use the same configuration:
.. code:: ini
server-host = "127.0.0.1"
server-port = 3000
server-reuseport = true
New connections are then distributed by the operating system between the
running PostgREST processes. This can be used to start a replacement process
before stopping the old one, or to run several PostgREST processes behind one
port.
If ``server-reuseport`` is disabled, starting another PostgREST process on
the same host and port will fail with the usual address-in-use error.
Enabling this setting on an operating system that does not support
``SO_REUSEPORT`` is a configuration error. PostgREST will fail to start
instead of falling back to a normal TCP socket.
When running multiple PostgREST instances on the same :ref:`server-port`, use
a different ``admin-server-port`` for each instance. Admin ports are not shared
between instances, so readiness checks always target one specific PostgREST
instance.
This setting does not apply when :ref:`server-unix-socket` is used.
.. _server-trace-header:
server-trace-header
@@ -969,3 +1042,37 @@ server-unix-socket-mode
.. code:: bash
server-unix-socket-mode = "660"
.. _url-use-legacy-target-names:
url-use-legacy-target-names
---------------------------
=============== =================================
**Type** Boolean
**Default** True
**Reloadable** Y
**Environment** PGRST_URL_USE_LEGACY_TARGET_NAMES
**In-Database** pgrst.url_use_legacy_target_names
=============== =================================
When active, it allows using the the name of an embedded table in filters, orders or limits even if it has an alias:
.. code:: bash
curl "http://localhost:3000/table?select=alias:target(*)&target.order=id" -i
.. code:: text
Warning: 299 PostgRESTv16 "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release. Update `target` to `alias` in query string filters, orders or limits."
[...]
Note that the response includes a deprecation message in the ``Warning`` header.
This will also show in the PostgREST logs:
.. code::
28/May/2026:20:33:22 -0500: WARNING: Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release.
28/May/2026:20:33:22 -0500: Update filters, orders or limits that use `target` to `alias` in `GET /table?select=alias:target(*)&target.order=id`
This feature will be removed in a future release, so you should start using the ``alias`` in these cases.
+2
View File
@@ -47,6 +47,8 @@ Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the co
To avoid this problem and save resources, a connection max lifetime (:ref:`db-pool-max-lifetime`) is enforced.
After the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released.
.. _pool_timeout:
Acquisition Timeout
-------------------
+1 -1
View File
@@ -199,7 +199,7 @@ Related to the HTTP request elements.
| | | :ref:`switching schemas <multiple-schemas>` is not present |
| PGRST106 | | in the :ref:`db-schemas` configuration variable. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst107: | 415 | The ``Content-Type`` sent in the request is invalid. |
| .. _pgrst107: | 406 | The ``Accept`` media type sent in the request is invalid. |
| | | |
| PGRST107 | | |
+---------------+-------------+-------------------------------------------------------------+
+18
View File
@@ -0,0 +1,18 @@
.. _http_server:
HTTP Server
###########
The HTTP server is provided by `Warp <https://aosabook.org/en/posa/warp.html>`_.
Graceful shutdown
-----------------
PostgREST uses Warp's graceful shutdown, when a ``SIGTERM`` is received:
- It stops accepting new requests.
- Allows requests that are already in progress to finish.
- Closes idle ``Keep-Alive`` connections instead of waiting for them to expire.
- Responses sent during shutdown indicate that the connection should not be reused (e.g. for HTTP/1.x, it sends ``Connection: close``).
This allows PostgREST to shut down promptly without interrupting in-flight requests. Useful for zero-downtime upgrades and autoscaling/load-balancing under cloud environments (AWS ECS, Kubernetes).
+42 -1
View File
@@ -39,7 +39,7 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
06/May/2024:08:16:11 -0500: Config reloaded
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 RPCs, 0 Domain Representations, 4 Media Type Handlers
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
06/May/2024:14:11:27 -0500: Config reloaded
@@ -238,6 +238,45 @@ pgrst_jwt_cache_evictions_total
The total number of JWT cache evictions.
GHC Runtime Metrics
-------------------
PostgREST can also expose GHC runtime system metrics. These use the ``ghc_*``
prefix and include
`GHC RTS statistics <https://ghc.gitlab.haskell.org/ghc/doc/libraries/base-4.22.0.0-inplace/GHC-Stats.html#g:1>`_
for runtime allocation, garbage collection, memory, and CPU/elapsed time.
These are useful for monitoring PostgREST process health and diagnosing memory
pressure or GC behavior.
To expose these metrics, enable GHC RTS statistics when starting PostgREST:
.. code-block:: bash
postgrest +RTS -T -RTS
When enabled, the admin ``/metrics`` endpoint includes samples such as:
.. code-block:: text
# HELP ghc_gcs_total Total number of GCs
# TYPE ghc_gcs_total counter
ghc_gcs_total 1
# HELP ghc_allocated_bytes_total Total bytes allocated
# TYPE ghc_allocated_bytes_total counter
ghc_allocated_bytes_total 12345678
Other available GHC runtime metrics include:
- ``ghc_gcs_total``
- ``ghc_major_gcs_total``
- ``ghc_allocated_bytes_total``
- ``ghc_max_live_bytes``
- ``ghc_max_mem_in_use_bytes``
- ``ghc_mutator_cpu_seconds_total``
- ``ghc_gc_cpu_seconds_total``
- ``ghc_elapsed_seconds_total``
Traces
======
@@ -391,6 +430,8 @@ By default the plan is assumed to generate the JSON representation of a resource
The other available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, which correspond to the `EXPLAIN command options <https://www.postgresql.org/docs/current/sql-explain.html>`_. To use the ``analyze`` and ``wal`` parameters for example, you would add them like ``Accept: application/vnd.pgrst.plan; options=analyze|wal``.
For a workflow that takes the ``Query Identifier`` from a verbose PostgREST plan and uses it to inspect the same query in ``pg_stat_statements``, see :ref:`debugging_performance_pg_stat_statements`.
Note that akin to the EXPLAIN command, the changes will be committed when using the ``analyze`` option. To avoid this, you can use the :ref:`db-tx-end` and the ``Prefer: tx=rollback`` header.
Securing the Execution Plan
+1 -1
View File
@@ -24,7 +24,7 @@ You can do this with UNIX signals or with PostgreSQL notifications. It's also po
.. note::
- Requests will wait until the schema cache reload is done. This to prevent client errors due to an stale schema cache.
- If the schema cache fails to reload (e.g. due to a ``statement_timeout`` or :ref:`pool timeout <pool_timeout>`), PostgREST will continue serving requests in a "best effort" basis.
- If you are using the :ref:`in_db_config`, a schema cache reload will :ref:`reload the configuration<config_reloading>` as well.
.. _schema_reloading_signals:
+1 -1
View File
@@ -172,7 +172,7 @@ Go back to :ref:`tut1_step3` and change the payload to
.. code-block:: bash
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":\"123456789\"}" | _base64)
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":123456789}" | _base64)
echo -n "$header.$payload.$signature"
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776949667,
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
"lastModified": 1784115452,
"narHash": "sha256-BoYPdqk6jlKXy+DyUzyGV/CtRGfAhk2MmIgBhsemTGI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
"rev": "35d3407a3816f3b341d8cf1d60abaf2b7b8166ac",
"type": "github"
},
"original": {
+7 -62
View File
@@ -70,55 +70,16 @@ The PostgREST utilities available in `nix-shell` all have names that begin with
`<tab>`) in `nix-shell` to see all that are available:
```bash
# Note: The utilities listed here might not be up to date.
[nix-shell]$ postgrest-<tab>
postgrest-build postgrest-parallel-curl
postgrest-check postgrest-profiled-run
postgrest-clean postgrest-push-cachix
postgrest-commitlint postgrest-release
postgrest-coverage postgrest-repl
postgrest-coverage-draft-overlay postgrest-run
postgrest-docs-build postgrest-style
postgrest-docs-check postgrest-style-check
postgrest-docs-dictcheck postgrest-test-big-schema
postgrest-docs-linkcheck postgrest-test-doctests
postgrest-docs-render postgrest-test-io
postgrest-docs-serve postgrest-test-memory
postgrest-docs-spellcheck postgrest-test-replica
postgrest-dump-minimal-imports postgrest-test-spec
postgrest-dump-schema postgrest-test-spec-idempotence
postgrest-gen-ctags postgrest-watch
postgrest-gen-jwt postgrest-with-all
postgrest-gen-secret postgrest-with-git
postgrest-git-hooks postgrest-with-pgrst
postgrest-hsie-graph-modules postgrest-with-pg-14
postgrest-hsie-graph-symbols postgrest-with-pg-15
postgrest-hsie-minimal-imports postgrest-with-pg-16
postgrest-lint postgrest-with-pg-17
postgrest-loadtest postgrest-with-pg-18
postgrest-loadtest-against postgrest-with-slow-pg
postgrest-loadtest-report postgrest-with-slow-postgrest
postgrest-nixpkgs-upgrade
postgrest-build
postgrest-cabal-update
postgrest-check
postgrest-clean
postgrest-commitlint
...
[nix-shell]$
```
The `docker` module has large dependencies to be build before the shell becomes
available, which could take an especially long time if the cachix binary cache
is not used. You can activate it by passing a flag to `nix-shell` with
`nix-shell --arg docker true`. This will make the respective utilities available:
```bash
$ nix-shell --arg docker true
[nix-shell]$ postgrest-docker-<tab>
postgrest-docker-load
...
```
Note that `postgrest-docker-load` is now also available.
Most of these commands provide a `--help` output, make sure to check it out.
To run one-off commands, you can also use `nix-shell --run <command>`, which
will launch the Nix shell, run that one command and exit. Note that the tab
@@ -134,16 +95,6 @@ $ nix-shell --run "postgrest-foo --bar"
```
A third option is to install utilities that you use very often locally:
```bash
$ nix-env -f default.nix -iA devTools
# `postgrest-style` can now be run directly:
$ postgrest-style
```
If you use `nix-shell` very often, you might like to use
https://github.com/xzfc/cached-nix-shell, which skips evaluating all our Nix
expressions if nothing changed, reducing startup time for the shell
@@ -209,13 +160,7 @@ The loadtests ensure that performance doesn't drop on a change. Underlyingly the
[nix-shell]$ postgrest-loadtest
# You can loadtest comparing to a different branch
[nix-shell]$ postgrest-loadtest-against master
# You can simulate latency client/postgrest and postgrest/database
[nix-shell]$ PGRST_DELAY=5ms PGDELAY=5ms postgrest-loadtest
# You can build postgrest directly with cabal for faster iteration
[nix-shell]$ PGRST_BUILD_CABAL=1 postgrest-loadtest
[nix-shell]$ postgrest-loadtest-against main
# Produce a markdown report to be used on CI
[nix-shell]$ postgrest-loadtest-report
+1 -2
View File
@@ -37,8 +37,7 @@ import Data.Text (Text)
import GHC.Driver.Errors.Types (GhcMessage)
import GHC.Generics (Generic)
import GHC.Hs.Extension (GhcPs)
import GHC.Types.Error (Messages, defaultDiagnosticOpts,
getMessages)
import GHC.Types.Error (Messages, defaultDiagnosticOpts, getMessages)
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (rdrNameOcc)
import GHC.Unit.Module (moduleNameString)
+7 -7
View File
@@ -5,10 +5,10 @@ project. It's available in PostgREST's `nix-shell` by default.
## Dumping imports
Given source code in the directories `src` and `main`, for example, you can run:
Given source code in the directories `src/library` and `src/executable`, for example, you can run:
```
hsie dump-imports src main
hsie dump-imports src/library src/executable
```
This dumps all imports of the modules in the given directory to a CSV file,
@@ -18,7 +18,7 @@ To dump to a JSON file (e.g., to further process with `jq`), add the `--json`
flag:
```
hsie dump-imports --json src main
hsie dump-imports --json src/library src/executable
```
## Graphing imports
@@ -27,7 +27,7 @@ The tool can generate `graphviz` graphs of module and symbol imports by printing
a file to `stdout` that can directly be rendered with `dot`:
```
hsie graph-modules src main | dot -Tpng -o modules.png
hsie graph-modules src/library src/executable | dot -Tpng -o modules.png
```
The command `graph-modules` prints a graph of which modules insert which other
@@ -39,7 +39,7 @@ To check whether modules are imported under consistent aliases in your project,
run:
```
hsie check-aliases main src
hsie check-aliases src/library src/executable
```
This will exit with a non-zero exit code if any inconsistent aliases are found.
@@ -48,13 +48,13 @@ The following command checks whether any modules are imported as wildcards, i.e.
not qualified and without specifying symbols.
```
hsie check-wildcards main src
hsie check-wildcards src/library src/executable
```
To whitelist certain modules to be imported as wildcards, use `--ok`:
```
hsie check-wildcards main src --ok Protolude --ok Test.Module
hsie check-wildcards src/library src/executable --ok Protolude --ok Test.Module
```
## Current limitations
+28 -7
View File
@@ -49,6 +49,26 @@ let
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
fuzzyset = prev.fuzzyset_0_2_4;
# TODO: Remove once available in nixpkgs
auto-update =
prev.callHackageDirect
{
pkg = "auto-update";
ver = "0.2.7";
sha256 = "sha256-fHX/OqF/cB9rbpGpLUtA29bcEJS43HUWHcK55yUxKoo=";
}
{ };
# TODO: Remove once available in nixpkgs
aeson-jsonpath =
prev.callHackageDirect
{
pkg = "aeson-jsonpath";
ver = "0.4.2.0";
sha256 = "sha256-K+3brf1zjSSjojtSCXFrip5rrP7AO/S4zndAxAnvEfc=";
}
{ };
http2 =
prev.callHackageDirect
{
@@ -77,13 +97,14 @@ let
{ };
warp =
lib.dontCheck (prev.callHackageDirect
{
pkg = "warp";
ver = "3.4.13";
sha256 = "sha256-jmr8kpeSPDkOhT0i9PhozZapX4nUs92cOX7POAGb7/M=";
}
{ });
lib.dontCheck
(prev.callHackageDirect
{
pkg = "warp";
ver = "3.4.14";
sha256 = "sha256-RnoOUlC6dOP0sK/tYAJCX1oLzVFG1GILUY+yVbmvW8Y=";
}
{ });
};
in
{
+10 -2
View File
@@ -51,7 +51,7 @@ let
docs = "Run PostgREST after building it interactively with cabal-install";
args =
[
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [], [PostgREST anonymous role. (default: 'postgrest_test_anonymous')])"
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
@@ -62,6 +62,10 @@ let
withEnv = postgrest.env;
}
''
# when there's a default, argbash conflates empty string with unset, so we do this workaround to be able to do `PGRST_DB_ANON_ROLE="" <command>` for manual testing
if [[ ! ''${PGRST_DB_ANON_ROLE+x} ]]; then
PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
fi
export PGRST_DB_ANON_ROLE
export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
@@ -80,7 +84,7 @@ let
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization.";
args =
[
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [], [PostgREST anonymous role. (default: 'postgrest_test_anonymous')])"
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
@@ -90,6 +94,10 @@ let
withEnv = postgrest.env;
}
''
# when there's a default, argbash conflates empty string with unset, so we do this workaround to be able to do `PGRST_DB_ANON_ROLE="" <command>` for manual testing
if [[ ! ''${PGRST_DB_ANON_ROLE+x} ]]; then
PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
fi
export PGRST_DB_ANON_ROLE
export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
+4 -157
View File
@@ -5,15 +5,13 @@
, curl
, devCabalOptions
, entr
, git
, fd
, graphviz
, hsie
, nix
, silver-searcher
, stdenv
, style
, tests
, withTools
, haskellPackages
, ctags
, openssl
@@ -41,7 +39,7 @@ let
}
''
while true; do
(! ${silver-searcher}/bin/ag -l . | ${entr}/bin/entr -dr "$_arg_command" "''${_arg_leftovers[@]}")
(! ${fd}/bin/fd -H -E .git | ${entr}/bin/entr -dr "$_arg_command" "''${_arg_leftovers[@]}")
done
'';
@@ -91,156 +89,6 @@ let
${style}/bin/postgrest-style-check
'';
gitHooks =
let
name = "postgrest-git-hooks";
in
checkedShellScript
{
inherit name;
docs =
''
Enable or disable git pre-commit and pre-push hooks.
Basic is faster and will only run:
- pre-commit: postgrest-style
- pre-push: postgrest-lint
Full takes a lot more time and will run:
- pre-commit: postgrest-style && postgrest-lint
- pre-push: postgrest-check
Changes made by postgrest-style will be staged automatically.
Example usage:
postgrest-git-hooks disable
postgrest-git-hooks enable basic
postgrest-git-hooks enable full
The "run" operation and "--hook" argument are only used internally.
'';
args =
[
"ARG_POSITIONAL_SINGLE([operation], [Operation])"
"ARG_TYPE_GROUP_SET([OPERATION], [OPERATION], [operation], [disable,enable,run])"
"ARG_POSITIONAL_SINGLE([mode], [Mode], [basic])"
"ARG_TYPE_GROUP_SET([MODE], [MODE], [mode], [basic,full])"
"ARG_OPTIONAL_SINGLE([hook], , [Hook], [pre-commit])"
"ARG_TYPE_GROUP_SET([HOOK], [HOOK], [hook], [pre-commit,pre-push])"
];
positionalCompletion =
''
if test "$prev" == "${name}"; then
COMPREPLY=( $(compgen -W "enable disable" -- "$cur") )
elif test "$prev" == "enable" || test "$prev" == "disable"; then
COMPREPLY=( $(compgen -W "basic full" -- "$cur") )
fi
'';
workingDir = "/";
}
''
if [ run != "$_arg_operation" ]; then
# Remove all hooks first and ignore failures because the file might be missing.
# This assumes that we're only adding lines that include "postgrest-git-hooks"
# to the hook file.
sed -i -e '/postgrest-git-hooks/d' .git/hooks/pre-{commit,push} 2> /dev/null || true
if [ disable != "$_arg_operation" ]; then
# The nix-shell && + nix-shell || pattern makes sure we can run the hook
# in a pure nix-shell, where nix-shell itself is not available, too.
# The $(nix-shell --run "command -v ...") pattern ensures we only need to enable
# the hooks once and still run the latest of our hook scripts, even when we
# update them in the repo.
echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-commit run' "$_arg_mode" \
>> .git/hooks/pre-commit
# shellcheck disable=SC2016
echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run "command -v postgrest-git-hooks") --hook=pre-commit run' "$_arg_mode" \
>> .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-push run' "$_arg_mode" \
>> .git/hooks/pre-push
# shellcheck disable=SC2016
echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run "command -v postgrest-git-hooks") --hook=pre-push run' "$_arg_mode" \
>> .git/hooks/pre-push
chmod +x .git/hooks/pre-push
fi
else
# When run from a git hook, the GIT_ environment variables conflict with our withGit helper.
# The following unsets all GIT_ variables.
unset "''${!GIT_@}"
# shellcheck disable=SC2329
function restore () {
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
# this will avoid merge conflicts when applying the stash
${git}/bin/git restore --source="$ref" .
# restore untracked files, too. could fail with no files
if [ "$(git show --numstat --format=oneline "$ref^3" | wc -l)" -gt 1 ]; then
${git}/bin/git restore --overlay --source="$ref^3" .
fi
${git}/bin/git stash drop "$ref"
}
case "$_arg_mode" in
basic)
case "$_arg_hook" in
pre-commit)
# To be able to automatically add only changes from postgrest-style to the staging area,
# we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping
# the stash afterwards.
${style}/bin/postgrest-style
stash="postgrest-git-hooks-$RANDOM"
${git}/bin/git stash push --include-untracked --keep-index -m "$stash"
if [ "$(git stash list --grep $stash)" ]; then
# Only create the stash pop trap, if we actually created a stash.
# Otherwise stash pop will cause havoc.
trap 'restore "$stash"' EXIT
fi
${style}/bin/postgrest-style
${git}/bin/git add .
;;
pre-push)
# Create a clean working tree without any uncommitted changes.
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
;;
esac
;;
full)
case "$_arg_hook" in
pre-commit)
# To be able to automatically add only changes from postgrest-style to the staging area,
# we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping
# the stash afterwards.
${style}/bin/postgrest-style
stash="postgrest-git-hooks-$RANDOM"
${git}/bin/git stash push --include-untracked --keep-index -m "$stash"
if [ "$(git stash list --grep $stash)" ]; then
# Only create the stash pop trap, if we actually created a stash.
# Otherwise stash pop will cause havoc.
trap 'restore "$stash"' EXIT
fi
${style}/bin/postgrest-style
${git}/bin/git add .
${style}/bin/postgrest-lint
;;
pre-push)
# Create a clean working tree without any uncommitted changes.
${withTools.withGit} HEAD ${check}
;;
esac
;;
esac
fi
'';
dumpMinimalImports =
checkedShellScript
{
@@ -281,10 +129,10 @@ let
{
name = "postgrest-hsie-graph-modules";
docs = "Create a PNG graph of modules imported within the codebase.";
args = [ "ARG_POSITIONAL_SINGLE([outfile], [Output filename])" ];
args = [ "ARG_OPTIONAL_SINGLE([outfile], [o], [Output filename], [postgrest-module-graph.png])" ];
}
''
${hsie} graph-modules main src | ${graphviz}/bin/dot -Tpng -o "$_arg_outfile"
${hsie} graph-modules src/library src/executable | ${graphviz}/bin/dot -Tpng -o "$_arg_outfile"
'';
hsieGraphSymbols =
@@ -395,7 +243,6 @@ buildToolbox
inherit
check
dumpMinimalImports
gitHooks
hsieGraphModules
hsieGraphSymbols
hsieMinimalImports
+4
View File
@@ -122,6 +122,8 @@ let
workingDir = "/docs";
}
''
echo "Checking spelling mistakes..."
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
@@ -144,6 +146,8 @@ let
workingDir = "/docs";
}
''
echo "Detecting obsolete dictionary entries..."
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
-52
View File
@@ -1,52 +0,0 @@
# Generate RSA JWK/public material for loadtests.
import argparse
import sys
from pathlib import Path
import jwcrypto.jwk as jwk
def main():
parser = argparse.ArgumentParser(
description="Generate RSA JWK/private key pair for loadtests"
)
parser.add_argument(
"--rsa",
dest="jwk_path",
metavar="JWK_PATH",
type=Path,
required=True,
help="Path to write the RSA JWK file",
)
parser.add_argument(
"--private-key",
dest="private_key_path",
metavar="PRIVATE_KEY_PATH",
type=Path,
required=True,
help="Path to write the RSA private key file",
)
args = parser.parse_args()
key = jwk.JWK.generate(kty="RSA", size=4096)
private_jwk, public_jwk = key.export_private(), key.export_public()
try:
args.jwk_path.write_text(public_jwk)
print(f"Created RSA JWK on {args.jwk_path}")
except OSError as e:
print(f"Error writing to {args.jwk_path}:{e}", file=sys.stderr)
sys.exit(1)
try:
args.private_key_path.write_text(private_jwk)
print(f"Created private key on {args.private_key_path}")
except OSError as e:
print(f"Error writing to {args.private_key_path}:{e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+40 -168
View File
@@ -12,81 +12,39 @@
# from an array
import time
import argparse
import subprocess
import sys
import random
import jwt
from typing import Optional
import jwcrypto.jwt as jwt
from pathlib import Path
from enum import Enum
URL = "http://postgrest"
secret_key = b"reallyreallyreallyreallyverysafe"
secret_key = "reallyreallyreallyreallyverysafe"
def generate_jwt(
def generate_target(
now: int,
exp_inc: Optional[int],
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],
) -> str:
"""Generate an HS256 or RS256 JWT"""
payload = {
key: jwt.JWK,
) -> list[str]:
"""Generate a target using an HS256 or RS256 JWT"""
headers = {
"sub": f"user_{random.getrandbits(32)}",
"iat": now,
}
claims = {
"role": "postgrest_test_author",
}
if exp_inc is not None:
payload["exp"] = now + exp_inc
headers["alg"] = "RS256" if key.get("kty") == "RSA" else "HS256"
if rsa_private_key is None:
key = secret_key
alg = "HS256"
else:
key = rsa_private_key
alg = "RS256"
return jwt.encode(payload, key, alg)
token = jwt.JWT(headers, claims)
token.make_signed_token(key)
HTTP_METHODS = (
"GET",
"OPTIONS",
)
HttpMethod = Enum(
"HttpMethod",
{method: method for method in HTTP_METHODS},
type=str,
module=__name__,
)
def append_targets(lines: list[str], token: str, http_method: HttpMethod):
lines.append(f"{http_method.value} {URL}/authors_only")
lines.append(f"Authorization: Bearer {token}")
lines.append("") # blank line to separate requests
# we use this to chain commands on loadtest.nix
def run_command(command: list[str]):
if not command:
return
if command[0] == "--":
command = command[1:]
if not command:
return
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as exc:
print(
f"Error executing command {' '.join(command)}: {exc}",
file=sys.stderr,
)
sys.exit(exc.returncode)
return [
f"OPTIONS {URL}/authors_only?{headers["alg"]}",
f"Authorization: Bearer {token.serialize()}",
"", # blank line to separate requests
]
def main():
@@ -94,133 +52,47 @@ def main():
description="Generate Vegeta targets with unique JWTs"
)
parser.add_argument(
"targets_path",
metavar="TARGETS_PATH",
help="Path to write the generated targets file",
)
parser.add_argument(
"--private-key",
dest="private_key_path",
metavar="PRIVATE_KEY_PATH",
"generated_path",
metavar="GENERATED_PATH",
help="Path to write the generated files",
type=Path,
default=None,
help="Path to the RSA private key file (required when --rsa is used)",
)
parser.add_argument(
"--worst",
dest="worst",
action=argparse.BooleanOptionalAction,
default=False,
help="Generate worst case targets for a JWT cache",
)
parser.add_argument(
"--rsa",
dest="jwk_path",
metavar="JWK_PATH",
type=Path,
default=None,
help="Path to an existing RSA JWK file used for signing tokens",
)
parser.add_argument(
"--method",
dest="http_method",
choices=list(HTTP_METHODS),
required=True,
help="HTTP method for the vegeta targets",
)
parser.add_argument(
"command",
nargs=argparse.REMAINDER,
help="Command (and arguments) to run after generating the targets",
)
args = parser.parse_args()
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None
targets_path = args.generated_path / "gen_targets.http"
is_hs = args.jwk_path is None
hs = jwt.JWK.from_password(secret_key)
rsa = jwt.JWK.generate(kty="RSA", size=4096)
http_method = HttpMethod(args.http_method)
jwks = jwt.JWKSet()
jwks.add(hs)
jwks.add(rsa)
nsamples = 1000
jwks_path = args.generated_path / "gen_jwks.json"
if is_hs:
ntargets = 200000
else:
# The asymmetric targets take too long to compute so we reduce them
ntargets = 50000
# Technically, this exports the private keys, because HS does not have the concept
# of a public key. This is not a problem for tests, though, PostgREST can verify
# tokens with the private key just as well.
jwks_path.write_text(jwks.export())
print(f"Created JWKSet on {jwks_path}")
if not is_hs:
if args.private_key_path is None:
parser.error("--rsa requires the --private-key option")
try:
private_key_data = args.private_key_path.read_text()
except OSError as e:
err = (
f"Error reading RSA private key from {args.private_key_path}: "
f"{e}. Generate RSA materials first with gen_rsa_materials.py."
)
print(err, file=sys.stderr)
sys.exit(1)
try:
rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)
except Exception as exc: # broad exception to capture parsing errors
err = (
f"Error loading RSA private key from {args.private_key_path}: " f"{exc}"
)
print(err, file=sys.stderr)
sys.exit(1)
ntargets = 1000
print(f"Generating {ntargets} targets...")
start_time = time.time()
now = int(start_time)
now = int(time.time())
lines = []
# We want to ensure 401 Unauthorized responses don't happen during
# JWT validation, this can happen when the jwt `exp` is too short.
# At the same time, we want to ensure the `exp` is not too big,
# so expires will occur and postgREST needs to
# clean cached expired JWTs
if args.worst:
# estimated time takes to build and run postgrest itself
build_run_postgrest_time = 2
for i in range(ntargets):
target = generate_target(now, hs if i % 2 == 0 else rsa)
lines.extend(target)
# estimated time it takes to generate the targets file
# the division numbers are tuned by hand
if is_hs: # hs generation is much faster
gen_time = ntargets // 66666
else: # asymmetric is slower so the time is higher
gen_time = ntargets // 220
with open(targets_path, "w") as f:
f.write("\n".join(lines))
# estimated exp time so some JWTs will expire
inc = build_run_postgrest_time + gen_time
for i in range(ntargets):
token = generate_jwt(now, inc + i // 1000, rsa_private_key)
append_targets(lines, token, http_method)
else:
tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]
for i in range(ntargets):
token = random.choice(tokens)
append_targets(lines, token, http_method)
try:
with open(args.targets_path, "w") as f:
f.write("\n".join(lines))
except IOError as e:
print(f"Error writing to {args.targets_path}: {e}", file=sys.stderr)
sys.exit(1)
elapsed = time.time() - start_time
print(f"Created {ntargets} targets", end=" ")
print(f"in {args.targets_path} ({elapsed:.2f}s)")
run_command(args.command)
if __name__ == "__main__":
+108 -137
View File
@@ -1,7 +1,11 @@
{ buildToolbox
, checkedShellScript
, git
, jq
, libfaketime
, python3
, python3Packages
, runCommand
, vegeta
, withTools
, writers
@@ -42,24 +46,18 @@ let
docs = "Run the vegeta loadtests with PostgREST.";
args = [
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])"
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])"
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-cache,jwt-cache-worst])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
"ARG_LEFTOVERS([additional vegeta arguments])"
];
workingDir = "/";
}
''
# previously required settings to make this work with older branches
export PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
export PGRST_DB_URI="postgresql://"
export PGRST_DB_SCHEMAS="test"
export PGRST_DB_CONFIG="false"
export PGRST_DB_POOL="1"
export PGRST_DB_SCHEMAS="test"
export PGRST_DB_TX_END="rollback-allow-override"
export PGRST_LOG_LEVEL="crit"
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
@@ -68,96 +66,29 @@ let
abs_output="$(realpath "$_arg_output")"
case "$_arg_kind" in
jwt-hs)
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
jwt-cache)
export PGRST_JWT_SECRET="@${generatedTargets}/gen_jwks.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-hs-cache)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-hs-cache-worst)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --worst "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-rsa)
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-rsa-cache)
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
jwt-rsa-cache-worst)
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
${withGenTargets} --method "$_arg_method" --worst --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
mixed)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
sh -c "cd \"$_arg_testdir\" && \
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
${withTools.withPg} -f test/load/fixtures.sql \
${withTools.withPgrst} --faketime '2000-01-01 00:00:00' -m "$_arg_monitor" \
sh -c "cd test/load && \
${runner} -targets ${generatedTargets}/gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
# here we sleep purposefully to check how much memory does the schema cache consume in the final report
errors)
mixed)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/errors.sql \
${withTools.withPg} -f test/load/fixtures.sql \
${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \
sh -c "cd \"$_arg_testdir\" && \
${runner} -targets errors.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
sh -c "cd test/load && \
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;;
esac
${vegeta}/bin/vegeta report -type=text "$_arg_output"
if [ "$_arg_kind" != "errors" ]; then
if [ "$_arg_kind" != "mixed" ]; then
# fail in case 401 happened on jwt loadtests
unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \
| ${jq}/bin/jq -r '.status_codes["401"] // 0')"
@@ -198,6 +129,9 @@ let
Run the vegeta loadtest against every target branch and HEAD:
- once on the every <target-#> branch
- once in the current worktree
Note that the Nix tooling is always taken from the HEAD branch, while the PostgREST binary is taken from the target branch.
For a discussion on why this is set up like this, see https://github.com/PostgREST/postgrest/pull/5013#discussion_r3431508441.
'';
args = [
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
@@ -212,8 +146,40 @@ let
workingDir = "/";
}
''
# run loadtest for every target
for tgt in "''${_arg_target[@]}"; do
# Build postgrest for every target and HEAD.
# Keeps a reference to the postgrest binary and faketime lib for every branch to run later.
declare -A pgrst faketime
for tgt in "''${_arg_target[@]}" HEAD; do
# not using withTmpDir here, because we don't want to keep the directory on error
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
${git}/bin/git worktree add -f "$tmpdir" "$tgt" > /dev/null
pushd "$tmpdir" > /dev/null
build_start=$SECONDS
echo -n "${name}: Building postgrest (nix) on $tgt... "
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
nix-build --no-out-link -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > build.log 2>&1 || {
echo "failed, output:"
cat build.log
exit 1
}
pgrst[$tgt]="$(nix-build --no-out-link -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage')/bin/postgrest"
# To avoid glibc mismatches with back-branches, we need to take libfaketime from the target branch.
faketime[$tgt]="$(nix-build --no-out-link -A pkgs.libfaketime)/lib/libfaketime.so.1"
build_end=$((SECONDS - build_start))
printf "done in %ss.\n" "$build_end"
popd > /dev/null
${git}/bin/git worktree remove -f "$tmpdir" > /dev/null
rm -rf "$tmpdir"
done
# Run loadtest for every target and HEAD.
# Running the tests is separated from building them to reduce the chances of
# other processes skewing the results between two runs.
for tgt in "''${_arg_target[@]}" HEAD; do
cat << EOF
@@ -221,12 +187,7 @@ let
EOF
# Runs the test files from the current working tree
# to make sure both tests are run with the same files.
# Save the results in the current working tree, too,
# otherwise they'd be lost in the temporary working tree
# created by withTools.withGit.
${withTools.withGit} "$tgt" ${loadtest} -k "$_arg_kind" -m "$PWD/loadtest/$tgt.csv" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
FAKETIME_LIB="''${faketime[$tgt]}" PGRST_CMD="''${pgrst[$tgt]}" ${loadtest} -k "$_arg_kind" -m "loadtest/$tgt.csv" --output "loadtest/$tgt.bin"
cat << EOF
@@ -235,22 +196,6 @@ let
EOF
done
# run loadtest once on HEAD
cat << EOF
Running "$_arg_kind" loadtest on HEAD...
EOF
${loadtest} -k "$_arg_kind" -m "$PWD/loadtest/head.csv" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
cat << EOF
Done running on HEAD.
EOF
'';
reporter =
@@ -260,12 +205,14 @@ let
docs = "Create a named json report for a single result file.";
args = [
"ARG_POSITIONAL_SINGLE([file], [Filename of result to create report for])"
"ARG_OPTIONAL_SINGLE([percentile], [p], [Percentile to report latency for], 50)"
"ARG_LEFTOVERS([additional vegeta arguments])"
];
workingDir = "/";
}
''
${vegeta}/bin/vegeta report -type=json "$_arg_file" \
${vegeta}/bin/vegeta encode "$_arg_file" \
| ${jq}/bin/jq --arg percentile "$_arg_percentile" --slurp 'map(select(.url != "")) | group_by("\(.code) \(.method) \(.url)") | map({("\(.[0].code) \(.[0].method) \(.[0].url)" | sub("http://postgrest";"")): map(.latency) | sort | .[(length-1) * ($percentile | tonumber) / 100 | floor] / 10e3 }) | .[]' \
| ${jq}/bin/jq --arg branch "$(basename "$_arg_file" .bin)" '. + {branch: $branch}'
'';
@@ -278,12 +225,31 @@ let
import sys
import pandas as pd
def evaluate_change(df):
try:
return ((df['HEAD'] / df['main'] - 1) * 100) \
.map(lambda r: "{icon} {ratio:.1f} %".format(
ratio=r,
# Hardcoded failure threshold for CI is 5% here.
icon="" if r < 5 else ":x:"
))
except KeyError:
return None
pd.read_json(sys.stdin) \
.set_index('param') \
.drop(['branch', 'earliest', 'end', 'latest']) \
.fillna("") \
.rename(columns={'latency': sys.argv[1]}) \
.set_index(sys.argv[1]) \
.drop(['branch']) \
.convert_dtypes() \
.to_markdown(sys.stdout, floatfmt='.0f')
.assign(change=evaluate_change) \
.to_markdown(
sys.stdout,
floatfmt='.1f',
colglobalalign='right',
colalign=('left',)
)
'';
@@ -294,41 +260,46 @@ let
docs = "Create a report of all loadtest reports as markdown.";
args = [
"ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])"
"ARG_OPTIONAL_SINGLE([percentile], [p], [Percentile to report latency for], 50)"
];
workingDir = "/";
}
''
marker=''${_arg_group:+"($_arg_group)"}
echo -e "## Loadtest results $_arg_group (P$_arg_percentile)\n"
echo -e "## Loadtest results $marker\n"
find loadtest -type f -iname '*.bin' -exec ${reporter} -p "$_arg_percentile" {} \; \
| ${jq}/bin/jq '[paths(scalars) as $path | {latency: $path | join("."), (.branch): getpath($path)}]' \
| ${jq}/bin/jq --slurp 'flatten | group_by(.latency) | map(add)' \
| ${toMarkdown} "P$_arg_percentile latency [μs]"
'';
find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \
| ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join("."), (.branch): getpath($path)}]' \
| ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \
| ${toMarkdown}
echo -e "\n\n## Loadtest elapsed seconds vs CPU/MEM usage $marker\n"
report-load =
checkedShellScript
{
name = "postgrest-loadtest-report-load";
docs = "Create a report of all CPU/MEM usage as markdown.";
args = [
"ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])"
];
workingDir = "/";
}
''
echo -e "\n\n## Loadtest elapsed seconds vs CPU/MEM usage $_arg_group\n"
find loadtest -type f -iname '*.csv' \
| sort -m \
| ${mergeMonitorResults}
'';
withGenTargets =
writers.writePython3 "postgrest-with-gen-loadtest-targets"
generatedTargets =
runCommand "postgrest-loadtest-targets"
{
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
doCheck = false; # postgrest-style conflicts with this
nativeBuildInputs = [ (python3.withPackages (pyps: [ pyps.jwcrypto ])) ];
}
(builtins.readFile ./generate_targets.py);
genRsaMaterials =
writers.writePython3 "postgrest-gen-rsa-materials"
{
libraries = [ python3Packages.jwcrypto ];
doCheck = false; # postgrest-style conflicts with this
}
(builtins.readFile ./gen_rsa_materials.py);
''
mkdir -p "$out"
${libfaketime}/bin/faketime '2000-01-01 00:00:00' python3 ${./generate_targets.py} "$out"
'';
mergeMonitorResults =
writers.writePython3 "postgrest-merge-monitor-results"
@@ -339,5 +310,5 @@ let
in
buildToolbox {
name = "postgrest-loadtest";
tools = { inherit loadtest loadtestAgainst report; };
tools = { inherit loadtest loadtestAgainst report report-load; };
}
+1 -1
View File
@@ -17,7 +17,7 @@ let
nix flake update
echo "# This file is auto-generated by postgrest-nixpkgs-upgrade" > docs/requirements.txt
cat "$(nix-build -A docs.requirements)" >> docs/requirements.txt
cat "$(nix-build --no-out-link -A docs.requirements)" >> docs/requirements.txt
'';
in
+6 -5
View File
@@ -7,7 +7,6 @@ let
{
name = "postgrest-release";
docs = "Patch postgrest.cabal, CHANGELOG.md, commit and push all in one go.";
args = [ "ARG_OPTIONAL_BOOLEAN([major], [m], [Bump to new major version (only applies on main branch).])" ];
workingDir = "/";
}
''
@@ -20,7 +19,6 @@ let
git diff --exit-code HEAD postgrest.cabal > /dev/null
trap "" ERR
# TODO: Support C+D bumps when implementing hackage releases
bump () {
current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
# shellcheck disable=SC2034
@@ -47,7 +45,10 @@ let
echo "Updating docs/conf.py ..."
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
git add postgrest.cabal docs/conf.py > /dev/null
echo "Updating Haskell source file links ..."
sed -i -E "s#(github\.com/PostgREST/postgrest/blob)/main/#\1/$new_version/#g" docs/explanations/architecture.rst
git add postgrest.cabal docs/conf.py docs/explanations/architecture.rst > /dev/null
}
today_date_for_changelog="$(date '+%Y-%m-%d')"
@@ -62,7 +63,7 @@ let
git add CHANGELOG.md > /dev/null
echo "Committing ..."
git commit -m "bump version to $new_version" > /dev/null
git commit -m "chore: bump version to $new_version" > /dev/null
if [[ "$current_branch" == "main" ]]; then
bump devel
@@ -74,7 +75,7 @@ let
git branch "v$A"
echo "Committing (devel bump)..."
git commit -m "bump version to $new_version" > /dev/null
git commit -m "chore: bump version to $new_version" > /dev/null
fi
trap "echo Remote not found. Please push manually ..." ERR
+8 -9
View File
@@ -3,13 +3,13 @@
, buildToolbox
, checkedShellScript
, deadnix
, fd
, git
, hlint
, hsie
, nixpkgs-fmt
, python3Packages
, ruff
, silver-searcher
, statix
, stylish-haskell
, writeText
@@ -21,19 +21,19 @@ let
name = "postgrest-style";
docs = "Automatically format Haskell, Nix and Python files.";
workingDir = "/";
withTmpDir = true;
}
''
# Format Nix files
${statix}/bin/statix fix
${nixpkgs-fmt}/bin/nixpkgs-fmt . > /dev/null 2> /dev/null
${nixpkgs-fmt}/bin/nixpkgs-fmt .
# Format Haskell files
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
${fd}/bin/fd '\.l?hs$' \
| xargs ${stylish-haskell}/bin/stylish-haskell -i
# Format Python files
${black}/bin/black . 2> /dev/null
TMPDIR="$tmpdir" ${black}/bin/black .
'';
# Script to check whether any uncommitted changes result from postgrest-style
@@ -82,18 +82,17 @@ let
# ruff has gaps in scanning for unused code, so we use vulture
echo "Scanning python files for unused code..."
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
${fd}/bin/fd '\.l?py$' \
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py --min-confidence 80
echo "Linting python files..."
${ruff}/bin/ruff check .
echo "Checking consistency of import aliases in Haskell code..."
${hsie} check-aliases main src
${hsie} check-aliases src/library src/executable
echo "Linting Haskell files..."
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
${fd}/bin/fd '\.l?hs$' \
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
'';
+3 -2
View File
@@ -9,6 +9,7 @@
, hpc-codecov
, jq
, lib
, nginx
, postgrest
, python3
, runtimeShell
@@ -54,8 +55,6 @@ let
withEnv = postgrest.env;
}
''
# This makes nix-env -iA tests.doctests.bin work.
export NIX_GHC=${postgrest.env.NIX_GHC}
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
'';
@@ -93,6 +92,7 @@ let
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
workingDir = "/";
withEnv = postgrest.env;
withPath = [ nginx ];
}
''
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
@@ -155,6 +155,7 @@ let
redirectTixFiles = false;
withEnv = postgrest.env;
withTmpDir = true;
withPath = [ nginx ];
}
(
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
+27 -77
View File
@@ -1,8 +1,8 @@
{ buildToolbox
, checkedShellScript
, curl
, git
, lib
, libfaketime
, postgresqlVersions
, postgrest
, python3Packages
@@ -11,9 +11,15 @@
}:
let
withTmpDb =
{ name, postgresql }:
{ name, postgresql, config ? "" }:
let
commandName = "postgrest-with-${name}";
postgresqlConf = writeText "postgresql.conf" ("
autovacuum = false
listen_addresses = ''
log_statement = all
shared_preload_libraries=pg_stat_statements
" + config);
in
checkedShellScript
{
@@ -72,6 +78,10 @@ let
TZ=$PGTZ initdb --no-locale --encoding=UTF8 --nosync -U postgres --auth=trust \
>> "$setuplog"
# Append our own config to the one initdb created to avoid replacing
# default values created by the latter.
cat ${postgresqlConf} >> "$tmpdir/db/postgresql.conf"
log "Starting the database cluster..."
# Instead of listening on a local port, we will listen on a unix domain socket.
@@ -80,7 +90,7 @@ let
# On MacOS, it's 104 chars
# See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c hba_file=$HBA_FILE -k $PGHOST " \
>> "$setuplog"
log "Creating a minimally privileged $PGUSER connection role..."
@@ -106,7 +116,7 @@ let
log "Starting replica on $replica_host"
# We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s)
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c hba_file=$HBA_FILE -k $replica_host -c max_standby_streaming_delay=\"3s\" " \
>> "$setuplog"
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
@@ -135,6 +145,7 @@ let
load_start=$SECONDS
>&2 printf "${commandName}: Loading fixtures under the postgres role..."
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
psql -U postgres -v ON_ERROR_STOP=1 -c "VACUUM ANALYZE;" >> "$setuplog"
load_end=$((SECONDS - load_start))
>&2 printf " done in %ss. Running command...\n" "$load_end"
fi
@@ -185,59 +196,6 @@ let
withPg = withTmpDb (builtins.head postgresqlVersions);
withGit =
let
name = "postgrest-with-git";
in
checkedShellScript
{
inherit name;
docs =
''
Create a new worktree of the postgrest repo in a temporary directory and
check out <commit>, then run <command> with arguments inside the temporary folder.
'';
args =
[
"ARG_POSITIONAL_SINGLE([commit], [Commit-ish reference to run command with])"
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])"
];
positionalCompletion =
''
if test "$prev" == "${name}"; then
__gitcomp_nl "$(__git_refs)"
else
_command_offset 2
fi
'';
workingDir = "/";
}
''
# not using withTmpDir here, because we don't want to keep the directory on error
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
${git}/bin/git worktree add -f "$tmpdir" "$_arg_commit" > /dev/null
cd "$tmpdir"
("$_arg_command" "''${_arg_leftovers[@]}")
${git}/bin/git worktree remove -f "$tmpdir" > /dev/null
'';
legacyConfig =
writeText "legacy.conf"
''
# Using this config file to support older postgrest versions for `postgrest-loadtest-against`
db-uri="$(PGRST_DB_URI)"
db-schema="$(PGRST_DB_SCHEMAS)"
db-anon-role="$(PGRST_DB_ANON_ROLE)"
db-pool="$(PGRST_DB_POOL)"
server-unix-socket="$(PGRST_SERVER_UNIX_SOCKET)"
log-level="$(PGRST_LOG_LEVEL)"
'';
waitForPgrstReady =
checkedShellScript
{
@@ -290,10 +248,12 @@ let
[
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])"
"ARG_OPTIONAL_SINGLE([faketime], [f], [Fake the system time when starting PostgREST. This is useful to test expiry of JWT, for example in loadtests])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])"
"ARG_OPTIONAL_SINGLE([sleep], [s], [Sleep time after PostgREST is ready, this is useful for monitoring])"
"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])"
"ARG_USE_ENV([FAKETIME_LIB], [${libfaketime}/lib/libfaketime.so.1], [Faketime Library to preload])"
"ARG_USE_ENV([PGRST_CMD], [postgrest-run], [PostgREST executable to run])"
];
positionalCompletion = "_command";
workingDir = "/";
@@ -303,32 +263,23 @@ let
''
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
if [ -z "''${PGRST_CMD:-}" ]; then
rm -f result
if [ "''${PGRST_CMD}" == "postgrest-run" ]; then
build_start=$SECONDS
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
echo -n "${commandName}: Building postgrest (nix)... "
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
echo "failed, output:"
cat "$tmpdir"/build.log
exit 1
}
PGRST_CMD=$(echo ./result*/bin/postgrest)
else
echo -n "${commandName}: Building postgrest (cabal)... "
postgrest-build
PGRST_CMD=postgrest-run
fi
echo -n "${commandName}: Building postgrest (cabal)... "
postgrest-build
build_end=$((SECONDS - build_start))
printf "done in %ss.\n" "$build_end"
fi
ver=$($PGRST_CMD ${legacyConfig} --version)
ver=$($PGRST_CMD --version)
echo -n "${commandName}: Starting $ver... "
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
if [[ -n "$_arg_faketime" ]]; then
LD_PRELOAD="$FAKETIME_LIB" FAKETIME="$_arg_faketime" "$PGRST_CMD" > "$tmpdir"/run.log 2>&1 &
else
$PGRST_CMD > "$tmpdir"/run.log 2>&1 &
fi
pid=$!
# shellcheck disable=SC2329
cleanup() {
@@ -378,7 +329,6 @@ buildToolbox
name = "postgrest-with";
tools = {
inherit
withGit
withPgAll
withPgrst;
} // builtins.listToAttrs (
+34 -22
View File
@@ -1,6 +1,6 @@
cabal-version: 3.0
name: postgrest
version: 15
version: 17
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
@@ -38,13 +38,16 @@ flag hpc
description: Enable HPC (dev only)
library
default-language: Haskell2010
default-language: GHC2021
default-extensions: OverloadedStrings
NoImplicitPrelude
hs-source-dirs: src
hs-source-dirs: src/library
exposed-modules: PostgREST.Admin
PostgREST.App
PostgREST.AppState
PostgREST.AppState.Pool
PostgREST.AppState.Reload
PostgREST.AppState.Types
PostgREST.Auth
PostgREST.Auth.Jwt
PostgREST.Auth.JwtCache
@@ -67,9 +70,9 @@ library
PostgREST.SchemaCache.Table
PostgREST.Error
PostgREST.Error.Types
PostgREST.Listener
PostgREST.Logger
PostgREST.MainTx
PostgREST.Logger.Apache
PostgREST.MediaType
PostgREST.Metrics
PostgREST.Network
@@ -102,7 +105,8 @@ library
, HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.6
, aeson >= 2.0.3 && < 2.3
, auto-update >= 0.1.4 && < 0.3
, aeson-jsonpath >= 0.4.2 && < 0.5
, auto-update >= 0.2.7 && < 0.3
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
, case-insensitive >= 1.2 && < 1.3
@@ -116,6 +120,7 @@ library
, directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0
, fast-logger >= 3.2.0 && < 3.3
, fuzzyset >= 0.2.4 && < 0.3
, hasql >= 1.9 && <= 1.9.3.1
, hasql-dynamic-statements >= 0.3.1 && <= 0.3.1.8
@@ -136,6 +141,7 @@ library
, parsec >= 3.1.11 && < 3.2
, postgresql-libpq >= 0.10
, prometheus-client >= 1.1.1 && < 1.2.0
, prometheus-metrics-ghc >= 1.0.1.2 && < 1.2
, protolude >= 0.3.1 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.4
, retry >= 0.7.4 && < 0.10
@@ -146,27 +152,27 @@ library
, time >= 1.6 && < 1.15
, unordered-containers >= 0.2.8 && < 0.3
, unix-compat >= 0.5.4 && < 0.8
, vault >= 0.3.1.5 && < 0.4
, vector >= 0.11 && < 0.14
, wai >= 3.2.1 && < 3.3
, wai-cors >= 0.2.5 && < 0.3
, wai-extra >= 3.1.8 && < 3.2
-- We already depend on wai-logger >= 2.3.7 indirectly via wai-extra,
-- but we want to depend on 2.4.0 which fixes 'unknownSocket' log output
-- for unix sockets; this is tested in test/io/test_io.py. See
-- for unix sockets; this is tested in test/io/test_log.py. See
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
, wai-logger >= 2.4.0
, warp >= 3.4.13 && < 3.5
, warp >= 3.4.14 && < 3.5
, stm >= 2.5 && < 3
, stm-hamt >= 1.2 && < 2
, focus >= 1.0 && < 2
, some >= 1.0.4.1 && < 2
, uuid >= 1.3 && < 2
-- -fno-spec-constr may help keep compile time memory use in check,
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
-- -optP-Wno-nonportable-include-path
-- prevents build failures on case-insensitive filesystems (macos),
-- see https://github.com/commercialhaskell/stack/issues/3918
ghc-options: -Werror -Wall -fwarn-identities
ghc-options: -j -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
if flag(dev)
@@ -175,22 +181,27 @@ library
ghc-options: -fhpc -hpcdir .hpc
else
ghc-options: -O2
if impl(ghc >= 9.12)
-- Makes GHC consider cross-module specialization for polymorphic functions
-- without explicitly needing to add INLINE, INLINABLE or SPECIALIZE pragmas.
-- Slightly increases the binary size but improves performance considerably.
ghc-options: -fexpose-overloaded-unfoldings -fspecialise-aggressively
if !os(windows)
build-depends:
unix
executable postgrest
default-language: Haskell2010
default-language: GHC2021
default-extensions: OverloadedStrings
NoImplicitPrelude
hs-source-dirs: main
hs-source-dirs: src/executable
main-is: Main.hs
build-depends: base >= 4.9 && < 4.22
, containers >= 0.5.7 && < 0.8
, postgrest
, protolude >= 0.3.1 && < 0.4
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
ghc-options: -j -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
-O2 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
@@ -205,7 +216,7 @@ executable postgrest
test-suite spec
type: exitcode-stdio-1.0
default-language: Haskell2010
default-language: GHC2021
default-extensions: OverloadedStrings
QuasiQuotes
NoImplicitPrelude
@@ -215,10 +226,12 @@ test-suite spec
Feature.Auth.AudienceJwtSecretSpec
Feature.Auth.AuthSpec
Feature.Auth.BinaryJwtSecretSpec
Feature.Auth.JwtCacheSpec
Feature.Auth.NoAnonSpec
Feature.Auth.NoJwtSecretSpec
Feature.ConcurrentSpec
Feature.CorsSpec
Feature.HttpHeaderSpec
Feature.ExtraSearchPathSpec
Feature.NoSuperuserSpec
Feature.ObservabilitySpec
@@ -247,6 +260,7 @@ test-suite spec
Feature.Query.Preferences.HandlingSpec
Feature.Query.Preferences.MaxAffectedSpec
Feature.Query.Preferences.TimezoneSpec
Feature.Query.PreparedStatementsSpec
Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec
Feature.Query.RangeSpec
@@ -289,10 +303,11 @@ test-suite spec
, regex-tdfa >= 1.2.2 && < 1.4
, scientific >= 0.3.4 && < 0.4
, text >= 1.2.2 && < 2.2
, time >= 1.6 && < 1.15
, transformers-base >= 0.4.4 && < 0.5
, wai >= 3.2.1 && < 3.3
, wai-extra >= 3.0.19 && < 3.2
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
ghc-options: -j -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-fno-warn-missing-signatures
-fwrite-ide-info
@@ -301,7 +316,7 @@ test-suite spec
test-suite observability
type: exitcode-stdio-1.0
default-language: Haskell2010
default-language: GHC2021
default-extensions: OverloadedStrings
QuasiQuotes
NoImplicitPrelude
@@ -327,7 +342,7 @@ test-suite observability
, protolude >= 0.3.1 && < 0.4
, text >= 1.2.2 && < 2.2
, wai >= 3.2.1 && < 3.3
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
ghc-options: -j -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-fwrite-ide-info
-- https://github.com/PostgREST/postgrest/issues/387
@@ -335,15 +350,12 @@ test-suite observability
test-suite doctests
type: exitcode-stdio-1.0
default-language: Haskell2010
default-extensions: OverloadedStrings
NoImplicitPrelude
default-language: GHC2021
hs-source-dirs: test/doc
main-is: Main.hs
build-depends: base >= 4.9 && < 4.22
, doctest >= 0.8
, doctest-parallel >= 0.4
, postgrest
, pretty-simple
, protolude >= 0.3.1 && < 0.4
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
ghc-options: -j -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-288
View File
@@ -1,288 +0,0 @@
{-|
Module : PostgREST.App
Description : PostgREST main application
This module is in charge of mapping HTTP requests to PostgreSQL queries.
Some of its functionality includes:
- Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION.
- Producing HTTP Headers according to RFCs.
- Content Negotiation
-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module PostgREST.App
( postgrest
, run
) where
import GHC.IO.Exception (IOErrorType (..))
import System.IO.Error (ioeGetErrorType)
import Control.Monad.Except (liftEither)
import Control.Monad.Extra (whenJust)
import Data.Either.Combinators (mapLeft, whenLeft)
import Data.Maybe (fromJust)
import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost,
setOnException, setPort,
setServerName)
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified PostgREST.Admin as Admin
import qualified PostgREST.ApiRequest as ApiRequest
import qualified PostgREST.AppState as AppState
import qualified PostgREST.Auth as Auth
import qualified PostgREST.Cors as Cors
import qualified PostgREST.Error as Error
import qualified PostgREST.Listener as Listener
import qualified PostgREST.Logger as Logger
import qualified PostgREST.MainTx as MainTx
import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query
import qualified PostgREST.Response as Response
import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..))
import PostgREST.Error (Error)
import PostgREST.Network (resolveSocketToAddress)
import PostgREST.Observation (Observation (..))
import PostgREST.Response.Performance (ServerTiming (..),
serverTimingHeader)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (docsVersion, prettyVersion)
import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L
import Data.Streaming.Network (bindPortTCP,
bindRandomPortTCP)
import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP
import qualified Network.HTTP.Types.Header as HTTP (hVary)
import qualified Network.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket)
import Protolude hiding (Handler)
type Handler = ExceptT Error
run :: AppState -> IO ()
run appState = do
conf@AppConfig{..} <- AppState.getConfig appState
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
(mainSocket, adminSocket) <- initSockets conf
let closeSockets = do
whenJust adminSocket NS.close
NS.close mainSocket
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Listener.runListener appState
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
do
address <- resolveSocketToAddress mainSocket
observer $ AppServerAddressObs address
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
where
observer = AppState.getObserver appState
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
onWarpException _ ex =
when (shouldDisplayException ex) $
observer $ WarpServerObs $ show ex
-- Similar to wai defaultShouldDisplayException in
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
shouldDisplayException :: SomeException -> Bool
shouldDisplayException se
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
| otherwise = True
serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} =
defaultSettings
& setHost (fromString $ toS configServerHost)
& setPort configServerPort
& setServerName ("postgrest/" <> prettyVersion)
-- | PostgREST application
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
postgrest logLevel appState connWorker =
traceHeaderMiddleware appState .
Cors.middleware appState .
Auth.middleware appState .
Logger.middleware logLevel Auth.getRole $
-- fromJust can be used, because the auth middleware will **always** add
-- some AuthResult to the vault.
\req respond -> do
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
case fromJust $ Auth.getResult req of
Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err
Right authResult -> do
maybeSchemaCache <- AppState.getSchemaCache appState
let
eitherResponse :: IO (Either Error Wai.Response)
eitherResponse =
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse
-- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before
-- the connWorker is done. However, when there's an empty schema cache
-- postgrest responds with the error `PGRST002`; this means that the schema
-- cache is still loading, so we don't launch the connWorker here because
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
resp <- do
delay <- AppState.getNextDelay appState
return $ addRetryHint delay response
respond resp
postgrestResponse
:: AppState.AppState
-> AppConfig
-> Maybe SchemaCache
-> AuthResult
-> Wai.Request
-> Handler IO Wai.Response
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthResult{..} req = do
let observer = AppState.getObserver appState
sCache <-
case maybeSchemaCache of
Just sCache ->
return sCache
Nothing -> do
lift $ observer SchemaCacheEmptyObs
throwError Error.NoSchemaCacheError
body <- lift $ Wai.strictRequestBody req
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
timezones = dbTimezones sCache
prefs = ApiRequest.userPreferences conf req timezones
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
(txTime, txResult) <- withTiming $ do
case tx of
MainTx.NoDbTx r -> pure r
MainTx.DbTx{..} -> do
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
-- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
-- This needs refactoring so only the below obsQuery is used.
lift $ whenLeft eitherResp $ obsQuery . Error.status
liftEither eitherResp
(respTime, resp) <- withTiming $ do
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
status' = either Error.status Response.pgrstStatus response
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
lift $ obsQuery status'
liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
where
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
varyHeader :: HTTP.Header
varyHeader = (HTTP.hVary, "Accept, Prefer, Range")
varyHeaderPresent :: [HTTP.Header] -> Bool
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
withTiming f = if configServerTimingEnabled
then do
(t, r) <- timeItT f
pure (Just t, r)
else do
r <- f
pure (Nothing, r)
traceHeaderMiddleware :: AppState -> Wai.Middleware
traceHeaderMiddleware appState app req respond = do
conf <- AppState.getConfig appState
case configServerTraceHeader conf of
Nothing -> app req respond
Just hdr ->
let hdrVal = L.lookup hdr $ Wai.requestHeaders req in
app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))
addRetryHint :: Int -> Wai.Response -> Wai.Response
addRetryHint delay response = do
let h = ("Retry-After", BS.pack $ show delay)
Wai.mapResponseHeaders (\hs -> if isServiceUnavailable response then h:hs else hs) response
isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
type AppSockets = (NS.Socket, Maybe NS.Socket)
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminHost = configAdminServerHost
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
-436
View File
@@ -1,436 +0,0 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
module PostgREST.AppState
( AppState
, destroy
, getConfig
, getSchemaCache
, getMainThreadId
, getPgVersion
, getNextDelay
, getNextListenerDelay
, getTime
, getJwtCacheState
, init
, initWithPool
, putConfig -- For tests TODO refactoring
, putNextListenerDelay
, putSchemaCache
, putPgVersion
, putIsListenerOn
, usePool
, readInDbConfig
, schemaCacheLoader
, getObserver
, isLoaded
, isPending
) where
import qualified Data.ByteString.Char8 as BS
import Data.Either.Combinators (whenLeft)
import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Config as SQL
import qualified Hasql.Session as SQL
import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Status as HTTP
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (prettyVersion)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction)
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
exponentialBackoff, retrying,
rsPreviousDelay)
import Data.IORef (IORef, atomicWriteIORef, newIORef,
readIORef)
import Data.Time.Clock (UTCTime, getCurrentTime)
import PostgREST.Auth.JwtCache (JwtCacheState, update)
import PostgREST.Config (AppConfig (..),
readAppConfig,
toConnectionSettings)
import PostgREST.Config.Database (queryDbSettings,
queryPgVersion,
queryRoleSettings)
import PostgREST.Config.PgVersion (PgVersion (..),
minimumPgVersion)
import PostgREST.Debounce (makeDebouncer)
import PostgREST.SchemaCache (SchemaCache (..),
querySchemaCache,
showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi)
import Protolude
data AppState = AppState
-- | Database connection pool
{ statePool :: SQL.Pool
-- | Database server version
, statePgVersion :: IORef PgVersion
-- | Schema cache
, stateSchemaCache :: IORef (Maybe SchemaCache)
-- | The schema cache status
, stateSCacheStatus :: SchemaCacheStatus
-- | State of the LISTEN channel
, stateIsListenerOn :: IORef Bool
-- | starts the connection worker with a debounce
, debouncedSCacheLoader :: IO ()
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
, stateGetTime :: IO UTCTime
-- | Used for killing the main thread in case a subthread fails
, stateMainThreadId :: ThreadId
-- | Keeps track of the next delay for db connection retry
, stateNextDelay :: IORef Int
-- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int
-- | Observation handler
, stateObserver :: ObservationHandler
-- | JWT Cache
, stateJwtCache :: JwtCache.JwtCacheState
, stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState
}
-- | Schema cache status.
-- Empty means pending and full means loaded.
newtype SchemaCacheStatus = SchemaCacheStatus
{ getSCStatusMVar :: MVar ()
}
init :: AppConfig -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
loggerState <- Logger.init
metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
observer $ AppStartObs prettyVersion
pool <- initPool conf observer
initWithPool pool conf loggerState metricsState observer
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool pool conf loggerState metricsState observer = mdo
appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
<*> newIORef Nothing
<*> newSchemaCacheStatus
<*> newIORef False
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
<*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> myThreadId
<*> newIORef 0
<*> newIORef 1
<*> pure observer
<*> JwtCache.init conf observer
<*> pure loggerState
<*> pure metricsState
return appState
destroy :: AppState -> IO ()
destroy = destroyPool
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool cfg@AppConfig{..} observer = do
SQL.acquire $ SQL.settings
[ SQL.size configDbPoolSize
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg
, SQL.observationHandler $ observer . HasqlPoolObs
]
-- | Run an action with a database connection.
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
observer PoolRequest
res <- SQL.use statePool sess
observer PoolRequestFullfilled
whenLeft res (\case
SQL.AcquisitionTimeoutUsageError ->
observer PoolAcqTimeoutObs
err@(SQL.ConnectionUsageError e) ->
let failureMessage = BS.unpack $ fromMaybe mempty e in
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
observer $ ExitDBFatalError ServerAuthError err
killThread mainThreadId
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
handleResultError err tpl resultErr
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
-- Passing the empty template will not work for schema cache queries, see TODO further below.
handleResultError err mempty resultErr
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
-- An error on the client-side, usually indicates problems with connection
observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
)
return res
where
handleResultError err tpl resultErr = do
case resultErr of
SQL.UnexpectedResult{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
SQL.RowError{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
SQL.UnexpectedAmountOfRows{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
-- Check for a syntax error (42601 is the pg code) only for queries that don't have `WITH pgrst_source` as prefix.
-- This would mean the error is on our schema cache queries, so we treat it as fatal.
-- TODO have a better way to mark this as a schema cache query
SQL.ServerError "42601" _ _ _ _ ->
unless ("WITH pgrst_source" `BS.isPrefixOf` tpl) $ do
observer $ ExitDBFatalError ServerPgrstBug err
killThread mainThreadId
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
-- This would mean that a connection pooler in transaction mode is being used
-- while prepared statements are enabled in the PostgREST configuration,
-- both of which are incompatible with each other.
SQL.ServerError "42P05" _ _ _ _ -> do
observer $ ExitDBFatalError ServerError42P05 err
killThread mainThreadId
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _ -> do
observer $ ExitDBFatalError ServerError08P01 err
killThread mainThreadId
SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err
-- | Flush the connection pool so that any future use of the pool will
-- use connections freshly established after this call.
-- | Emits PoolFlushed observation
flushPool :: AppState -> IO ()
flushPool AppState{..} = do
SQL.release statePool
stateObserver PoolFlushed
-- | Destroy the pool on shutdown.
-- | Differs from flushPool in not emiting PoolFlushed observation.
destroyPool :: AppState -> IO ()
destroyPool AppState{..} = SQL.release statePool
getPgVersion :: AppState -> IO PgVersion
getPgVersion = readIORef . statePgVersion
putPgVersion :: AppState -> PgVersion -> IO ()
putPgVersion = atomicWriteIORef . statePgVersion
getSchemaCache :: AppState -> IO (Maybe SchemaCache)
getSchemaCache = readIORef . stateSchemaCache
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
schemaCacheLoader :: AppState -> IO ()
schemaCacheLoader = debouncedSCacheLoader
getNextDelay :: AppState -> IO Int
getNextDelay = readIORef . stateNextDelay
getNextListenerDelay :: AppState -> IO Int
getNextListenerDelay = readIORef . stateNextListenerDelay
putNextListenerDelay :: AppState -> Int -> IO ()
putNextListenerDelay = atomicWriteIORef . stateNextListenerDelay
getConfig :: AppState -> IO AppConfig
getConfig = readIORef . stateConf
putConfig :: AppState -> AppConfig -> IO ()
putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache
getMainThreadId :: AppState -> ThreadId
getMainThreadId = stateMainThreadId
isConnEstablished :: AppState -> IO Bool
isConnEstablished appState = do
AppConfig{..} <- getConfig appState
if configDbChannelEnabled then -- if the listener is enabled, we can be sure the connection is up
readIORef $ stateIsListenerOn appState
else -- otherwise the only way to check the connection is to make a query
isRight <$> usePool appState (SQL.sql "SELECT 1")
putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
isLoaded :: AppState -> IO Bool
isLoaded x = do
scacheLoaded <- isSchemaCacheLoaded x
connEstablished <- isConnEstablished x
return $ scacheLoaded && connEstablished
isPending :: AppState -> IO Bool
isPending x = do
scacheLoaded <- isSchemaCacheLoaded x
connEstablished <- isConnEstablished x
return $ not scacheLoaded || not connEstablished
getObserver :: AppState -> ObservationHandler
getObserver = stateObserver
-- | Try to load the schema cache and retry if it fails.
--
-- This is done by repeatedly: 1) flushing the pool, 2) querying the version and validating that the postgres version is supported by us, and 3) loading the schema cache.
-- It's necessary to flush the pool:
--
-- + Because connections cache the pg catalog(see #2620)
-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.
retryingSchemaCacheLoad :: AppState -> IO ()
retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} =
void $ retrying retryPolicy shouldRetry (\RetryStatus{rsIterNumber, rsPreviousDelay} -> do
when (rsIterNumber > 0) $ do
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
observer $ ConnectionRetryObs delay
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
)
where
qPgVersion :: IO (Maybe PgVersion)
qPgVersion = do
AppConfig{..} <- getConfig appState
pgVersion <- usePool appState queryPgVersion
case pgVersion of
Left e -> do
observer $ QueryPgVersionError e
unless configDbPoolAutomaticRecovery $ do
observer ExitDBNoRecoveryObs
killThread mainThreadId
return Nothing
Right actualPgVersion ->
if actualPgVersion < minimumPgVersion then do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId
return Nothing
else do
observer $ DBConnectedObs $ pgvFullName actualPgVersion
observer $ PoolInit configDbPoolSize
putPgVersion appState actualPgVersion
return $ Just actualPgVersion
qInDbConfig :: IO ()
qInDbConfig = do
AppConfig{..} <- getConfig appState
when configDbConfig $ readInDbConfig False appState
qSchemaCache :: IO (Maybe SchemaCache)
qSchemaCache = do
conf@AppConfig{..} <- getConfig appState
(resultTime, result) <-
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
case result of
Left e -> do
markSchemaCachePending appState
putSchemaCache appState Nothing
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
return Nothing
Right sCache -> do
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
markSchemaCachePending appState
putSchemaCache appState $ Just sCache
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
-- Flush the pool after loading the schema cache to reset any stale session cache entries
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
-- and after marking sCacheStatus as pending,
flushPool appState
observer $ SchemaCacheQueriedObs resultTime $ dbQueryTimings sCache
observer $ SchemaCacheLoadedObs loadTime summary
markSchemaCacheLoaded appState
return $ Just sCache
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
shouldRetry _ (pgVer, sCache) = do
AppConfig{..} <- getConfig appState
let itShould = configDbPoolAutomaticRecovery && (isNothing pgVer || isNothing sCache)
return itShould
retryPolicy :: RetryPolicy
retryPolicy =
let delayMicroseconds = 32*oneSecondInUs {-32 seconds-} in
capDelay delayMicroseconds $ exponentialBackoff oneSecondInUs
oneSecondInUs = 1000000 -- one second in microseconds
newSchemaCacheStatus :: IO SchemaCacheStatus
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar
markSchemaCachePending :: AppState -> IO ()
markSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus
markSchemaCacheLoaded :: AppState -> IO ()
markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus
isSchemaCacheLoaded :: AppState -> IO Bool
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
-- | 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
conf <- getConfig appState
pgVer <- getPgVersion appState
dbSettings <-
if configDbConfig conf then do
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf))
case qDbSettings of
Left e -> do
observer $ ConfigReadErrorObs e
pure mempty
Right x -> pure x
else
pure mempty
(roleSettings, roleIsolationLvl) <-
if configDbConfig conf then do
rSettings <- usePool appState (queryRoleSettings pgVer)
case rSettings of
Left e -> do
observer $ QueryRoleSettingsErrorObs e
pure (mempty, mempty)
Right x -> pure x
else
pure mempty
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
else
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
update (getJwtCacheState appState) newConf
if startingUp then
pass
else
observer ConfigSucceededObs
-77
View File
@@ -1,77 +0,0 @@
{-# LANGUAGE RecordWildCards #-}
{-|
Module : PostgREST.Auth
Description : PostgREST authentication functions.
This module provides functions to deal with the JWT authentication (http://jwt.io).
It also can be used to define other authentication functions,
in the future Oauth, LDAP and similar integrations can be coded here.
Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database.
-}
module PostgREST.Auth
( getResult
, getJwtDur
, getRole
, middleware
) where
import qualified Data.ByteString as BS
import qualified Data.Vault.Lazy as Vault
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.HttpAuth as Wai
import Data.List (lookup)
import PostgREST.TimeIt (timeItT)
import System.IO.Unsafe (unsafePerformIO)
import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
getTime)
import PostgREST.Auth.Jwt (parseClaims)
import PostgREST.Auth.JwtCache (lookupJwtCache)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.Error (Error (..))
import Protolude
-- | Validate authorization header
-- Parse and store JWT claims for future use in the request.
middleware :: AppState -> Wai.Middleware
middleware appState app req respond = do
conf@AppConfig{..} <- getConfig appState
time <- getTime appState
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
parseJwt = runExceptT $ lookupJwtCache jwtCacheState token >>= parseClaims conf time
jwtCacheState = getJwtCacheState appState
-- If ServerTimingEnabled -> calculate JWT validation time
req' <- if configServerTimingEnabled then do
(dur, authResult) <- timeItT parseJwt
pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
else do
authResult <- parseJwt
pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
app req' respond
authResultKey :: Vault.Key (Either Error AuthResult)
authResultKey = unsafePerformIO Vault.newKey
{-# NOINLINE authResultKey #-}
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
getResult = Vault.lookup authResultKey . Wai.vault
jwtDurKey :: Vault.Key Double
jwtDurKey = unsafePerformIO Vault.newKey
{-# NOINLINE jwtDurKey #-}
getJwtDur :: Wai.Request -> Maybe Double
getJwtDur = Vault.lookup jwtDurKey . Wai.vault
getRole :: Wai.Request -> Maybe BS.ByteString
getRole req = authRole <$> (rightToMaybe =<< getResult req)
-150
View File
@@ -1,150 +0,0 @@
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# LANGUAGE LambdaCase #-}
module PostgREST.Config.JSPath
( JSPath
, JSPathExp(..)
, FilterExp(..)
, dumpJSPath
, pRoleClaimKey
, walkJSPath
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Text.ParserCombinators.Parsec as P
import Data.Either.Combinators (mapLeft)
import Text.ParserCombinators.Parsec ((<?>))
import Text.Read (read)
import Protolude
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
type JSPath = [JSPathExp]
-- NOTE: We only accept one JSPFilter expr (at the end of input)
-- | jspath expression
data JSPathExp
= JSPKey Text -- .property or ."property-dash"
| JSPIdx Int -- [0]
| JSPSlice (Maybe Int) (Maybe Int) -- [0:5] or [0:] or [:5] or [:]
| JSPFilter FilterExp -- [?(@ == "match")]
data FilterExp
= EqualsCond Text
| NotEqualsCond Text
| StartsWithCond Text
| EndsWithCond Text
| ContainsCond Text
dumpJSPath :: JSPathExp -> Text
-- TODO: this needs to be quoted properly for special chars
dumpJSPath (JSPKey k) = "." <> show k
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
dumpJSPath (JSPSlice s e) = "[" <> maybe "" show s <> ":" <> maybe "" show e <> "]"
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
where
expr =
case cond of
EqualsCond text -> " == " <> show text
NotEqualsCond text -> " != " <> show text
StartsWithCond text -> " ^== " <> show text
EndsWithCond text -> " ==^ " <> show text
ContainsCond text -> " *== " <> show text
-- | Evaluate JSPath on a JSON
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath (Just (JSON.String str)) (JSPSlice start end:rest) =
let
len = T.length str
norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
norm = fmap (\i -> max 0 $ min len $ if i < 0 then len + i else i)
s = fromMaybe 0 $ norm start -- normalized start index
e = fromMaybe len $ norm end -- normalized end index
slicedString = if s >= e then T.empty else T.take (e-s) $ T.drop s str
in
walkJSPath (Just $ JSON.String slicedString) rest
walkJSPath (Just (JSON.Array ar)) (JSPFilter jspFilter:rest) = case jspFilter of
EqualsCond txt -> walkJSPath (findFirstMatch (==) txt ar) rest
NotEqualsCond txt -> walkJSPath (findFirstMatch (/=) txt ar) rest
StartsWithCond txt -> walkJSPath (findFirstMatch T.isPrefixOf txt ar) rest
EndsWithCond txt -> walkJSPath (findFirstMatch T.isSuffixOf txt ar) rest
ContainsCond txt -> walkJSPath (findFirstMatch T.isInfixOf txt ar) rest
where
findFirstMatch matchWith pattern = find (\case
JSON.String txt -> pattern `matchWith` txt
_ -> False)
walkJSPath _ _ = Nothing
-- Used for the config value "role-claim-key"
pRoleClaimKey :: Text -> Either Text JSPath
pRoleClaimKey selStr =
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
pJSPath :: P.Parser JSPath
pJSPath = P.many1 pJSPathExp <* P.eof
pJSPathExp :: P.Parser JSPathExp
pJSPathExp = P.try pJSPKey <|> P.try pJSPFilter <|> P.try pJSPIdx <|> pJSPSlice
pJSPKey :: P.Parser JSPathExp
pJSPKey = do
P.char '.'
val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue
return (JSPKey val) <?> "pJSPKey: JSPath attribute key"
pJSPIdx :: P.Parser JSPathExp
pJSPIdx = do
P.char '['
num <- read <$> P.many1 P.digit
P.char ']'
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
pJSPSlice :: P.Parser JSPathExp
pJSPSlice = do
P.char '['
startSign <- P.optionMaybe $ P.char '-'
startIndex <- P.optionMaybe (read <$> P.many1 P.digit)
P.char ':'
endSign <- P.optionMaybe $ P.char '-'
endIndex <- P.optionMaybe (read <$> P.many1 P.digit)
P.char ']'
let start' = if isJust startSign then ((-1) *) <$> startIndex else startIndex
end' = if isJust endSign then ((-1) *) <$> endIndex else endIndex
return (JSPSlice start' end') <?> "pJSPSlice: JSPath string slice"
pJSPFilter :: P.Parser JSPathExp
pJSPFilter = do
P.try $ P.string "[?("
condition <- pFilterConditionParser
P.char ')'
P.char ']'
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
pFilterConditionParser :: P.Parser FilterExp
pFilterConditionParser = do
P.char '@'
P.spaces
filt <- matchOperator
P.spaces
filt <$> pQuotedValue
where
matchOperator =
P.try (P.string "==^" $> EndsWithCond)
<|> P.try (P.string "==" $> EqualsCond)
<|> P.try (P.string "!=" $> NotEqualsCond)
<|> P.try (P.string "^==" $> StartsWithCond)
<|> P.try (P.string "*==" $> ContainsCond)
pQuotedValue :: P.Parser Text
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
-110
View File
@@ -1,110 +0,0 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.Listener (runListener) where
import qualified Data.ByteString.Char8 as BS
import qualified Hasql.Connection as SQL
import qualified Hasql.Notifications as SQL
import PostgREST.AppState (AppState, getConfig)
import PostgREST.Config (AppConfig (..))
import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config
import Control.Arrow ((&&&))
import Data.Bitraversable (bisequence)
import Data.Either.Combinators (whenRight)
import qualified Data.Text as T
import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Hasql.Session as SQL
import PostgREST.Config.Database (queryPgVersion)
import PostgREST.Config.PgVersion (pgvFullName)
import Protolude
-- | Starts the Listener in a thread
runListener :: AppState -> IO ()
runListener appState = do
AppConfig{..} <- getConfig appState
when configDbChannelEnabled $
void . forkIO . void $ retryingListen appState
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
-- | This function never returns (but can throw) and return type enforces that.
retryingListen :: AppState -> IO Void
retryingListen appState = do
cfg@AppConfig{..} <- AppState.getConfig appState
let
dbChannel = toS configDbChannel
onError err = do
AppState.putIsListenerOn appState False
observer $ DBListenFail dbChannel (Right err)
when (isDbListenerBug err) $
observer DBListenBugHint
unless configDbPoolAutomaticRecovery $
killThread mainThreadId
-- retry the listener
delay <- AppState.getNextListenerDelay appState
observer $ DBListenRetry delay
threadDelay (delay * oneSecondInMicro)
unless (delay == maxDelay) $
AppState.putNextListenerDelay appState (delay * 2)
-- loop running the listener
retryingListen appState
-- Execute the listener with with error handling
handle onError $ do
-- Make sure we don't leak connections on errors
bracket
-- acquire connection
(SQL.acquire $
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
-- release connection
(`whenRight` releaseConnection) $
-- use connection
\case
Right db -> do
SQL.listen db $ SQL.toPgIdentifier dbChannel
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
AppState.putIsListenerOn appState True
delay <- AppState.getNextListenerDelay appState
when (delay > 1) $ do -- if we did a retry
-- assume we lost notifications, refresh the schema cache
AppState.schemaCacheLoader appState
-- reset the delay
AppState.putNextListenerDelay appState 1
observer $ DBListenStart pqHost pqPort pgFullName dbChannel
-- wait for notifications
-- this will never return, in case of an error it will throw and be caught by onError
forever $ SQL.waitForNotifications handleNotification db
Left err -> do
observer $ DBListenFail dbChannel (Left err)
exitFailure
where
observer = AppState.getObserver appState
mainThreadId = AppState.getMainThreadId appState
oneSecondInMicro = 1000000
maxDelay = 32
handleNotification channel msg =
if | BS.null msg -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
| msg == "reload schema" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
| msg == "reload config" -> observer (DBListenerGotConfigMsg channel) >> AppState.readInDbConfig False appState
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
cacheReloader =
AppState.schemaCacheLoader appState
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
-79
View File
@@ -1,79 +0,0 @@
{-# LANGUAGE RecordWildCards #-}
{-|
Module : PostgREST.Logger
Description : Metrics based on the Observation module. See Observation.hs.
-}
module PostgREST.Metrics
( init
, MetricsState (..)
, observationMetrics
, metricsToText
) where
import qualified Data.ByteString.Lazy as LBS
import qualified Hasql.Pool.Observation as SQL
import Prometheus
import PostgREST.Observation
import Protolude
data MetricsState =
MetricsState {
poolTimeouts :: Counter,
poolAvailable :: Gauge,
poolWaiting :: Gauge,
poolMaxSize :: Gauge,
schemaCacheLoads :: Vector Label1 Counter,
schemaCacheQueryTime :: Gauge,
jwtCacheRequests :: Counter,
jwtCacheHits :: Counter,
jwtCacheEvictions :: Counter
}
init :: Int -> IO MetricsState
init configDbPoolSize = do
metricState <- MetricsState <$>
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
register (gauge (Info "pgrst_db_pool_available" "Available connections in the pool")) <*>
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
register (gauge (Info "pgrst_schema_cache_query_time_seconds" "The query time in seconds of the last schema cache load")) <*>
register (counter (Info "pgrst_jwt_cache_requests_total" "The total number of JWT cache lookups")) <*>
register (counter (Info "pgrst_jwt_cache_hits_total" "The total number of JWT cache hits")) <*>
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
pure metricState
-- Only some observations are used as metrics
observationMetrics :: MetricsState -> ObservationHandler
observationMetrics MetricsState{..} obs = case obs of
PoolAcqTimeoutObs -> do
incCounter poolTimeouts
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
SQL.ReadyForUseConnectionStatus _ -> do
incGauge poolAvailable
SQL.InUseConnectionStatus -> do
decGauge poolAvailable
SQL.TerminatedConnectionStatus _ -> do
decGauge poolAvailable
SQL.ConnectingConnectionStatus -> pure ()
PoolRequest ->
incGauge poolWaiting
PoolRequestFullfilled ->
decGauge poolWaiting
SchemaCacheLoadedObs resTime _ -> do
withLabel schemaCacheLoads "SUCCESS" incCounter
setGauge schemaCacheQueryTime resTime
SchemaCacheErrorObs{} -> do
withLabel schemaCacheLoads "FAIL" incCounter
JwtCacheLookup True -> incCounter jwtCacheRequests *> incCounter jwtCacheHits
JwtCacheLookup False -> incCounter jwtCacheRequests
JwtCacheEviction -> incCounter jwtCacheEvictions
_ ->
pure ()
metricsToText :: IO LBS.ByteString
metricsToText = exportMetricsAsText
-53
View File
@@ -1,53 +0,0 @@
module PostgREST.Plan.ReadPlan
( ReadPlanTree
, ReadPlan(..)
, JoinCondition(..)
, SpreadType(..)
) where
import Data.Tree (Tree (..))
import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
JoinType, NodeName)
import PostgREST.Plan.Types (CoercibleLogicTree,
CoercibleOrderTerm,
CoercibleSelectField (..),
RelSelectField (..),
SpreadType (..))
import PostgREST.RangeQuery (NonnegRange)
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier)
import PostgREST.SchemaCache.Relationship (Relationship)
import Protolude
type ReadPlanTree = Tree ReadPlan
data JoinCondition =
JoinCondition
(QualifiedIdentifier, FieldName)
(QualifiedIdentifier, FieldName)
deriving (Eq, Show)
-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable
data ReadPlan = ReadPlan
{ select :: [CoercibleSelectField]
, from :: QualifiedIdentifier
, fromAlias :: Maybe Alias
, where_ :: [CoercibleLogicTree]
, order :: [CoercibleOrderTerm]
, range_ :: NonnegRange
, relName :: NodeName
, relToParent :: Maybe Relationship
, relJoinConds :: [JoinCondition]
, relAlias :: Maybe Alias
, relAggAlias :: Alias
, relHint :: Maybe Hint
, relJoinType :: Maybe JoinType
, relSpread :: Maybe SpreadType
, relSelect :: [RelSelectField]
, depth :: Depth
-- ^ used for aliasing
}
deriving (Eq, Show)
@@ -7,11 +7,10 @@ import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import Control.Monad.Extra (whenJust)
import Network.Socket hiding (addrFamily)
import Network.Socket.ByteString
import Control.Monad.Extra (whenJust)
import PostgREST.AppState (AppState)
import PostgREST.AppState (AppState, getConfig, killApp)
import PostgREST.Config (AppConfig (..))
import PostgREST.MediaType (MediaType (..), toContentType)
import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveSocketToAddress)
@@ -22,30 +21,39 @@ import qualified PostgREST.AppState as AppState
import qualified Network.Socket as NS
import Protolude
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
runAdmin appState maybeAdminSocket socketREST settings = do
runAdmin :: AppState -> Maybe NS.Socket -> IO Bool -> Warp.Settings -> IO ()
runAdmin appState maybeAdminSocket checkMainAppLive settings = do
conf <- getConfig appState
whenJust maybeAdminSocket $ \adminSocket -> do
address <- resolveSocketToAddress adminSocket
observer $ AdminStartObs address
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
void . forkIO $ handle onError $
Warp.runSettingsSocket (adminServerSettings conf address) adminSocket adminApp
where
adminApp = admin appState socketREST
adminApp = admin appState checkMainAppLive
observer = AppState.getObserver appState
adminServerSettings config addr=
settings
& Warp.setBeforeMainLoop (observer $ AdminStartObs addr)
& maybe identity Warp.setPort (configAdminServerPort config)
onError ex = do
observer $ AdminServerCrashedObs ex
killApp appState -- Admin server crash is deemed unrecoverable, so we kill postgrest
-- | PostgREST admin application
admin :: AppState.AppState -> NS.Socket -> Wai.Application
admin appState socketREST req respond = do
isMainAppReachable <- isRight <$> reachMainApp socketREST
admin :: AppState.AppState -> IO Bool -> Wai.Application
admin appState checkMainAppLive req respond = do
isMainAppLive <- checkMainAppLive
isLoaded <- AppState.isLoaded appState
isPending <- AppState.isPending appState
case Wai.pathInfo req of
["live"] ->
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty
respond $ Wai.responseLBS (if isMainAppLive then HTTP.status200 else HTTP.status500) [] mempty
["ready"] ->
let
status | not isMainAppReachable = HTTP.status500
| isPending = HTTP.status503
status | isPending = HTTP.status503
| not isMainAppLive = HTTP.status500
| isLoaded = HTTP.status200
| otherwise = HTTP.status500
in
@@ -58,18 +66,3 @@ admin appState socketREST req respond = do
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
_ ->
respond $ Wai.responseLBS HTTP.status404 [] mempty
-- Try to connect to the main app socket
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
reachMainApp :: Socket -> IO (Either IOException ())
reachMainApp appSock = do
sockAddr <- getSocketName appSock
sock <- socket (addrFamily sockAddr) Stream defaultProtocol
try $ do
connect sock sockAddr
withSocketsDo $ bracket (pure sock) close sendEmpty
where
sendEmpty sock = void $ send sock mempty
addrFamily (SockAddrInet _ _) = AF_INET
addrFamily (SockAddrInet6 {}) = AF_INET6
addrFamily (SockAddrUnix _) = AF_UNIX
@@ -8,6 +8,7 @@ module PostgREST.ApiRequest
( ApiRequest(..)
, userApiRequest
, userPreferences
, userBearerAuth
) where
import qualified Data.CaseInsensitive as CI
@@ -16,32 +17,28 @@ import qualified Data.List.NonEmpty as NonEmptyList
import qualified Data.Set as S
import qualified Data.Text.Encoding as T
import Data.List (lookup)
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
rangeIsEmpty)
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies)
import Data.List (lookup)
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
rangeIsEmpty)
import Network.HTTP.Types.Header (RequestHeaders, hAuthorization, hCookie)
import Network.Wai (Request (..))
import Network.Wai.Middleware.HttpAuth (extractBearerAuth)
import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies)
import PostgREST.ApiRequest.Payload (getPayload)
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types (Action (..), DbAction (..),
InvokeMethod (..),
Mutation (..), Payload (..),
RequestBody, Resource (..))
import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.Config.Database (TimezoneNames)
import PostgREST.Error (ApiRequestError (..),
RangeError (..))
InvokeMethod (..), Mutation (..),
Payload (..), RequestBody,
Resource (..))
import PostgREST.Config (AppConfig (..), OpenAPIMode (..))
import PostgREST.Error (ApiRequestError (..), RangeError (..))
import PostgREST.MediaType (MediaType (..))
import PostgREST.RangeQuery (NonnegRange, allRange,
convertToLimitZeroRange,
hasLimitZero,
convertToLimitZeroRange, hasLimitZero,
rangeRequested)
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier (..),
Schema)
import qualified PostgREST.ApiRequest.Preferences as Preferences
@@ -111,8 +108,12 @@ userApiRequest conf prefs req reqBody = do
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
-- | Parses the Prefer header
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req
userPreferences :: AppConfig -> Request -> Preferences.Preferences
userPreferences conf req = Preferences.fromHeaders (configDbTxAllowOverride conf) $ requestHeaders req
-- | Obtains the Bearer Auth
userBearerAuth :: Request -> Maybe ByteString
userBearerAuth req = extractBearerAuth =<< lookup hAuthorization (requestHeaders req)
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
@@ -21,20 +21,21 @@ module PostgREST.ApiRequest.Preferences
, shouldCount
, shouldExplainCount
, prefAppliedHeader
, toHeaderValue
) where
import qualified Data.ByteString.Char8 as BS
import qualified Data.Map as Map
import qualified Data.Set as S
import qualified Network.HTTP.Types.Header as HTTP
import PostgREST.Config.Database (TimezoneNames)
import Protolude
-- $setup
-- Setup for doctests
-- >>> :set -XStandaloneDeriving
-- >>> import Text.Pretty.Simple (pPrint)
-- >>> import qualified Data.Set as S
-- >>> import Protolude
-- >>> deriving instance Show PreferResolution
-- >>> deriving instance Show PreferRepresentation
-- >>> deriving instance Show PreferCount
@@ -62,10 +63,8 @@ data Preferences
-- |
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
--
-- >>> let sc = S.fromList ["America/Los_Angeles"]
--
-- One header with comma-separated values can be used to set multiple preferences:
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
@@ -82,7 +81,7 @@ data Preferences
--
-- Multiple headers can also be used:
--
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid"), ("Prefer", "max-affected=5999")]
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid"), ("Prefer", "max-affected=5999")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
@@ -98,13 +97,13 @@ data Preferences
--
-- If a preference is set more than once, only the first is used:
--
-- >>> preferTransaction $ fromHeaders True sc [("Prefer", "tx=commit, tx=rollback")]
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
-- Just Commit
--
-- This is also the case across multiple headers:
--
-- >>> :{
-- preferResolution . fromHeaders True sc $
-- preferResolution . fromHeaders True $
-- [ ("Prefer", "resolution=ignore-duplicates")
-- , ("Prefer", "resolution=merge-duplicates")
-- ]
@@ -114,7 +113,7 @@ data Preferences
--
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
--
-- >>> pPrint $ fromHeaders True sc [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- Preferences
-- { preferResolution = Nothing
-- , preferRepresentation = Just Full
@@ -127,8 +126,8 @@ data Preferences
-- , invalidPrefs = [ "anything" ]
-- }
--
fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences
fromHeaders allowTxDbOverride acceptedTzNames headers =
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
fromHeaders allowTxDbOverride headers =
Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
@@ -136,7 +135,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient]
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
, preferTimezone = PreferTimezone <$> timezonePref
, preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
, invalidPrefs = filter isUnacceptable prefs
}
@@ -156,12 +155,11 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
timezonePref = listStripPrefix "timezone=" prefs
isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
isUnacceptable p = p `notElem` acceptedPrefs &&
(isNothing (BS.stripPrefix "timezone=" p) || not isTimezonePrefAccepted) &&
isNothing (BS.stripPrefix "timezone=" p) &&
isNothing (BS.stripPrefix "max-affected=" p)
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
@@ -4,12 +4,22 @@
--
-- This module is in charge of parsing all the querystring values in an url, e.g.
-- the select, id, order in `/projects?select=id,name&id=eq.1&order=id,name.desc`.
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
module PostgREST.ApiRequest.QueryParams
( parse
, QueryParams(..)
, pFieldForest
, pFieldName
, pFieldSelect
, pJsonPath
, pLogicTree
, pOpExpr
, pOrder
, pRelationSelect
, pRequestFilter
, pRequestRange
, pSingleVal
, pSpreadRelationSelect
) where
import qualified Data.ByteString.Char8 as BS
@@ -28,40 +38,38 @@ import Data.List (init, last)
import Data.Ranged.Boundaries (Boundary (..))
import Data.Ranged.Ranges (Range (..))
import Data.Tree (Tree (..))
import Text.Parsec.Error (errorMessages,
showErrorMessages)
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
anyChar, between, char, choice,
digit, eof, errorPos, letter,
lookAhead, many1, noneOf,
notFollowedBy, oneOf,
optionMaybe, sepBy, sepBy1,
string, try, (<?>))
import Text.Parsec.Error (errorMessages, showErrorMessages)
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser, anyChar,
between, char, choice, digit, eof,
errorPos, letter, lookAhead, many1,
noneOf, notFollowedBy, oneOf, optionMaybe,
sepBy, sepBy1, string, try, (<?>))
import PostgREST.RangeQuery (NonnegRange, allRange,
rangeGeq, rangeLimit,
rangeOffset, restrictRange)
import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
rangeLimit, rangeOffset,
restrictRange)
import PostgREST.SchemaCache.Identifiers (FieldName)
import PostgREST.ApiRequest.Types (AggregateFunction (..),
EmbedParam (..), EmbedPath, Field,
Filter (..), FtsOperator (..),
Hint, IsVal (..), JoinType (..),
JsonOperand (..),
JsonOperation (..), JsonPath,
ListVal, LogicOperator (..),
LogicTree (..), OpExpr (..),
OpQuantifier (..), Operation (..),
OrderDirection (..),
import PostgREST.ApiRequest.Types (AggregateFunction (..), EmbedParam (..),
EmbedPath, Field, Filter (..),
FtsOperator (..), Hint, IsVal (..),
JoinType (..), JsonOperand (..),
JsonOperation (..), JsonPath, ListVal,
LogicOperator (..), LogicTree (..),
OpExpr (..), OpQuantifier (..),
Operation (..), OrderDirection (..),
OrderNulls (..), OrderTerm (..),
QuantOperator (..),
SelectItem (..),
QuantOperator (..), SelectItem (..),
SimpleOperator (..), SingleVal)
import PostgREST.Error (QPError (..))
import Protolude hiding (Sum, try)
-- $setup
-- >>> import qualified Text.ParserCombinators.Parsec as P
-- >>> import Protolude hiding (Sum, try)
data QueryParams =
QueryParams
{ qsCanonical :: ByteString
@@ -42,8 +42,7 @@ module PostgREST.ApiRequest.Types
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Set as S
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier (..),
Schema)
import Protolude
+345
View File
@@ -0,0 +1,345 @@
{-|
Module : PostgREST.App
Description : PostgREST main application
This module is in charge of mapping HTTP requests to PostgreSQL queries.
Some of its functionality includes:
- Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION.
- Producing HTTP Headers according to RFCs.
- Content Negotiation
-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module PostgREST.App
( postgrest
, run
) where
import GHC.Conc (ThreadStatus (..), threadStatus)
import GHC.IO.Exception (IOErrorType (..))
import GHC.Weak
import System.IO.Error (ioeGetErrorType)
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft, whenLeft)
import Data.IORef (atomicWriteIORef, newIORef, readIORef)
import Data.String (IsString (..), String)
import Network.Wai.Handler.Warp (defaultSettings, setBeforeMainLoop, setHost,
setOnException, setPort, setServerName)
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai.Header as WaiHeader
import qualified PostgREST.Admin as Admin
import qualified PostgREST.ApiRequest as ApiRequest
import qualified PostgREST.AppState as AppState
import qualified PostgREST.Auth as Auth
import qualified PostgREST.Cors as Cors
import qualified PostgREST.Error as Error
import qualified PostgREST.MainTx as MainTx
import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query
import qualified PostgREST.Response as Response
import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState)
import PostgREST.AppState.Reload (runListener)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.Error (Error)
import PostgREST.Network (resolveSocketToAddress)
import PostgREST.Observation (Observation (..))
import PostgREST.Response.Performance (ServerTiming (..), serverTimingHeader)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (docsVersion, prettyVersion)
import Control.Monad.Writer
import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L
import Data.Streaming.Network (HostPreference, bindPortGenEx,
bindPortTCP)
import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.Header (hVary, hWarning)
import qualified Network.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket)
import System.Posix.Types (FileMode)
import Protolude hiding (Handler)
import System.Directory (doesPathExist)
run :: AppState -> Weak ThreadId -> IO ()
run appState mainThreadIdRef = do
conf@AppConfig{configServerReusePort} <- AppState.getConfig appState
mainSocketRef <- newIORef Nothing
let setMainSocketRef = atomicWriteIORef mainSocketRef . Just
clearMainSocketRef = atomicWriteIORef mainSocketRef Nothing
bracket (initAdminServerSocket conf) ensureSocketClosed $ \adminSocket -> do
let closeSockets = do
ensureSocketClosed adminSocket
ensureSocketClosed =<< readIORef mainSocketRef
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Admin.runAdmin appState adminSocket (checkMainAppLive (readIORef mainSocketRef) mainThreadIdRef) (serverSettings conf)
runListener appState
-- Kick off and wait for the initial SchemaCache load before creating the
-- main API socket.
AppState.schemaCacheLoader appState
if configServerReusePort then
AppState.waitForSchemaCacheLoaded appState
else
AppState.waitForSchemaCacheInit appState
bracket (initServerSocket conf) NS.close $ \mainSocket -> do
let app = postgrest appState (AppState.schemaCacheLoader appState)
address <- resolveSocketToAddress mainSocket
let
appServerSettings = serverSettings conf
& setPort (configServerPort conf)
& setOnException onWarpException
& setBeforeMainLoop (setMainSocketRef mainSocket *> observer (AppServerAddressObs address))
Warp.runSettingsSocket appServerSettings mainSocket app
`finally` clearMainSocketRef
where
observer = AppState.getObserver appState
ensureSocketClosed = foldMap NS.close
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
onWarpException _ ex =
when (shouldDisplayException ex) $
observer $ WarpServerObs $ show ex
-- Similar to wai defaultShouldDisplayException in
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
shouldDisplayException :: SomeException -> Bool
shouldDisplayException se
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
| otherwise = True
serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} =
defaultSettings
& setHost (fromString $ toS configServerHost)
& setServerName ("postgrest/" <> prettyVersion)
-- | PostgREST application
postgrest :: AppState.AppState -> IO () -> Wai.Application
postgrest appState connWorker =
traceHeaderMiddleware appState .
Cors.middleware appState $
\req respond -> do
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
maybeSchemaCache <- AppState.getSchemaCache appState
let handleError = fmap (either (Error.errorResponseFor configClientErrorVerbosity) identity)
-- writer to save authRole (uses `tell` for this and `getLast` to obtain it)
-- has to be before runExceptT to make sure role is not lost on error
(response, authRole) <- runWriterT . handleError . runExceptT $ do
(jwtTime, authResult@AuthResult{..}) <- withTiming appConf $
Auth.getAuthResult appState $ ApiRequest.userBearerAuth req
tell $ pure authRole
postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req
AppState.getObserver appState $ genResponseObs (getLast authRole) req response
-- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before
-- the connWorker is done. However, when there's an empty schema cache
-- postgrest responds with the error `PGRST002`; this means that the schema
-- cache is still loading, so we don't launch the connWorker here because
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
delay <- AppState.getNextDelay appState
respond $ addRetryHint delay response
where
-- TODO WaiHeader.contentLength does a lookup everytime, see: https://hackage.haskell.org/package/wai-extra-3.1.17/docs/src/Network.Wai.Header.html#contentLength
-- It might be possible to gain some perf by returning the response length from `postgrestResponse`. We calculate the length manually on Response.hs.
genResponseObs :: Maybe ByteString -> Wai.Request -> Wai.Response -> Observation
genResponseObs user req resp =
ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp)
postgrestResponse
:: (MonadError Error m, MonadIO m)
=> AppState.AppState
-> AppConfig
-> Maybe SchemaCache
-> Maybe Double
-> AuthResult
-> Wai.Request
-> m Wai.Response
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do
let observer = AppState.getObserver appState
sCache <-
case maybeSchemaCache of
Just sCache ->
return sCache
Nothing -> do
liftIO $ observer SchemaCacheEmptyObs
throwError Error.NoSchemaCacheError
let prefs = ApiRequest.userPreferences conf req
body <- liftIO $ Wai.strictRequestBody req
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let warnings = Plan.legacyWarnings plan
legacyWarnMsg = "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
legacyWarnHint = let replacement (relName, alias) = "`" <> relName <> "` to `" <> alias <> "`" in T.intercalate ", " (replacement <$> warnings)
shouldShowWarnings = configUrlUseLegacyTargetNames && not (null warnings)
liftIO $ when shouldShowWarnings $
observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency
pgVer <- liftIO $ AppState.getPgVersion appState
let mainQ = Query.mainQuery pgVer plan conf apiReq authResult configDbPreRequest
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
(txTime, txResult) <- withTiming conf $ do
case tx of
MainTx.NoDbTx r -> pure r
MainTx.DbTx dbSession -> do
dbRes <- liftIO $ AppState.usePool appState dbSession
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
-- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
-- This needs refactoring so only the below obsQuery is used.
liftIO $ whenLeft eitherResp $ obsQuery . Error.status
liftEither eitherResp
(respTime, resp) <- withTiming conf $ do
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
status' = either Error.status Response.pgrstStatus response
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
liftIO $ obsQuery status'
liftEither response
let warnHdrMsgs = if shouldShowWarnings then Just (legacyWarnMsg, legacyWarnHint) else Nothing
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) warnHdrMsgs resp
where
toWaiResponse :: ServerTiming -> Maybe (Text, Text) -> Response.PgrstResponse -> Wai.Response
toWaiResponse timing warnMsgs (Response.PgrstResponse st hdrs bod) =
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ warningHeaders warnMsgs ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
varyHeader :: HTTP.Header
varyHeader = (hVary, "Accept, Prefer, Range")
varyHeaderPresent :: [HTTP.Header] -> Bool
varyHeaderPresent = any (\(h, _v) -> h == hVary)
warningHeaders :: Maybe (Text, Text) -> [HTTP.Header]
warningHeaders Nothing = []
warningHeaders (Just (msg, hint)) =
let warnMsg = msg <> " Update " <> hint <> " in query string filters, orders or limits."
pgrstVer = "PostgRESTv" <> BS.filter (/= ' ') prettyVersion
in
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
then do
(t, r) <- timeItT f
pure (Just t, r)
else do
r <- f
pure (Nothing, r)
traceHeaderMiddleware :: AppState -> Wai.Middleware
traceHeaderMiddleware appState app req respond = do
conf <- AppState.getConfig appState
case configServerTraceHeader conf of
Nothing -> app req respond
Just hdr ->
let hdrVal = L.lookup hdr $ Wai.requestHeaders req in
app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))
addRetryHint :: Int -> Wai.Response -> Wai.Response
addRetryHint delay response = do
let h = ("Retry-After", BS.pack $ show delay)
Wai.mapResponseHeaders (\hs -> if isServiceUnavailable response then h:hs else hs) response
isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
initSocket :: (Applicative f, Traversable f) => Maybe String -> FileMode -> Text -> f Int -> (Int -> HostPreference -> IO NS.Socket) -> IO (f NS.Socket)
initSocket unixSocket unixSocketMode tcpHost tcpPort bindTCP =
maybe initTCPSocket initDomainSocket unixSocket
where
initTCPSocket = traverse (`bindTCP` (fromString $ T.unpack tcpHost)) tcpPort
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
initDomainSocket = fmap pure . (`createAndBindDomainSocket` unixSocketMode)
initServerSocket :: AppConfig -> IO NS.Socket
initServerSocket AppConfig{..} =
runIdentity <$> initSocket
configServerUnixSocket configServerUnixSocketMode
configServerHost (pure configServerPort)
(if configServerReusePort then bindPortTCPWithReusePort else bindPortTCP)
initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket)
initAdminServerSocket AppConfig{..} =
initSocket
configAdminServerUnixSocket configAdminServerUnixSocketMode
configAdminServerHost configAdminServerPort
bindPortTCP
bindPortTCPWithReusePort :: Int -> HostPreference -> IO NS.Socket
bindPortTCPWithReusePort port hostPreference =
bindPortGenEx [(NS.ReusePort, 1)] NS.Stream port hostPreference >>= listenSocket
where
listenSocket sock = NS.listen sock (max 2048 NS.maxListenQueue) $> sock
checkMainAppLive :: IO (Maybe NS.Socket) -> Weak ThreadId -> IO Bool
checkMainAppLive getMainSocket mainThreadIdRef =
handle (\(_ :: IOException) -> pure False) $
checkMainThread <&&> checkSocket
where
checkSocket = getMainSocket >>=
maybe (pure False)
(NS.getSocketName >=> \case
-- in case of unix socket, check if it still exists
NS.SockAddrUnix fp -> doesPathExist fp
_ -> pure True)
checkMainThread = deRefWeak mainThreadIdRef >>=
maybe (pure False)
(fmap isRunning . threadStatus)
isRunning = \case
ThreadRunning -> True
ThreadBlocked _ -> True
_ -> False
+112
View File
@@ -0,0 +1,112 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
module PostgREST.AppState
( AppState
, destroy
, getConfig
, getSchemaCache
, getPgVersion
, getNextDelay
, getTime
, getJwtCacheState
, init
, initWithPool
, killApp
, putConfig -- For tests TODO refactoring
, putSchemaCache
, putPgVersion
, putIsListenerOn
, usePool
, readInDbConfig
, schemaCacheLoader
, getObserver
, isLoaded
, isPending
, waitForSchemaCacheInit
, waitForSchemaCacheLoaded
) where
import qualified Hasql.Pool as SQL
import qualified Hasql.Session as SQL
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation
import PostgREST.Version (prettyVersion)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction)
import Control.Concurrent.STM (newEmptyTMVarIO)
import Data.IORef (IORef, newIORef, readIORef)
import Data.Time.Clock (getCurrentTime)
import PostgREST.AppState.Pool (destroy, initPool, usePool)
import PostgREST.AppState.Reload (isSchemaCacheLoaded, readInDbConfig,
retryingSchemaCacheLoad,
waitForSchemaCacheInit,
waitForSchemaCacheLoaded)
import PostgREST.AppState.Types
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (minimumPgVersion)
import PostgREST.Debounce (makeDebouncer)
import Protolude
init :: AppConfig -> IO () -> IO AppState
init conf@AppConfig{configDbPoolSize} appKiller = do
-- We need to create IORef first, so we can make its read action part of
-- loggerState. This is needed for log-level config reloading.
confRef <- newIORef conf
loggerState <- Logger.init (configLogLevel <$> readIORef confRef)
metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState) (Metrics.observationMetrics metricsState)
observer $ AppStartObs prettyVersion
pool <- initPool conf observer
initWithPool pool confRef loggerState metricsState observer appKiller
initWithPool :: SQL.Pool -> IORef AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO () -> IO AppState
initWithPool pool confRef loggerState metricsState observer appKiller = mdo
conf <- readIORef confRef
appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
<*> newIORef Nothing
<*> newSchemaCacheStatus
<*> newIORef False
<*> newIORef Nothing
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
<*> pure confRef
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> pure appKiller
<*> newIORef 0
<*> pure observer
<*> JwtCache.init conf observer
<*> pure loggerState
<*> pure metricsState
return appState
isConnEstablished :: AppState -> IO Bool
isConnEstablished appState = do
AppConfig{..} <- getConfig appState
if configDbChannelEnabled then -- if the listener is enabled, we can be sure the connection is up
readIORef $ stateIsListenerOn appState
else -- otherwise the only way to check the connection is to make a query
isRight <$> usePool appState (SQL.sql "SELECT 1")
isLoaded :: AppState -> IO Bool
isLoaded x = do
scacheLoaded <- isSchemaCacheLoaded x
connEstablished <- isConnEstablished x
return $ scacheLoaded && connEstablished
isPending :: AppState -> IO Bool
isPending x = do
scacheLoaded <- isSchemaCacheLoaded x
connEstablished <- isConnEstablished x
return $ not scacheLoaded || not connEstablished
newSchemaCacheStatus :: IO SchemaCacheStatus
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyTMVarIO
+110
View File
@@ -0,0 +1,110 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.AppState.Pool
( destroy
, initPool
, flushPool
, usePool
) where
import qualified Data.ByteString.Char8 as BS
import Data.Either.Combinators (whenLeft)
import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Config as SQL
import qualified Hasql.Session as SQL
import qualified Network.HTTP.Types.Status as HTTP
import qualified PostgREST.Error as Error
import PostgREST.Observation
import PostgREST.Config (AppConfig (..), toConnectionSettings)
import PostgREST.AppState.Types
import Protolude
-- | Destroy the pool on shutdown.
-- | Differs from flushPool in not emiting PoolFlushed observation.
destroy :: AppState -> IO ()
destroy AppState{..} = SQL.release statePool
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool cfg@AppConfig{..} observer = do
SQL.acquire $ SQL.settings
[ SQL.size configDbPoolSize
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg
, SQL.observationHandler $ observer . HasqlPoolObs
]
-- | Run an action with a database connection.
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
usePool appState@AppState{stateObserver=observer, ..} sess = do
observer PoolRequest
res <- SQL.use statePool sess
observer PoolRequestFullfilled
whenLeft res (\case
SQL.AcquisitionTimeoutUsageError ->
observer PoolAcqTimeoutObs
err@(SQL.ConnectionUsageError e) ->
let failureMessage = BS.unpack $ fromMaybe mempty e in
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
observer $ ExitDBFatalError ServerAuthError err
killApp appState
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
handleResultError err tpl resultErr
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
-- Passing the empty template will not work for schema cache queries, see TODO further below.
handleResultError err mempty resultErr
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
-- An error on the client-side, usually indicates problems with connection
observer $ QueryErrorCodeHighObs err
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
)
return res
where
handleResultError err tpl resultErr = do
case resultErr of
SQL.UnexpectedResult{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killApp appState
SQL.RowError{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killApp appState
SQL.UnexpectedAmountOfRows{} -> do
observer $ ExitDBFatalError ServerPgrstBug err
killApp appState
-- Check for a syntax error (42601 is the pg code) only for queries that don't have `WITH pgrst_source` as prefix.
-- This would mean the error is on our schema cache queries, so we treat it as fatal.
-- TODO have a better way to mark this as a schema cache query
SQL.ServerError "42601" _ _ _ _ ->
unless ("WITH pgrst_source" `BS.isPrefixOf` tpl) $ do
observer $ ExitDBFatalError ServerPgrstBug err
killApp appState
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
-- This would mean that a connection pooler in transaction mode is being used
-- while prepared statements are enabled in the PostgREST configuration,
-- both of which are incompatible with each other.
SQL.ServerError "42P05" _ _ _ _ -> do
observer $ ExitDBFatalError ServerError42P05 err
killApp appState
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _ -> do
observer $ ExitDBFatalError ServerError08P01 err
killApp appState
SQL.ServerError{} ->
when (Error.status (Error.PgError False err) >= HTTP.status500) $
observer $ QueryErrorCodeHighObs err
-- | Flush the connection pool so that any future use of the pool will
-- use connections freshly established after this call.
-- | Emits PoolFlushed observation
flushPool :: AppState -> IO ()
flushPool AppState{..} = do
SQL.release statePool
stateObserver PoolFlushed
+294
View File
@@ -0,0 +1,294 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.AppState.Reload
( isSchemaCacheLoaded
, readInDbConfig
, retryingSchemaCacheLoad
, runListener
, waitForSchemaCacheInit
, waitForSchemaCacheLoaded
) where
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Hasql.Connection as SQL
import qualified Hasql.Notifications as SQL
import qualified Hasql.Session as SQL
import qualified Hasql.Transaction.Sessions as SQL
import qualified PostgREST.Config as Config
import Control.Arrow ((&&&))
import Control.Concurrent.STM (putTMVar, readTMVar, tryReadTMVar, tryTakeTMVar)
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
exponentialBackoff, retrying, rsPreviousDelay)
import Data.Bitraversable (bisequence)
import Data.Either.Combinators (whenRight)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import PostgREST.AppState.Pool (flushPool, usePool)
import PostgREST.Auth.JwtCache (update)
import PostgREST.Config (AppConfig (..), readAppConfig)
import PostgREST.Config.Database (queryDbSettings, queryPgVersion,
queryRoleSettings)
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
import PostgREST.Observation (Observation (..))
import PostgREST.SchemaCache (SchemaCache (..), querySchemaCache,
showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi)
import PostgREST.TimeIt (timeItT)
import PostgREST.AppState.Types
import Protolude
-- | Try to load the schema cache and retry if it fails.
--
-- This is done by repeatedly: 1) flushing the pool, 2) querying the version and validating that the postgres version is supported by us, and 3) loading the schema cache.
-- It's necessary to flush the pool:
--
-- + Because connections cache the pg catalog(see #2620)
-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.
retryingSchemaCacheLoad :: AppState -> IO ()
retryingSchemaCacheLoad appState@AppState{stateObserver=observer} =
void $ retrying retryPolicy shouldRetry (\RetryStatus{rsIterNumber, rsPreviousDelay} -> do
when (rsIterNumber > 0) $ do
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
observer $ ConnectionRetryObs delay
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
)
where
qPgVersion :: IO (Maybe PgVersion)
qPgVersion = do
AppConfig{..} <- getConfig appState
pgVersion <- usePool appState queryPgVersion
case pgVersion of
Left e -> do
observer $ QueryPgVersionError e
unless configDbPoolAutomaticRecovery $ do
observer ExitDBNoRecoveryObs
killApp appState
return Nothing
Right actualPgVersion ->
if actualPgVersion < minimumPgVersion then do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killApp appState
return Nothing
else do
observer $ DBConnectedObs $ pgvFullName actualPgVersion
observer $ PoolInit configDbPoolSize
putPgVersion appState actualPgVersion
return $ Just actualPgVersion
qInDbConfig :: IO ()
qInDbConfig = do
AppConfig{..} <- getConfig appState
when configDbConfig $ readInDbConfig False appState
qSchemaCache :: IO (Maybe SchemaCache)
qSchemaCache = do
conf@AppConfig{..} <- getConfig appState
pgVer <- getPgVersion appState
(resultTime, result) <-
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache pgVer conf)
case result of
Left e -> do
markSchemaCachePending appState
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
return Nothing
Right (sCache, queryTimings) -> do
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
markSchemaCachePending appState
putSchemaCache appState $ Just sCache
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
-- Flush the pool after loading the schema cache to reset any stale session cache entries
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
-- and after marking sCacheStatus as pending,
flushPool appState
observer $ SchemaCacheQueriedObs resultTime queryTimings
observer $ SchemaCacheLoadedObs loadTime summary
markSchemaCacheLoaded appState
return $ Just sCache
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
shouldRetry _ (pgVer, sCache) = do
AppConfig{..} <- getConfig appState
let itShould = configDbPoolAutomaticRecovery && (isNothing pgVer || isNothing sCache)
return itShould
retryPolicy :: RetryPolicy
retryPolicy =
let delayMicroseconds = 32*oneSecondInUs {-32 seconds-} in
capDelay delayMicroseconds $ exponentialBackoff oneSecondInUs
oneSecondInUs = 1_000_000 -- one second in microseconds
markSchemaCachePending :: AppState -> IO ()
markSchemaCachePending = atomically . liftA2 (*>) tryTakeTMVar (`putTMVar` False) . getSCStatusTMVar . stateSCacheStatus
markSchemaCacheLoaded :: AppState -> IO ()
markSchemaCacheLoaded = atomically . liftA2 (*>) tryTakeTMVar (`putTMVar` True) . getSCStatusTMVar . stateSCacheStatus
isSchemaCacheLoaded :: AppState -> IO Bool
isSchemaCacheLoaded = atomically . (pure . fromMaybe False <=< tryReadTMVar) . getSCStatusTMVar . stateSCacheStatus
-- | Wait for initial schema cache load to either finish or retry
-- | We wait until scStatusTMVar is not empty.
waitForSchemaCacheInit :: AppState -> IO ()
waitForSchemaCacheInit = atomically . void . readTMVar . getSCStatusTMVar . stateSCacheStatus
waitForSchemaCacheLoaded :: AppState -> IO ()
waitForSchemaCacheLoaded = atomically . (check <=< readTMVar) . getSCStatusTMVar . stateSCacheStatus
-- | 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
oldConf <- getConfig appState
pgVer <- getPgVersion appState
dbSettings <-
if configDbConfig oldConf then do
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig oldConf))
case qDbSettings of
Left e -> do
observer $ ConfigReadErrorObs e
pure mempty
Right x -> pure x
else
pure mempty
(roleSettings, roleIsolationLvl) <-
if configDbConfig oldConf then do
rSettings <- usePool appState (queryRoleSettings pgVer)
case rSettings of
Left e -> do
observer $ QueryRoleSettingsErrorObs e
pure (mempty, mempty)
Right x -> pure x
else
pure mempty
readAppConfig dbSettings (configFilePath oldConf) (Just $ configDbUri oldConf) roleSettings roleIsolationLvl >>= \case
Left err ->
if startingUp then
panic err -- die on invalid config if the program is starting up
else
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
update (getJwtCacheState appState) newConf
-- If db-channel-enabled is changed, then reload listener
when (((/=) `on` configDbChannelEnabled) newConf oldConf) $ do
-- 1. Kill the listener thread
getListenerThreadId appState >>= mapM_ (`throwTo` ListenerRestart)
putIsListenerOn appState False
-- 2. Restart listener
runListener appState
if startingUp then
pass
else
observer ConfigSucceededObs
-- | Starts the Listener in a thread
runListener :: AppState -> IO ()
runListener appState = do
AppConfig{..} <- getConfig appState
when configDbChannelEnabled $ do
nextDelay <- newIORef 1
listenerThreadId <- forkIO . void $ retryingListen appState nextDelay False
putListenerThreadId appState (Just listenerThreadId)
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
-- | This function never returns (but can throw) and return type enforces that.
retryingListen :: AppState -> IORef Int -> Bool -> IO ()
retryingListen appState nextDelay hasDbListenerBug = do
cfg@AppConfig{..} <- getConfig appState
let
dbChannel = toS configDbChannel
onError err = case fromException err of
Just ListenerRestart -> traverse_ killThread =<< getListenerThreadId appState
Nothing -> do -- for any other exception
putIsListenerOn appState False
observer $ DBListenFail dbChannel (Right err)
when (isDbListenerBug err) $
observer DBListenBugCallQueryFix
unless configDbPoolAutomaticRecovery $
killApp appState
-- retry the listener
delay <- readIORef nextDelay
observer $ DBListenRetry delay
threadDelay (delay * oneSecondInMicro)
unless (delay == maxDelay) $
writeIORef nextDelay (delay * 2)
-- loop running the listener
retryingListen appState nextDelay (isDbListenerBug err)
-- Execute the listener with error handling
handle onError $ do
-- Make sure we don't leak connections on errors
bracket
-- acquire connection
(SQL.acquire $
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
-- release connection
(`whenRight` releaseConnection) $
-- use connection
\case
Right db -> do
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
SQL.listen db $ SQL.toPgIdentifier dbChannel
putIsListenerOn appState True
delay <- readIORef nextDelay
when (delay > 1) $ do -- if we did a retry
-- assume we lost notifications, refresh the schema cache
schemaCacheLoader appState
-- reset the delay
writeIORef nextDelay 1
observer $ DBListenStart pqHost pqPort pgFullName dbChannel
-- wait for notifications
-- this will never return, in case of an error it will throw and be caught by onError
forever $ SQL.waitForNotifications handleNotification db
Left err -> do
observer $ DBListenFail dbChannel (Left err)
exitFailure
where
observer = getObserver appState
oneSecondInMicro = 1_000_000
maxDelay = 32
handleNotification channel msg =
if | BS.null msg -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
| msg == "reload schema" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
| msg == "reload config" -> observer (DBListenerGotConfigMsg channel) >> readInDbConfig False appState
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
cacheReloader =
schemaCacheLoader appState
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
-- Used to fix a Postgres bug in the listener, see: https://github.com/PostgREST/postgrest/issues/3147#issuecomment-3494591361
-- This query advances the async notification query tail, which solves this issue.
callNotifQueryUsage :: SQL.Session ()
callNotifQueryUsage = SQL.sql "SELECT pg_notification_queue_usage();"
+113
View File
@@ -0,0 +1,113 @@
{-|
Module : PostgREST.AppState.Types
Description : AppState data type and stateful functions
-}
{-# LANGUAGE DeriveAnyClass #-}
module PostgREST.AppState.Types where
import qualified Hasql.Pool as SQL
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation
import Data.IORef (IORef, atomicWriteIORef, readIORef)
import Data.Time.Clock (UTCTime)
import Control.Concurrent.STM (TMVar)
import PostgREST.Auth.JwtCache (JwtCacheState)
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.SchemaCache (SchemaCache (..))
import Protolude
data AppState = AppState
-- | Database connection pool
{ statePool :: SQL.Pool
-- | Database server version
, statePgVersion :: IORef PgVersion
-- | Schema cache
, stateSchemaCache :: IORef (Maybe SchemaCache)
-- | The schema cache status
, stateSCacheStatus :: SchemaCacheStatus
-- | State of the LISTEN channel
, stateIsListenerOn :: IORef Bool
-- | Listener Thread ID
, stateListenerThreadId :: IORef (Maybe ThreadId)
-- | starts the connection worker with a debounce
, debouncedSCacheLoader :: IO ()
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
, stateGetTime :: IO UTCTime
-- | Used for killing the main thread in case a subthread fails
, stateKillApp :: IO ()
-- | Keeps track of the next delay for db connection retry
, stateNextDelay :: IORef Int
-- | Observation handler
, stateObserver :: ObservationHandler
-- | JWT Cache
, stateJwtCache :: JwtCache.JwtCacheState
, stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState
}
-- | Schema cache status.
-- Empty means initial loading on startup, False means pending and True means loaded.
-- "Initial" state is needed so that we can wait with application socket listening
-- until after initial schema cache querying.
newtype SchemaCacheStatus = SchemaCacheStatus
{ getSCStatusTMVar :: TMVar Bool
}
-- |
-- We define a custom exception and throw this on listener reload. The
-- KillThread exception can occur in an unexpected scenario, so we should
-- avoid using that.
data ListenerException = ListenerRestart deriving (Show, Exception)
getPgVersion :: AppState -> IO PgVersion
getPgVersion = readIORef . statePgVersion
putPgVersion :: AppState -> PgVersion -> IO ()
putPgVersion = atomicWriteIORef . statePgVersion
getSchemaCache :: AppState -> IO (Maybe SchemaCache)
getSchemaCache = readIORef . stateSchemaCache
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
schemaCacheLoader :: AppState -> IO ()
schemaCacheLoader = debouncedSCacheLoader
getNextDelay :: AppState -> IO Int
getNextDelay = readIORef . stateNextDelay
getConfig :: AppState -> IO AppConfig
getConfig = readIORef . stateConf
putConfig :: AppState -> AppConfig -> IO ()
putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache
killApp :: AppState -> IO ()
killApp = stateKillApp
putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
getListenerThreadId :: AppState -> IO (Maybe ThreadId)
getListenerThreadId = readIORef . stateListenerThreadId
putListenerThreadId :: AppState -> Maybe ThreadId -> IO ()
putListenerThreadId = atomicWriteIORef . stateListenerThreadId
getObserver :: AppState -> ObservationHandler
getObserver = stateObserver
+32
View File
@@ -0,0 +1,32 @@
{-|
Module : PostgREST.Auth
Description : PostgREST authentication functions.
This module provides functions to deal with the JWT authentication (http://jwt.io).
It also can be used to define other authentication functions,
in the future Oauth, LDAP and similar integrations can be coded here.
Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database.
-}
module PostgREST.Auth
( getAuthResult )
where
import PostgREST.AppState (AppState, getConfig, getJwtCacheState, getTime)
import PostgREST.Auth.Jwt (parseClaims)
import PostgREST.Auth.JwtCache (lookupJwtCache)
import PostgREST.Auth.Types (AuthResult)
import PostgREST.Error (Error)
import Protolude
-- | Perform authentication and authorization
-- Parse JWT and return AuthResult
getAuthResult :: (MonadError Error m, MonadIO m) => AppState -> Maybe ByteString -> m AuthResult
getAuthResult appState token = do
conf <- liftIO $ getConfig appState
time <- liftIO $ getTime appState
parseClaims conf time =<< lookupJwtCache (getJwtCacheState appState) token
@@ -4,8 +4,6 @@ Description : PostgREST JWT support functions.
This module provides functions to deal with JWT parsing and validation (http://jwt.io).
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
@@ -31,7 +29,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), audMatchesCfg)
import PostgREST.Config.JSPath (walkJSPath)
import PostgREST.Config.JSPath (evaluateJSPath)
import PostgREST.Error (Error (..), JwtClaimsError (..),
JwtDecodeError (..), JwtError (..))
@@ -114,7 +112,7 @@ parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims
validateClaims time (audMatchesCfg cfg) mclaims
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
unquoted <$> evaluateJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
pure AuthResult
{ authClaims = mclaims
, authRole = role
@@ -4,12 +4,9 @@ Description : PostgREST JWT validation results Cache.
This module provides functions to deal with the JWT cache.
-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
module PostgREST.Auth.JwtCache
( init
@@ -23,13 +20,12 @@ import qualified Data.Aeson.KeyMap as KM
import PostgREST.Error (Error (..), JwtError (JwtSecretMissing))
import Control.Concurrent.STM (newTVarIO, readTVar,
writeTVar)
import Control.Concurrent.STM (newTVarIO, readTVar, writeTVar)
import Control.Concurrent.STM.TVar (TVar)
import Control.Monad.Error.Class (liftEither)
import Data.ByteString hiding (all, init)
import Data.IORef (IORef, newIORef,
readIORef, writeIORef)
import Data.IORef (IORef, newIORef, readIORef,
writeIORef)
import Jose.Jwk (JwkSet)
import PostgREST.Auth.Jwt (parseAndDecodeClaims)
import PostgREST.Cache.Sieve (alwaysValid)
@@ -42,7 +38,7 @@ import Protolude
data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
class CacheVariant m v where
cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object
cached :: (MonadError Error n, MonadIO n) => SC.Cache m ByteString v -> ByteString -> n JSON.Object
{-|
Jwt caching can have three different configurations:
@@ -60,12 +56,12 @@ data JwtCache =
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
instance CacheVariant IO (Either Error JSON.Object) where
cached c = lift . SC.cached c >=> liftEither
cached c = liftIO . SC.cached c >=> liftEither
instance CacheVariant (ExceptT Error IO) JSON.Object where
cached = SC.cached
cached c = liftIO . runExceptT . SC.cached c >=> liftEither
decode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object
decode :: (MonadError Error m, MonadIO m) => JwtCache -> ByteString -> m JSON.Object
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
decode (JwtNoCache key) = parseAndDecodeClaims key
decode (JwtCache _ _ c) = cached c
@@ -110,5 +106,5 @@ newJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler =
(const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
alwaysValid) -- no invalidation for now
lookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object
lookupJwtCache :: (MonadError Error m, MonadIO m) => JwtCacheState -> Maybe ByteString -> m JSON.Object
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode
@@ -42,11 +42,13 @@ runClientCommand conf CmdReady = Client.ready conf
-- | Run postgrest with command
runAppCommand :: AppConfig -> RunCommand -> IO ()
runAppCommand conf@AppConfig{..} runCmd = do
mainThreadId <- myThreadId
mainThreadIdRef <- mkWeakThreadId mainThreadId
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
-- explicitly close the connections to PostgreSQL on shutdown.
-- 'AppState.destroy' takes care of that.
bracket
(AppState.init conf)
(AppState.init conf (killThread mainThreadId))
AppState.destroy
(\appState -> case runCmd of
CmdDumpConfig -> do
@@ -55,20 +57,21 @@ runAppCommand conf@AppConfig{..} runCmd = do
CmdDumpSchema -> do
when configDbConfig $ AppState.readInDbConfig True appState
putStrLn =<< dumpSchema appState
CmdRun -> App.run appState)
CmdRun -> App.run appState mainThreadIdRef)
-- | Dump SchemaCache schema to JSON
dumpSchema :: AppState -> IO LBS.ByteString
dumpSchema appState = do
conf@AppConfig{..} <- AppState.getConfig appState
pgVer <- AppState.getPgVersion appState
result <-
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache pgVer conf)
case result of
Left e -> do
let observer = AppState.getObserver appState
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
exitFailure
Right sCache -> return $ JSON.encode sCache
Right (sCache, _) -> return $ JSON.encode sCache
-- | Command line interface options
data CLI = CLI
@@ -8,11 +8,9 @@ This module provides implementation of a mutable cache on Sieve algorithm.
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TupleSections #-}
module PostgREST.Cache.Sieve (
Cache
@@ -3,20 +3,16 @@ Module : PostgREST.Config
Description : Manages PostgREST configuration type and parser.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Config
( AppConfig (..)
, Environment
, JSPath
, JSPathExp(..)
, FilterExp(..)
, defaultRoleJSPathKey
, LogLevel(..)
, OpenAPIMode(..)
, Proxy(..)
@@ -61,15 +57,12 @@ import Numeric (readOct, showOct)
import System.Environment (getEnvironment)
import System.Posix.Types (FileMode)
import PostgREST.Config.Database (RoleIsolationLvl,
RoleSettings)
import PostgREST.Config.JSPath (FilterExp (..), JSPath,
JSPathExp (..), dumpJSPath,
pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
toQi)
import PostgREST.Config.Database (RoleIsolationLvl, RoleSettings)
import PostgREST.Config.JSPath (JSPath (..), defaultRoleJSPathKey,
dumpJSPath, pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..), isMalformedProxyUri,
toURI)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), toQi)
import PostgREST.Version (prettyVersion)
import Protolude hiding (Proxy, toList)
@@ -78,57 +71,59 @@ audMatchesCfg :: AppConfig -> Text -> Bool
audMatchesCfg = maybe (const True) (==) . configJwtAudience
data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)]
, configClientErrorVerbosity :: Verbosity
, configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
, configDbExtraSearchPath :: [Text]
, configDbHoistedTxSettings :: [Text]
, configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool
, configDbPoolSize :: Int
, configDbPoolAcquisitionTimeout :: Int
, configDbPoolMaxLifetime :: Int
, configDbPoolMaxIdletime :: Int
, configDbPoolAutomaticRecovery :: Bool
, configDbPreRequest :: Maybe QualifiedIdentifier
, configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe QualifiedIdentifier
, configDbSchemas :: NonEmpty Text
, configDbConfig :: Bool
, configDbPreConfig :: Maybe QualifiedIdentifier
, configDbTimezoneEnabled :: Bool
, configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool
, configDbUri :: Text
, configFilePath :: Maybe FilePath
, configJWKS :: Maybe JwkSet
, configJwtAudience :: Maybe Text
, configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxEntries :: Int
, configLogLevel :: LogLevel
, configLogQuery :: Bool
, configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configServerCorsAllowedOrigins :: Maybe [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCQuerySleep :: Maybe Int32
, configInternalSCLoadSleep :: Maybe Int32
, configInternalSCRelLoadSleep :: Maybe Int32
{ configAppSettings :: [(Text, Text)]
, configClientErrorVerbosity :: Verbosity
, configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
, configDbExtraSearchPath :: [Text]
, configDbHoistedTxSettings :: [Text]
, configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool
, configDbPoolSize :: Int
, configDbPoolAcquisitionTimeout :: Int
, configDbPoolMaxLifetime :: Int
, configDbPoolMaxIdletime :: Int
, configDbPoolAutomaticRecovery :: Bool
, configDbPreRequest :: Maybe QualifiedIdentifier
, configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe QualifiedIdentifier
, configDbSchemas :: NonEmpty Text
, configDbConfig :: Bool
, configDbPreConfig :: Maybe QualifiedIdentifier
, configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool
, configDbUri :: Text
, configFilePath :: Maybe FilePath
, configJWKS :: Maybe JwkSet
, configJwtAudience :: Maybe Text
, configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxEntries :: Int
, configLogLevel :: LogLevel
, configLogQuery :: Bool
, configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configServerCorsAllowedOrigins :: [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerReusePort :: Bool
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configUrlUseLegacyTargetNames :: Bool
, configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int
, configAdminServerUnixSocket :: Maybe FilePath
, configAdminServerUnixSocketMode :: FileMode
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCQuerySleepFst :: Maybe Int32
, configInternalSCQuerySleepSnd :: Maybe Int32
}
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
@@ -163,7 +158,7 @@ dumpOpenApiMode = \case
-- | Dump the config
toText :: AppConfig -> Text
toText conf =
unlines $ (\(k, v) -> k <> " = " <> v) <$> pgrstSettings ++ appSettings
unlines $ sort $ (\(k, v) -> k <> " = " <> v) <$> pgrstSettings ++ appSettings
where
-- apply conf to all pgrst settings
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
@@ -187,11 +182,10 @@ toText conf =
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
,("db-config", T.toLower . show . configDbConfig)
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
,("db-timezone-enabled", T.toLower . show . configDbTimezoneEnabled)
,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri)
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
,("jwt-role-claim-key", q . dumpJSPath . configJwtRoleClaimKey)
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("jwt-cache-max-entries", show . configJwtCacheMaxEntries)
@@ -200,15 +194,19 @@ toText conf =
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("server-cors-allowed-origins", q . maybe "" (T.intercalate ",") . configServerCorsAllowedOrigins)
,("server-cors-allowed-origins", q . T.intercalate "," . configServerCorsAllowedOrigins)
,("server-host", q . configServerHost)
,("server-port", show . configServerPort)
,("server-reuseport", T.toLower . show . configServerReusePort)
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("url-use-legacy-target-names", T.toLower . show . configUrlUseLegacyTargetNames)
,("admin-server-host", q . configAdminServerHost)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket)
,("admin-server-unix-socket-mode", q . T.pack . showAdminSocketMode)
]
-- quote all app.settings
@@ -232,6 +230,7 @@ toText conf =
where
secret = fromMaybe mempty $ configJwtSecret c
showSocketMode c = showOct (configServerUnixSocketMode c) mempty
showAdminSocketMode c = showOct (configAdminServerUnixSocketMode c) mempty
-- This class is needed for the polymorphism of overrideFromDbOrEnvironment
-- because C.required and C.optional have different signatures
@@ -297,7 +296,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> parseDbSchemas "db-schemas" "db-schema"
<*> (fromMaybe True <$> optBool "db-config")
<*> (fmap toQi <$> optString "db-pre-config")
<*> (fromMaybe True <$> optBool "db-timezone-enabled")
<*> parseTxEnd "db-tx-end" snd
<*> parseTxEnd "db-tx-end" fst
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
@@ -318,18 +316,21 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
<*> (defaultServerHost <$> optString "server-host")
<*> parseServerPort "server-port"
<*> (fromMaybe False <$> optBool "server-reuseport")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
<*> (fromMaybe False <$> optBool "server-timing-enabled")
<*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> (fromMaybe True <$> optBool "url-use-legacy-target-names")
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
(optString "server-host"))
<*> parseAdminServerPort "admin-server-port"
<*> (fmap T.unpack <$> optString "admin-server-unix-socket")
<*> parseSocketFileMode "admin-server-unix-socket-mode"
<*> pure roleSettings
<*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-query-sleep-before-queries"
<*> optInt "internal-schema-cache-query-sleep"
<*> optInt "internal-schema-cache-load-sleep"
<*> optInt "internal-schema-cache-relationship-load-sleep"
where
parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity
parseErrorVerbosity k =
@@ -376,10 +377,10 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Just fileModeText ->
case readOct $ T.unpack fileModeText of
[] ->
fail "Invalid server-unix-socket-mode: not an octal"
fail $ "Invalid " <> T.unpack k <> ": not an octal"
(fileMode, _):_ ->
if fileMode < 384 || fileMode > 511
then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
then fail $ "Invalid " <> T.unpack k <> ": needs to be between 600 and 777"
else pure fileMode
parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode
@@ -423,13 +424,13 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath
parseRoleClaimKey k al =
optWithAlias (optString k) (optString al) >>= \case
Nothing -> pure [JSPKey "role"]
Nothing -> pure defaultRoleJSPathKey -- $.role
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
parseCORSAllowedOrigins k =
optString k >>= \case
Nothing -> pure Nothing
Just orig -> pure $ Just (T.strip <$> T.splitOn "," orig)
Nothing -> pure []
Just orig -> pure (T.strip <$> T.splitOn "," orig)
optWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a)
optWithAlias orig alias =
@@ -590,6 +591,7 @@ pgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.i
-- | Adds a `fallback_application_name` value to the connection string. This allows querying the PostgREST version on pg_stat_activity.
--
-- >>> import Protolude
-- >>> let ver = "11.1.0 (5a04ec7)"::ByteString
-- >>> let strangeVer = "11'1&0@#$%,.:\"[]{}?+^()=asdfqwer"::ByteString
--
@@ -729,9 +731,6 @@ exampleConfigFile = S.unlines
, "## The name of which database schema to expose to REST clients"
, "db-schemas = \"public\""
, ""
, "## Enable quering pg_timezone_names from db"
, "# db-timezone-enabled = true"
, ""
, "## How to terminate database transactions"
, "## Possible values are:"
, "## commit (default)"
@@ -779,6 +778,7 @@ exampleConfigFile = S.unlines
, ""
, "server-host = \"!4\""
, "server-port = 3000"
, "server-reuseport = false"
, ""
, "## Allow getting the request-response timing information through the `Server-Timing` header"
, "server-timing-enabled = false"
@@ -790,4 +790,9 @@ exampleConfigFile = S.unlines
, "## Unix socket file mode"
, "## When none is provided, 660 is applied by default"
, "# server-unix-socket-mode = \"660\""
, ""
, "## Use legacy target names in relationship filters"
, "## If active, allows using the target name of the relationship in filters even if it has an alias."
, "## Otherwise it only allows the alias in filters"
, "url-use-legacy-target-names = true"
]
@@ -7,7 +7,6 @@ module PostgREST.Config.Database
, queryRoleSettings
, RoleSettings
, RoleIsolationLvl
, TimezoneNames
, toIsolationLevel
) where
@@ -16,6 +15,7 @@ import Control.Arrow ((***))
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
@@ -30,10 +30,9 @@ import Protolude
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
toIsolationLevel a = case a of
toIsolationLevel :: Text -> SQL.IsolationLevel
toIsolationLevel a = case T.toLower a of
"repeatable read" -> SQL.RepeatableRead
"serializable" -> SQL.Serializable
_ -> SQL.ReadCommitted
@@ -56,7 +55,6 @@ dbSettingsNames =
,"db_prepared_statements"
,"db_root_spec"
,"db_schemas"
,"db_timezone_enabled"
,"db_tx_end"
,"db_hoisted_tx_settings"
,"jwt_aud"
@@ -70,6 +68,7 @@ dbSettingsNames =
,"server_cors_allowed_origins"
,"server_trace_header"
,"server_timing_enabled"
,"url_use_legacy_target_names"
]
queryPgVersion :: Session PgVersion
@@ -148,7 +147,7 @@ queryRoleSettings pgVer =
SELECT
rolname,
substr(setting, 1, strpos(setting, '=') - 1) as key,
lower(substr(setting, strpos(setting, '=') + 1)) as value
substr(setting, strpos(setting, '=') + 1) as value
FROM role_setting
),
iso_setting AS (
+60
View File
@@ -0,0 +1,60 @@
{-|
Module : PostgREST.Config.JSPath
Description : Parsing and evaluation logic of JSPath
-}
module PostgREST.Config.JSPath
( JSPath(..)
, defaultRoleJSPathKey
, dumpJSPath
, pRoleClaimKey
, evaluateJSPath
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.JSONPath as JSP
import qualified Data.Aeson.JSONPath.Parser as JSP
import qualified Data.Aeson.JSONPath.Types as JSP
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Text.ParserCombinators.Parsec as P
import Data.Either.Combinators (mapLeft)
import Data.Either.Extra (fromRight')
import Text.ParserCombinators.Parsec ((<?>))
import Protolude
-- | full jspath, e.g. "$.property[0].attr.detail[?(@ == "role1")]"
newtype JSPath = JSPath JSP.Query
-- | Default value for "jwt-role-claim-key" config
defaultRoleJSPathKey :: JSPath
defaultRoleJSPathKey = fromRight' $ P.parse pJSPath "" "$.role"
-- | Dump JSPath
-- e.g. "$.property[0].attr.detail[?(@ == "role1")]"
dumpJSPath :: JSPath -> Text
dumpJSPath (JSPath query) = (escapeDollarChar . escapeDoubleQuotes) jsPathDump
where
jsPathDump = JSP.dumpQuery query
escapeDoubleQuotes = T.replace "\"" "\\\""
-- When dumping, $ must be escaped
escapeDollarChar = T.replace "$" "$$"
-- |
-- Evaluate JSPath on a JSON
-- The result of JSON Path query is a Vector, we select the first
-- string element as the role.
evaluateJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
evaluateJSPath Nothing _ = Nothing
evaluateJSPath (Just json) (JSPath query) = JSP.queryQQ query json V.!? 0
-- Used for the config value "role-claim-key"
pRoleClaimKey :: Text -> Either Text JSPath
pRoleClaimKey selStr =
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
-- | Parse RFC 9535 JSPath: $.roles[0]
pJSPath :: P.Parser JSPath
pJSPath = JSPath <$> JSP.pQuery <?> "pJSPath: JSPath root query"
@@ -1,11 +1,11 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
module PostgREST.Config.PgVersion
( PgVersion(..)
, minimumPgVersion
, pgVersion150
, pgVersion170
, pgVersion180
, pgVersion190
) where
import qualified Data.Aeson as JSON
@@ -38,3 +38,6 @@ pgVersion170 = PgVersion 170000 "17.0" "17.0"
pgVersion180 :: PgVersion
pgVersion180 = PgVersion 180000 "18.0" "18.0"
pgVersion190 :: PgVersion
pgVersion190 = PgVersion 190000 "19.0" "19.0"
@@ -2,9 +2,6 @@
Module : PostgREST.Cors
Description : Wai Middleware to set cors policy.
-}
{-# LANGUAGE TupleSections #-}
module PostgREST.Cors (middleware) where
import qualified Data.ByteString.Char8 as BS
@@ -26,11 +23,13 @@ middleware appState app req res = do
Wai.cors (corsPolicy $ configServerCorsAllowedOrigins conf) app req res
-- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy :: [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
Just _ ->
Just Wai.CorsResourcePolicy
{ Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins
{ Wai.corsOrigins = case corsAllowedOrigins of
[] -> Nothing
origins -> Just (map T.encodeUtf8 origins, True)
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
, Wai.corsRequestHeaders = "Authorization" : accHeaders
, Wai.corsExposedHeaders = Just
@@ -19,6 +19,8 @@ module PostgREST.Error
, JwtClaimsError(..)
, errorPayload
, status
, noRelBetweenHint
, noRpcHint
) where
import qualified Data.Aeson as JSON
@@ -44,19 +46,21 @@ import qualified PostgREST.MediaType as MediaType
import PostgREST.Config (Verbosity (..))
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
Schema)
import PostgREST.SchemaCache.Relationship (Cardinality (..),
Junction (..),
Relationship (..),
RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..),
RoutineParam (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), Schema)
import PostgREST.SchemaCache.Relationship (Cardinality (..), Junction (..),
Relationship (..), RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..), RoutineParam (..))
import PostgREST.Error.Types
import Protolude
-- $setup
-- >>> import qualified Data.HashMap.Strict as HM
-- >>> import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
-- >>> import PostgREST.SchemaCache.Relationship (Relationship (..))
-- >>> import PostgREST.SchemaCache.Routine (Routine (..), RoutineParam (..))
-- | Encode Error to ByteString
errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString
errorPayload verb = JSON.encode . toJsonPgrstError verb
@@ -175,7 +179,7 @@ instance ErrorBody ApiRequestError where
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
message (NotEmbedded resource _) = "'" <> resource <> "' is not an embedded resource in this request"
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
message GucStatusError = "response.status guc must be a valid status code"
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
@@ -207,11 +211,13 @@ instance ErrorBody ApiRequestError where
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
details (NotImplemented details') = Just $ JSON.String details'
details (NotEmbedded _ (Just _)) = Just $ JSON.String "Target names are not allowed in filters if they have an alias"
details _ = Nothing
-- HINT: Maybe JSON.Value
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
hint (NotEmbedded resource Nothing) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
hint (NotEmbedded _ (Just (name, alias))) = Just $ JSON.String $ "Change '" <> name <> "' to '" <> alias <> "' in filters, orders or limits."
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
@@ -475,7 +481,7 @@ instance ErrorBody SQL.UsageError where
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
message (SQL.ConnectionUsageError _) = "Database connection error."
message (SQL.SessionUsageError (SQL.PipelineError e)) = message e
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
@@ -22,8 +22,7 @@ import qualified Hasql.Pool as SQL
import PostgREST.MediaType (MediaType (..))
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Relationship (..),
RelationshipsMap)
import PostgREST.SchemaCache.Relationship (Relationship (..), RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..))
import Protolude
@@ -44,7 +43,7 @@ data ApiRequestError
| InvalidPreferences [ByteString]
| InvalidRange RangeError
| InvalidRpcMethod ByteString
| NotEmbedded Text
| NotEmbedded Text (Maybe (Text, Text))
| NotImplemented Text
| PutLimitNotAllowedError
| QueryParamError QPError
@@ -7,15 +7,13 @@ Description : Logging based on the Observation.hs module. Access logs get sent t
-}
-- TODO log with buffering enabled to not lose throughput on logging levels higher than LogError
module PostgREST.Logger
( middleware
, observationLogger
(observationLogger
, init
, LoggerState
) where
import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate,
updateAction)
mkAutoUpdate, updateAction)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text.Encoding as T
import qualified Hasql.Decoders as HD
@@ -23,20 +21,16 @@ import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Statement as SQL
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
getZonedTime)
import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.RequestLogger as Wai
import Data.Time (ZonedTime, defaultTimeLocale, formatTime, getZonedTime)
import Network.HTTP.Types.Status (Status, status400, status500)
import System.IO.Unsafe (unsafePerformIO)
import PostgREST.Config (LogLevel (..), Verbosity (..))
import PostgREST.Debounce (makeDebouncer)
import PostgREST.Config (LogLevel (..), Verbosity (..))
import PostgREST.Debounce (makeDebouncer)
import PostgREST.Logger.Apache (apacheFormat)
import PostgREST.Observation
import PostgREST.Query (MainQuery (..))
import PostgREST.SchemaCache (queryTimingsWLabels)
import PostgREST.Query (MainQuery (..))
import PostgREST.SchemaCache (queryTimingsWLabels)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
@@ -51,32 +45,19 @@ import Protolude
data LoggerState = LoggerState
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
, stateLogDebouncePoolTimeout :: IO () -- ^ Logs with a debounce
, getLogLevel :: IO LogLevel -- ^ Get LogLevel from Config
}
init :: IO LoggerState
init = mdo
init :: IO LogLevel -> IO LoggerState
init getLogLvl = mdo
let
oneSecond = 1000000
loggerState = LoggerState zTime debouncePoolTimeout
oneSecond = 1_000_000
loggerState = LoggerState zTime debouncePoolTimeout getLogLvl
zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
debouncePoolTimeout <- makeDebouncer $
logWithZTime loggerState (observationMessages PoolAcqTimeoutObs) *> threadDelay (5 * oneSecond)
pure loggerState
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
middleware logLevel getAuthRole =
unsafePerformIO $
Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
{ Wai.outputFormat =
Wai.ApacheWithSettings $
Wai.defaultApacheSettings &
Wai.setApacheRequestFilter (\_ res -> shouldLogResponse logLevel $ Wai.responseStatus res) &
Wai.setApacheUserGetter getAuthRole
, Wai.autoFlush = True
, Wai.destination = Wai.Handle stdout
}
shouldLogResponse :: LogLevel -> Status -> Bool
shouldLogResponse logLevel = case logLevel of
LogCrit -> const False
@@ -86,48 +67,58 @@ shouldLogResponse logLevel = case logLevel of
LogDebug -> const True
-- All observations are logged except some that depend on the log-level
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
observationLogger loggerState logLevel obs = case obs of
PoolAcqTimeoutObs -> do
when (logLevel >= LogError) $
stateLogDebouncePoolTimeout loggerState
o@(QueryErrorCodeHighObs _) -> do
when (logLevel >= LogError) $ do
observationLogger :: LoggerState -> ObservationHandler
observationLogger loggerState obs = do
logLevel <- getLogLevel loggerState -- We need to do the IO action to read the "log-level" config value because it can be reloaded
case obs of
PoolAcqTimeoutObs -> do
when (logLevel >= LogError) $
stateLogDebouncePoolTimeout loggerState
o@(QueryErrorCodeHighObs _) -> do
when (logLevel >= LogError) $ do
logWithZTime loggerState $ observationMessages o
o@SchemaCacheEmptyObs ->
when (logLevel >= LogError) $ do
logWithZTime loggerState $ observationMessages o
o@SchemaCacheEmptyObs ->
when (logLevel >= LogError) $ do
logWithZTime loggerState $ observationMessages o
o@(HasqlPoolObs _) -> do
when (logLevel >= LogDebug) $ do
o@(HasqlPoolObs _) -> do
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@(QueryObs _ status) -> do
when (shouldLogResponse logLevel status) $
logWithZTime loggerState $ observationMessages o
o@PoolRequest ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@PoolRequestFullfilled ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
ResponseObs maybeRole req status contentLen ->
when (shouldLogResponse logLevel status) $ do
zTime <- stateGetZTime loggerState
putStr $ apacheFormat maybeRole (BS.pack $ formatZonedTime zTime) req status contentLen -- putStr prints to stdout
o@PoolFlushed ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@JwtCacheEviction ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@(JwtCacheLookup _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@(WarpServerObs _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o ->
logWithZTime loggerState $ observationMessages o
o@(QueryObs _ status) -> do
when (shouldLogResponse logLevel status) $
logWithZTime loggerState $ observationMessages o
o@PoolRequest ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@PoolRequestFullfilled ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@PoolFlushed ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@JwtCacheEviction ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@(JwtCacheLookup _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o@(WarpServerObs _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessages o
o ->
logWithZTime loggerState $ observationMessages o
logWithZTime :: LoggerState -> [Text] -> IO ()
logWithZTime loggerState txts = do
zTime <- stateGetZTime loggerState
traverse_ (hPutStrLn stderr . (toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <>)) txts
let prefix = toS (formatZonedTime zTime) <> ": "
traverse_ (hPutStrLn stderr . (prefix <>)) txts
formatZonedTime :: ZonedTime -> [Char]
formatZonedTime = formatTime defaultTimeLocale "%d/%b/%Y:%T %z"
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
@@ -142,6 +133,8 @@ observationMessages :: Observation -> [Text]
observationMessages = \case
AdminStartObs address ->
pure $ "Admin server listening on " <> address
AdminServerCrashedObs ex ->
pure $ "Admin server crashed unexpectedly: " <> (showOnSingleLine '\t' . show) ex
AppStartObs ver ->
pure $ "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerAddressObs address ->
@@ -188,8 +181,8 @@ observationMessages = \case
either showListenerConnError showListenerException listenErr
DBListenRetry delay ->
pure $ "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
DBListenBugHint ->
pure "HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
DBListenBugCallQueryFix ->
pure "This is likely a PostgreSQL bug in the notification queue, executing the following to try to solve it: SELECT pg_notification_queue_usage();"
DBListenerGotSCacheMsg channel ->
pure $ "Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel ->
@@ -200,6 +193,10 @@ observationMessages = \case
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
in
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
LegacyTargetNameWarningObs (warningMsg, warningHints) requestMethod requestTarget ->
[ "WARNING: " <> warningMsg
, "Update filters, orders or limits that use " <> warningHints <> " in " <> "`" <> T.decodeUtf8 (requestMethod <> " " <> requestTarget) <> "`"
]
ConfigReadErrorObs usageErr ->
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr ->
@@ -243,6 +240,8 @@ observationMessages = \case
pure $ "Received termination unix signal " <> signal
WarpServerObs txt ->
pure $ "Warp server: " <> txt
ResponseObs {} ->
mempty -- Control flow never reaches here, the observation message is returned in observationLogger function
where
showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) x ""
+48
View File
@@ -0,0 +1,48 @@
module PostgREST.Logger.Apache
( apacheFormat
) where
import qualified Data.ByteString.Char8 as BS
import Network.Wai.Logger
import System.Log.FastLogger
import Network.HTTP.Types.Status (Status, statusCode)
import Network.Wai
import Protolude
apacheFormat :: ToLogStr user => Maybe user -> FormattedTime -> Request -> Status -> Maybe Integer -> ByteString
apacheFormat maybeUser tmstr req status msize =
fromLogStr $ apacheLogStr maybeUser tmstr req status msize
-- This code is vendored from
-- https://github.com/kazu-yamamoto/logger/blob/57bc4d3b26ca094fd0c3a8a8bb4421bcdcdd7061/wai-logger/Network/Wai/Logger/Apache.hs#L44-L45
apacheLogStr :: ToLogStr user => Maybe user -> FormattedTime -> Request -> Status -> Maybe Integer -> LogStr
apacheLogStr maybeUser tmstr req status msize =
toLogStr (getSourceFromSocket req)
<> " - "
<> maybe "-" toLogStr maybeUser
<> " ["
<> toLogStr tmstr
<> "] \""
<> toLogStr (requestMethod req)
<> " "
<> toLogStr path
<> " "
<> toLogStr (show (httpVersion req)::Text)
<> "\" "
<> toLogStr (show (statusCode status)::Text)
<> " "
<> toLogStr (maybe "-" show msize::Text)
<> " \""
<> toLogStr (fromMaybe "" mr)
<> "\" \""
<> toLogStr (fromMaybe "" mua)
<> "\"\n"
where
path = rawPathInfo req <> rawQueryString req
mr = requestHeaderReferer req
mua = requestHeaderUserAgent req
getSourceFromSocket :: Request -> ByteString
getSourceFromSocket = BS.pack . showSockAddr . remoteHost
@@ -16,8 +16,7 @@ module PostgREST.MainTx
import Control.Lens ((^?))
import Control.Monad.Extra (whenJust)
import qualified Data.Aeson.Lens as L
import qualified Data.ByteString as BS hiding
(break)
import qualified Data.ByteString as BS hiding (break)
import qualified Data.ByteString.Char8 as BS
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S
@@ -32,21 +31,17 @@ import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferHandling (..),
import PostgREST.ApiRequest.Preferences (PreferCount (..), PreferHandling (..),
PreferMaxAffected (..),
PreferTransaction (..),
Preferences (..))
import PostgREST.ApiRequest.Types (Mutation (..))
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.Config (AppConfig (..), OpenAPIMode (..))
import PostgREST.Error (Error)
import PostgREST.MediaType (MediaType (..))
import PostgREST.Plan (ActionPlan (..),
CrudPlan (..),
DbActionPlan (..),
InfoPlan (..),
import PostgREST.Plan (ActionPlan (..), CrudPlan (..),
DbActionPlan (..), InfoPlan (..),
InspectPlan (..))
import PostgREST.Query (MainQuery (..))
import PostgREST.SchemaCache (SchemaCache (..))
@@ -59,12 +54,7 @@ import Protolude hiding (Handler)
type DbHandler = ExceptT Error SQL.Transaction
data MainTx
= DbTx {
dqIsoLevel :: SQL.IsolationLevel
, dqTxMode :: SQL.Mode
, dqDbHandler :: DbHandler DbResult
, dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error DbResult) -> SQL.Session (Either Error DbResult)
}
= DbTx (SQL.Session (Either Error DbResult))
| NoDbTx DbResult
data DbResult
@@ -96,7 +86,7 @@ data ResultSet
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
DbTx isoLvl txMode dbHandler SQL.transactionNoRetry
DbTx $ SQL.transactionNoRetry isoLvl txMode $ runExceptT dbHandler
where
isoLvl = planIsoLvl conf authRole plan
txMode = planTxMode plan
@@ -115,7 +105,7 @@ planTxMode (MayUseDb x) = ipTxmode x
planIsoLvl :: AppConfig -> ByteString -> DbActionPlan -> SQL.IsolationLevel
planIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of
DbCrud _ CallReadPlan{crProc} -> fromMaybe roleIsoLvl $ pdIsoLvl crProc
_ -> roleIsoLvl
_ -> roleIsoLvl
where
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl

Some files were not shown because too many files have changed in this diff Show More