Compare commits

..
142 Commits
Author SHA1 Message Date
steve-chavez 064e5fea7b chore: bump version to 14.17 2026-08-13 10:48:30 -05:00
Michał KłeczekandSteve Chavez cbdafd6f8b 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.

(cherry picked from commit a2a592a617)
2026-08-13 09:30:02 -05:00
renovate[bot]andWolfgang Walther b6ffe9bcf8 chore(deps): update vmactions/freebsd-vm action to v1.5.3 2026-08-07 07:40:59 +00:00
renovate[bot]andWolfgang Walther c57a10a8c4 chore(deps): update all dependencies 2026-08-04 21:04:54 +00:00
renovate[bot]andWolfgang Walther aee0d52b4f chore(deps): update docker/login-action action to v4.6.0 2026-07-30 20:41:24 +00:00
Taimoor Zaeem 673bbbf291 chore: bump version to 14.16 2026-07-27 11:19:37 +05:00
renovate[bot]andWolfgang Walther 17c39dbef6 chore(deps): update docker/login-action action to v4.5.1 2026-07-24 19:26:07 +00:00
renovate[bot]andWolfgang Walther 1a777919c6 chore(deps): update all dependencies 2026-07-24 18:51:21 +00:00
Taimoor Zaeem 85c8828c0d fix: kill postgrest main thread on admin server crash
Fixes #5096.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-21 22:00:56 +05:00
Taimoor Zaeem 6206f55673 docs: update PostgREST source file locations in architecture page
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-18 18:24:46 +05:00
Taimoor Zaeem 2aa023e2d7 chore: add .ghc.environment* to .gitignore
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-07-18 18:24:46 +05:00
renovate[bot]andWolfgang Walther 82315a45ff chore(deps): update ubuntu:resolute docker digest to 3131b4c 2026-07-17 17:42:26 +00:00
renovate[bot]andWolfgang Walther d9931d4165 chore(deps): update ubuntu:resolute docker digest to 651ba3f 2026-07-16 21:19:27 +00:00
steve-chavez 167a0a1800 chore: bump version to 14.15 2026-07-13 17:15:03 -05:00
Steve ChavezandGitHub 032ceb62c1 fix: admin server dies silently by upgrading warp
* 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.

* amend: use warp 3.4.14 for 3522917

Use a release instead of a git commit

* amend: update cabal index so cabal build succeeds

* chore: pin jose-jwt to build on CI
2026-07-13 15:45:54 -05:00
steve-chavez 71a894623c ci: add git to freebsd
Otherwise when using a git commit on stack the build fails because it
lacks the git dependency.
2026-07-10 21:49:58 -05:00
renovate[bot]andWolfgang Walther 3eab24400c chore(deps): update docker/login-action action to v4.4.0 2026-07-05 12:49:36 +00:00
renovate[bot]andWolfgang Walther 01593a8a2e chore(deps): update docker/setup-buildx-action action to v4.2.0 2026-07-02 20:08:01 +00:00
renovate[bot]andWolfgang Walther 099f053f50 chore(deps): update all dependencies 2026-07-02 08:56:42 +00:00
ncikandTaimoor Zaeem 46dd242c9c 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
(cherry picked from commit 69d21a82c9)
2026-07-02 13:46:04 +05:00
renovate[bot]andWolfgang Walther f06a03329f chore(deps): update korthout/backport-action action to v4.6 2026-06-30 06:24:42 +00:00
steve-chavez c1be852d55 chore: bump version to 14.14 2026-06-29 13:20:56 -05:00
Taimoor Zaeemandsteve-chavez ae65768569 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 13:13:16 -05:00
renovate[bot]andWolfgang Walther c98a8c2a29 chore(deps): update vmactions/freebsd-vm action to v1.5.0 2026-06-29 17:45:43 +00:00
renovate[bot]andWolfgang Walther 1853a4a2b7 chore(deps): update vmactions/freebsd-vm action to v1.4.9 2026-06-27 09:41:00 +00:00
renovate[bot]andWolfgang Walther 64407a45fc chore(deps): update actions/cache action to v6.1.0 2026-06-26 19:48:31 +00:00
renovate[bot]andWolfgang Walther 2641bf48a8 chore(deps): update all dependencies 2026-06-25 20:10:24 +00:00
renovate[bot]andWolfgang Walther 050e54965f chore(deps): update all dependencies to v1.4.8 2026-06-20 21:57:15 +00:00
renovate[bot]andWolfgang Walther 5826ce50c8 chore(deps): update all dependencies 2026-06-19 19:08:29 +00:00
renovate[bot]andWolfgang Walther 9772e075f6 chore(deps): update all dependencies 2026-06-19 19:07:45 +00:00
steve-chavezandTaimoor Zaeem ef54d94cc1 amend: required membership for postgrest_test_anonymous
Previous test correction required GRANTing membership for
postgrest_test_anonymous.
2026-06-12 12:34:04 +05:00
steve-chavezandTaimoor Zaeem d67f68330a 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-12 12:34:04 +05:00
Wolfgang Walther 2e4b72d130 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 15:04:34 +02:00
renovate[bot]andWolfgang Walther 4e68ffbc92 chore(deps): update all dependencies to v7 2026-06-07 11:01:20 +00:00
steve-chavez b2501edf8d chore: bump version to 14.13 2026-06-04 13:49:01 -05:00
Laurence IslaandTaimoor Zaeem 4dc7d84ff1 fix: request failures when work_mem is set on a role 2026-06-04 21:25:50 +05:00
Taimoor Zaeem 627e8c07ab test(io): set client_min_messages to warning in SQL fixtures
To suppress unwanted "NOTICE: ..." messages which pollute the log.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-04 15:23:43 +05:00
Taimoor Zaeem 3decedce1e test(io): refactor SQL fixtures
- Move privileges and grants to privileges.sql
- Move schema, tables and functions to schema.sql
- Move global database settings to database.sql

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-04 15:23:43 +05:00
Taimoor Zaeem c71bfacc1d test(io): move role fixtures to roles.sql
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-06-04 15:23:43 +05:00
netandSteve Chavez 4f69c407e3 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 13:36:33 -05:00
Wolfgang Walther 80d0caaff9 ci: remove deprecation warning
app-id was replaced with client-id
2026-06-03 10:09:47 +02:00
renovate[bot]andWolfgang Walther 0dbbf734b7 chore(deps): update all dependencies to v6.0.3 2026-06-03 07:39:54 +00:00
Taimoor Zaeem 8357488ded 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>
(cherry picked from commit 669fc9ede8)
2026-06-02 15:07:46 +05:00
Wolfgang Walther 7bf7da627b chore: fix actionlint with ubuntu-slim jobs 2026-06-02 10:39:04 +02:00
Wolfgang Walther b1864594e6 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:15:14 +02:00
Wolfgang Walther e9464bc535 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:15:14 +02:00
Wolfgang Walther 27734f5d51 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-06-02 10:15:13 +02:00
Wolfgang Walther 8ce8939259 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-06-02 10:15:12 +02:00
Wolfgang Walther f71f70152a ci: avoid running pg18 tests twice
We already test PG18, the default, with the coverage job, so why run it
twice?
2026-06-02 10:15:12 +02:00
Wolfgang Walther 41b58579cb 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-06-02 10:15:11 +02:00
Wolfgang Walther 6d2fa4d3a7 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-06-02 10:15:10 +02:00
Wolfgang Walther 99905c7988 ci: test build of dev shell on darwin 2026-06-02 10:13:29 +02:00
Wolfgang Walther df38175645 ci: use nix-build-uncached to save useless downloads
There is no point to actually realize the derivations to disk, when they
are in cache. `nix-build-uncached` checks the cache and stops before
downloading dependencies if they are available. It will however build as
normal on a cache-miss. Should speed up CI when no Nix dependencies
change a fair bit.

We can only do this for the MacOS job, which only tests the builds. We
can not do this for other jobs which require the build results to be
present before uploading parts of them as artifacts.
2026-06-02 10:13:28 +02:00
Wolfgang Walther ebac332cb1 ci: trigger workflows when nixpkgs pin is updated
Otherwise we might break things...
2026-06-02 10:13:27 +02:00
Wolfgang Walther 6e573b911d ci: Use FreeBSD VM instead of cirrus 2026-06-02 10:10:22 +02:00
Taimoor Zaeem e49a17ea56 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>
(cherry picked from commit e1d547623e)
2026-05-28 14:28:37 +05:00
renovate[bot]andWolfgang Walther b4eec4bbfb chore(deps): update all dependencies 2026-05-22 20:51:50 +00:00
Michał KłeczekandTaimoor Zaeem 83e0df24f5 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-21 12:01:10 +05:00
Michał KłeczekandTaimoor Zaeem b1a53e008c 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-21 12:01:10 +05:00
steve-chavez 6200fbad58 chore: bump version to 14.12 2026-05-20 12:20:38 -05:00
steve-chavez b71345ab09 test: adjust test_positive_pool_metric
Adjusted so it waits for liveness instead of requiring sleeping
2026-05-20 10:29:12 -05:00
steve-chavez 9be5c41c05 test: change wait_for_readiness to an enum
This so we can wait for both readiness and liveness
2026-05-20 10:29:12 -05:00
Michał KłeczekandSteve Chavez ed2dc1fe86 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-20 10:29:12 -05:00
steve-chavez f6e99117ab 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-20 10:29:12 -05:00
steve-chavez 0be41a4941 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-20 10:29:12 -05:00
renovate[bot]andWolfgang Walther ae1e3d6c4d chore(deps): update codecov/codecov-action action to v6.0.1 2026-05-19 07:04:25 +00:00
Taimoor Zaeem f2932bffea test(io): move utility function to util.py
The function `match_log` should be in `util.py` so it can be reused
in other modules.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-05-19 10:01:51 +05:00
Taimoor ZaeemandSteve Chavez 15b83ff4da 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>
(cherry picked from commit ab43ee1fbb)
2026-05-15 19:40:45 -05:00
renovate[bot]andWolfgang Walther a2d76b01b7 chore(deps): update dependency macos to v26 2026-05-15 20:38:43 +00:00
renovate[bot]andTaimoor Zaeem 16d4477c62 chore(deps): update actions/create-github-app-token action to v3.2.0 2026-05-14 10:14:25 +05:00
Taimoor Zaeem d75d2838fc 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>
(cherry picked from commit f90d7d7296)
2026-05-14 09:56:19 +05:00
renovate[bot]andWolfgang Walther 4e08b2748c chore(deps): update actions/create-github-app-token action to v3.2.0 2026-05-13 21:01:16 +00:00
Taimoor Zaeem 86f03c5ded docs: move PostGIS under Integrations
For better visibility.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 055921eaf5)
2026-05-13 13:27:10 +05:00
renovate[bot]andWolfgang Walther eda5a10aa5 chore(deps): update all dependencies 2026-05-12 13:03:04 +00:00
Taimoor ZaeemandWolfgang Walther 51c51ea140 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>
(cherry picked from commit 96f12f83f0)
2026-05-07 07:10:27 +00:00
Taimoor Zaeem 3824e73c91 nix(chore): add commit prefix in release commit message
We should follow our commit convention.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 4c04572783)
2026-05-07 09:29:56 +05:00
renovate[bot]andWolfgang Walther d65288dfd6 chore(deps): update korthout/backport-action digest to bf97bcf 2026-05-06 19:47:08 +00:00
Michał KłeczekandTaimoor Zaeem 80ea1f4a1b refactor: make Observation Logger and Listener modules more cohesive
Observation module exports observationMessage and isDbListenerBug functions. The first one is used only in Logger module and the second one - only in Listener.

This change moves observationMessage function to Logger module and isDbListenerBug function to Listener module, making all three modules more cohesive and lessening dependencies.
It also gives the compiler more opportunities for intra-module inlining and optimization.
2026-05-07 00:36:36 +05:00
renovate[bot]andWolfgang Walther 23b0c8d82a chore(deps): update ubuntu:resolute docker digest to f3d2860 2026-05-05 15:53:44 +00:00
Taimoor Zaeem 1f69247cc2 bump version to 14.11 2026-05-04 22:59:24 +05:00
Taimoor Zaeem e25a87bd84 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 22:08:06 +05:00
Laurence IslaandTaimoor Zaeem 0502488b70 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-05-04 22:08:06 +05:00
Michal KleczekandTaimoor Zaeem a62b6de1d0 fix: Restore showing LISTEN pgrst in pg_stat_activity 2026-05-04 22:08:06 +05:00
Taimoor ZaeemandWolfgang Walther 9baf17aed0 fix: login with uppercase and mixed case role names
PostgREST failed when querying role settings where current
role name contained uppercase letters. This commit resolves
it by quoting the CURRENT_USER.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-04-27 19:53:52 +00:00
renovate[bot]andWolfgang Walther d9eabb2742 chore(deps): update korthout/backport-action action to v4.5 2026-04-27 19:51:56 +00:00
renovate[bot]andWolfgang Walther 7711a86c0c chore(deps): update ubuntu docker tag to v26 2026-04-24 06:29:44 +00:00
renovate[bot]andWolfgang Walther d75a2bdce9 chore(deps): update korthout/backport-action action to v4.4.0 2026-04-21 09:05:15 +00:00
steve-chavezandWolfgang Walther dd1956557d ci: reinstate macos x86 releases
Closes https://github.com/PostgREST/postgrest/issues/4755

(cherry picked from commit b6c6bb27a8)
2026-04-19 12:19:17 +00:00
steve-chavez 83e1bebcdc bump version to 14.10 2026-04-16 13:54:44 -05:00
renovate[bot]andWolfgang Walther 89132e886a chore(deps): update ubuntu:noble docker digest to c4a8d55 2026-04-16 10:44:03 +00:00
Michal KleczekandTaimoor Zaeem abd76ca8ac fix: Flush pool as late as possible during schema cache reloading (#4645)
retryingSchemaCacheLoad flushes the pool upon every retry before it starts reloading the schema. This is too early as schema reloading might take some time during which new connections might be acquired. The consequence is that:
* upon successful schema cache reload we might have some connections created with the old schema cache
* we close connections upon each retry and under load we will keep closing and re-opening connections until schema cache load succeeds

This change is to make sure we flush the pool only after successful schema cache querying but before loading (so that connections acquired during loading wait for it and do not interfere with timing the loading process).
2026-04-16 11:55:43 +05:00
Michał KłeczekandTaimoor Zaeem df87ce46ed add: log pool flushes
Emit a dedicated PoolFlushed observation when the DB pool is released during schema cache reload.
2026-04-16 11:18:44 +05:00
renovate[bot]andWolfgang Walther d369d2c41e chore(deps): update haskell-actions/setup action to v2.11.0 2026-04-15 18:05:21 +00:00
Wolfgang Walther 9ee366a903 docs: fix broken link 2026-04-15 09:30:05 +02:00
Michał KłeczekandTaimoor Zaeem 01f7f13389 test(refactor): make debug output testing more robust
This change introduces a match_log function, that implements lenient matching of log output lines. Tests of log output are retrofited to use the function.
2026-04-15 07:40:16 +05:00
steve-chavez 4922a9feaf docs: target_session_attrs=read-only in libpq>=14 2026-04-14 11:11:22 -05:00
renovate[bot]andWolfgang Walther 679b957355 chore(deps): update actions/cache action to v5.0.5 2026-04-13 21:04:21 +00:00
renovate[bot]andWolfgang Walther 05df2c5692 chore(deps): update all dependencies 2026-04-12 15:25:03 +00:00
steve-chavez 16e2c73a95 bump version to 14.9 2026-04-10 18:25:04 -05:00
steve-chavez fb31654277 fix: remove red herring warp logs on default log-level
The logs added on e95e815483 are red
herrings under normal operation.

This moves them to `log-level=debug` and removes "error" from the
message prefix.

Fixes https://github.com/PostgREST/postgrest/issues/4799
2026-04-10 18:16:19 -05:00
steve-chavez 9230f3f49a Revert "refactor: Simplify schema cache loading triggering logic"
This reverts commit 810023a47c.
2026-04-10 18:16:19 -05:00
steve-chavez 96bbc86756 Revert "refactor: clarify debouncer function"
This reverts commit 577ed4dd4c.
2026-04-10 18:16:19 -05:00
renovate[bot]andWolfgang Walther dd394d6f81 chore(deps): update haskell-actions/setup action to v2.10.4 2026-04-09 08:48:45 +00:00
Steve ChavezandTaimoor Zaeem 577ed4dd4c refactor: clarify debouncer function
(cherry picked from commit 3d98f8d65b)
2026-04-08 10:46:38 +05:00
Michał KłeczekandTaimoor Zaeem 810023a47c refactor: Simplify schema cache loading triggering logic
DISCLAIMER:
This commit was authored entirely by a human without the assistance of LLMs.

Using debouncer to trigger schema cache loading makes it difficult to understand when exactly it is triggered.

(cherry picked from commit a4c1d945ee)
2026-04-08 10:46:38 +05:00
renovate[bot]andWolfgang Walther 687ebf0850 chore(deps): update ubuntu:noble docker digest to 84e77de 2026-04-07 08:10:14 +00:00
Michał KłeczekandSteve Chavez b9c8562641 add: Log pg version details of listener connection
Follow-up to #4617 adding more information to log entry produced upon successful listener connection establishement.
2026-04-06 11:11:16 -05:00
Michał KłeczekandSteve Chavez 34a767a5cc add: Log actual host and port of listener connection
Diagnosing problems with listener channel notifications not being handled properly by PostgREST connected to read replicas is difficult. Issues might be related to lost connections and listener not being connected to the right host after failover or database server restarts.
This patch adds logging of actual host:port used by libpq connection opened by the listener. It should make it easier to find out if PostgREST is connected to the right host.
2026-04-06 11:11:16 -05:00
steve-chavezandTaimoor Zaeem 5eac8bd203 docs: clarify set operators need views/functions
Closes https://github.com/PostgREST/postgrest/issues/4780.
2026-04-06 11:18:42 +05:00
steve-chavez 9f722e0799 bump version to 14.8 2026-04-03 16:51:55 -05:00
Artur Bento de CarvalhoandSteve Chavez 0d4d1dca51 fix: use int32/int64 formats for integer types
Fixed integer type mapping in OpenAPI 2.0: replaced the invalid integer format with int32/int64 and added the toSwaggerFormat function to map PostgreSQL types to valid OpenAPI 2.0 formats:

smallint -> int32
integer -> int32
bigint -> int64
2026-04-03 16:36:55 -05:00
Laurence IslaandSteve Chavez 250747aadc add(logs): Include a HINT when the LISTEN channel breaks due to a Postgres bug
The HINT shows a SQL command that solves the issue.
2026-04-03 16:36:55 -05:00
Michał KłeczekandTaimoor Zaeem aae929a718 test: Schema cache load debouncing
test: adjust replicateM to 100
(cherry picked from commit 328598eaed)
2026-04-03 08:00:57 +05:00
renovate[bot]andWolfgang Walther 77dde73057 chore(deps): update docker/login-action action to v4.1.0 2026-04-02 20:06:52 +00:00
Michal KleczekandSteve Chavez afb95a5268 refactor(test): provide means to validate metrics and observations
Some helpers are provided for introspecting metrics already (used in JWT cache tests). This change provides facilities to additionally validate emited Observation events.
A new Spec module is also implemented, adding basic tests of schema cache reloading - their main goal is to excercise the new infrastructure.
2026-04-02 13:34:38 -05:00
Michał KłeczekandTaimoor Zaeem 8262faa235 refactor: move socket creation and management to App module
Right now listening sockets initialization, management and usage is split between App, AppState and Admin modules: they are created in AppState.init and remembered in AppState but used only in App and Admin.

It has several negative consequences:
- sockets are initialized even if not needed (eg. command line invocations like dump-config or dump-schema)
- it is impossible to start listening on a socket after initial schema cache load because it requires AppState

This change decouples listen socket management from AppState. Sockets are created only when needed (ie. not in command line tools invocation) and passed to admin application and to Warp by the App module.
2026-04-02 12:27:31 +05:00
Michał KłeczekandTaimoor Zaeem 1d40fe5d93 test: Fix flakiness of test_second_request_for_non_existent_table_should_be_quick
Changed divider in assertion (response.elapsed.total_seconds() < first_duration / divider) to 2 (from 10).

(cherry picked from commit 886df84e87)
2026-04-01 15:14:45 +05:00
renovate[bot]andWolfgang Walther 3a4dc5eff3 chore(deps): update codecov/codecov-action action to v6 2026-03-27 11:02:38 +00:00
renovate[bot]andWolfgang Walther 55912515c0 chore(deps): update all dependencies 2026-03-27 08:45:13 +00:00
postgrest-ci[bot]andGitHub 863d1c9e9b docs: explain schema cache reload behavior with NOTIFY debouncing 2026-03-26 11:00:40 +05:00
steve-chavez 806579e659 bump version to 14.7 2026-03-20 14:22:31 -05:00
steve-chavez 1856434a74 fix: not logging termination unix signals
Under container environments like ECS, it's hard to know when PostgREST
is being terminated.
2026-03-20 13:38:40 -05:00
Taimoor ZaeemandSteve Chavez dade15acc4 nix(test): add test suite for observability tests
- Create separate test suite for observability tests

- Create wrapper script `postgrest-test-observability`

- Add to CI and `postgrest-check`

- Move JWT cache tests under observability tests

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-03-20 09:13:47 -05:00
Michał KłeczekandSteve Chavez 79531a612d test(spec): Move metrics state helpers from JwtCacheSpec to SpecHelpers
Refactoring: State validation helpers used in JwtCacheSpec moved to SpecHelper
to make them available in other Spec modules.

(cherry picked from commit 85a313a8cc)
2026-03-19 14:30:18 -05:00
renovate[bot]andWolfgang Walther 9d0e13d961 chore(deps): update ubuntu:noble docker digest to 186072b 2026-03-19 09:15:59 +00:00
renovate[bot]andWolfgang Walther 3e61dc024e chore(deps): update cachix/cachix-action action to v17 2026-03-18 21:24:50 +00:00
renovate[bot]andWolfgang Walther 1bdf773417 chore(deps): update all dependencies 2026-03-18 20:01:41 +00:00
renovate[bot]andWolfgang Walther 55868351f3 chore(deps): update actions/cache action to v5.0.4 2026-03-18 19:09:06 +00:00
renovate[bot]andWolfgang Walther 8c3cb76fbd chore(deps): update ubuntu:noble docker digest to 0d39fcc 2026-03-17 10:22:17 +00:00
renovate[bot]andWolfgang Walther 3bdb69bae7 chore(deps): update actions/create-github-app-token action to v3 2026-03-14 18:06:45 +00:00
renovate[bot]andWolfgang Walther 1ed3c2c197 chore(deps): update all dependencies 2026-03-13 17:23:12 +00:00
renovate[bot]andWolfgang Walther dde145b685 chore(deps): update all dependencies 2026-03-13 17:19:11 +00:00
Wolfgang Walther 720705e32b docs: fix prometheus text format link
Reported by linkcheck.
2026-03-11 09:45:21 +01:00
steve-chavez 6813ceaf31 bump version to 14.6 2026-03-06 16:30:04 -05:00
Taimoor Zaeemandsteve-chavez 73c8ae0bbc fix(error): leaking table and function names when calculating hint
Increase similarity score to 0.75 from 0.33 for table and functions
error hint.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-03-06 16:16:05 -05:00
Laurence Isla 6f356e32b6 chore: update sponsors
* The list of sponsors is now ordered by total contribution value (highest first)
* Removed former backers from sponsor pages
2026-03-03 23:11:22 -05:00
Laurence Isla c4772184e4 chore: update sponsor 2026-03-03 23:11:22 -05:00
dshukertjrandWolfgang Walther 0602080acb docs: Update the Supabase logo to a correct one 2026-02-19 10:54:29 +01:00
steve-chavez dc67e4d3f5 chore: fix changelog entries 2026-02-13 13:40:38 -05:00
steve-chavez d89826b982 bump version to 14.5 2026-02-12 20:12:52 -05:00
steve-chavez 94350fdde6 fix: don't hide async exceptions in logs
Fixes #4646. Using the repro on #4646, this now produces the log:

```
11/Feb/2026:09:40:08 -0500: Warp server error: stack overflow
```

When:
```
$ curl localhost:3000/
curl: (52) Empty reply from server
```

(cherry picked from commit e95e815483)
2026-02-12 20:11:44 -05:00
Taimoor ZaeemandWolfgang Walther 230eb3630c docs(install): update postgresql minimum supported version
PostgREST dropped support for PostgreSQL version 12 however,
it was not reflected in the docs.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-02-12 10:23:29 +01:00
101 changed files with 2431 additions and 1291 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
+3
View File
@@ -2,4 +2,7 @@
# and made its way to us through nixpkgs.
self-hosted-runner:
labels:
- macos-15-intel
- macos-26
- ubuntu-24.04-arm
- ubuntu-slim
@@ -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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }}
if-no-files-found: error
+2 -2
View File
@@ -19,14 +19,14 @@ inputs:
runs:
using: composite
steps:
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
- 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 }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ runner.os }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
- 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@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
with:
name: postgrest
authToken: ${{ inputs.authToken }}
+5 -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@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.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,14 @@ jobs:
# This is required for backport action to cherry-pick the PR
- name: Fetch PR ref
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
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@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6
with:
github_token: ${{ steps.app-token.outputs.token }}
pull_description: 'Backport for #${pull_number}.'
+45 -57
View File
@@ -16,6 +16,7 @@ on:
- .github/*
- '*.nix'
- nix/**
- flake.lock
- .cirrus.yml
- cabal.project*
- postgrest.cabal
@@ -33,7 +34,7 @@ jobs:
name: Nix - Linux x86-64 static
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -42,7 +43,7 @@ jobs:
- name: Build static executable
run: nix-build -A postgrestStatic -A postgrestStatic.tests
- name: Save built executable as artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: postgrest-linux-static-x86-64
path: result/bin/postgrest
@@ -51,7 +52,7 @@ jobs:
- name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
- name: Save built Docker image as artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: postgrest-docker-x86-64
path: postgrest-docker.tar.gz
@@ -60,26 +61,21 @@ jobs:
macos:
name: Nix - MacOS
runs-on: macos-15
runs-on: macos-26
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Install gnu sed
run: brew install gnu-sed
- name: Install nix-build-uncached
run: nix-env -f default.nix -iA nix-build-uncached
- name: Build everything
run: |
# The --dry-run will give us a list of derivations to download from cachix and
# derivations to build. We only take those that would have to be built and then build
# those explicitly. This has the advantage that pure verification will not include
# a download anymore, making it much faster. If something needs to be built, only
# the dependencies required to do so will be downloaded, but not everything.
nix-build --dry-run 2>&1 \
| gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \
| xargs nix-build
- name: Build everything (default.nix)
run: nix-build-uncached
- name: Build everything (shell.nix)
run: nix-build-uncached shell.nix
stack:
@@ -87,66 +83,71 @@ 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: Linux aarch64
runs-on: ubuntu-24.04-arm
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-ubuntu-aarch64
deps: sudo apt-get update && sudo apt-get install libpq-dev
- 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
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: haskell-actions/setup@dc63c94789664bb2910876ec3dfeeaa24d23b96b # v2.10.2
- 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.6.7
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact }}
path: |
@@ -155,19 +156,6 @@ jobs:
if-no-files-found: error
freebsd:
name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
task: Build FreeBSD (Stack)
download: bin
upload: postgrest-freebsd-x86-64
cabal:
strategy:
matrix:
@@ -176,8 +164,8 @@ jobs:
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: haskell-actions/setup@dc63c94789664bb2910876ec3dfeeaa24d23b96b # v2.10.2
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit
+3 -2
View File
@@ -14,6 +14,7 @@ on:
- .github/actions/setup-nix/**
- default.nix
- nix/**
- flake.lock
- docs/**
- '!**.md'
@@ -27,7 +28,7 @@ jobs:
name: Build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -41,7 +42,7 @@ jobs:
name: Spellcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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.
+22 -41
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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,25 +49,9 @@ jobs:
echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md
- name: Save CHANGES.md as artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download all artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
- name: Create release bundle with archives for all builds
@@ -81,6 +66,9 @@ jobs:
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
-C artifacts/postgrest-macos-aarch64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
-C artifacts/postgrest-macos-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
-C artifacts/postgrest-freebsd-x86-64 postgrest
@@ -91,7 +79,7 @@ jobs:
artifacts/postgrest-windows-x86-64/postgrest.exe
- name: Save release bundle
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-bundle
path: release-bundle
@@ -113,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
@@ -129,23 +117,23 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download x86-64 Docker image
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-docker-x86-64
- name: Download aarch64 binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.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 }}
@@ -183,16 +171,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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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 }}
+16 -8
View File
@@ -17,6 +17,7 @@ on:
- .github/actions/setup-nix/**
- default.nix
- nix/**
- flake.lock
- .stylish-haskell.yaml
- cabal.project
- postgrest.cabal
@@ -39,7 +40,7 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -51,7 +52,7 @@ jobs:
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
@@ -69,7 +70,8 @@ jobs:
strategy:
fail-fast: false
matrix:
pgVersion: [13, 14, 15, 16, 17]
# Latest version is tested via `coverage` above.
pgVersion: [13, 14, 15, 16]
name: PG ${{ matrix.pgVersion }}
runs-on: ubuntu-24.04
defaults:
@@ -78,12 +80,12 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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.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.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
- run: postgrest-cabal-update
@@ -91,6 +93,10 @@ jobs:
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec
- name: Run observability tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
- name: Run IO tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv
@@ -104,7 +110,7 @@ jobs:
name: Memory
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
@@ -119,12 +125,13 @@ jobs:
loadtest:
strategy:
fail-fast: false
matrix:
kind: ['mixed', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
name: Loadtest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Nix Environment
@@ -152,13 +159,14 @@ jobs:
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Nix Environment
+1
View File
@@ -26,3 +26,4 @@ loadtest
.docs-build
gen_targets.http
gen_jwk.json
.ghc.environment.*
+13 -8
View File
@@ -13,26 +13,26 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
</a>
</td>
<td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.svg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.png">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/bytebase.svg">
</a>
</td>
</tr>
@@ -93,6 +93,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="222px" src="static/code-build.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr>
</tbody>
</table>
+95
View File
@@ -4,6 +4,101 @@ All notable changes to this project will be documented in this file. From versio
## Unreleased
## [14.17] - 2026-08-13
### Fixed
- JWT validation uses wrong current time due to a bug in auto-update by @mkleczek in #5159
## [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
### Added
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
### Fixed
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
## [14.9] - 2026-04-10
### Added
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
### Fixed
- Remove red herring warp logs on default log-level, only emit them on `log-level=debug` by @steve-chavez in #4799
## [14.8] - 2026-04-03
### Added
- Log a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581
### Fixed
- Fix invalid OpenAPI 2.0 format for integer types (`smallint`, `integer`, `bigint`) by @arturbent0 in #4641
## [14.7] - 2026-03-20
### Fixed
- Fix not logging SIGTERM and SIGINT by @steve-chavez in #4728
## [14.6] - 2026-03-06
### Fixed
- Fix leaking table and function names when calculating error hint by @taimoorzaeem in #4675
## [14.5] - 2026-02-12
### Fixed
- Don't hide async exceptions in logs by @stevechavez in #4646
## [14.4] - 2026-01-29
### Fixed
+1 -1
View File
@@ -2,7 +2,7 @@
# The x86-64 is a single-static-binary image built via Nix, see:
# nix/tools/docker/README.md
FROM ubuntu:noble@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 AS postgrest
FROM ubuntu:resolute@sha256:678c6550cc43645e08669028bc177f50be4e7c5b8cca677067b1914d4afc7a03 AS postgrest
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
+8 -8
View File
@@ -22,26 +22,26 @@ API than you are likely to write from scratch.
</a>
</td>
<td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.svg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.png">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/bytebase.svg">
</a>
</td>
</tr>
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2025-10-29T04:02:18Z
index-state: hackage.haskell.org 2026-08-10T16:58:32Z
+3
View File
@@ -108,6 +108,9 @@ rec {
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
};
# Used by CI on MacOS
inherit (pkgs) nix-build-uncached;
### Tools
cabalTools =
+8 -8
View File
@@ -19,26 +19,26 @@ write from scratch.
</a>
</td>
<td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
<a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/bytebase.svg">
</a>
</td>
</tr>
+1 -1
View File
@@ -6,7 +6,7 @@ Community Tutorials
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
+11 -11
View File
@@ -31,58 +31,58 @@ 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/v14/main/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/v14/src/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/v14/src/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/v14/src/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/v14/src/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/v14/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.
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/v14/src/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/v14/src/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/v14/src/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`.
`Admin.hs <https://github.com/PostgREST/postgrest/blob/v14/src/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.
HTTP
----
@@ -92,4 +92,4 @@ The HTTP server is provided by `Warp <https://aosabook.org/en/posa/warp.html>`_.
Listener
--------
`Listener.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Listener.hs>`_ is in charge of the :ref:`listener`.
`Listener.hs <https://github.com/PostgREST/postgrest/blob/v14/src/PostgREST/Listener.hs>`_ is in charge of the :ref:`listener`.
+1 -1
View File
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
=============================
=============== =================================
**Supported** PostgreSQL >= 12
**Supported** PostgreSQL >= 13
=============== =================================
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
@@ -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>
+26 -19
View File
@@ -38,6 +38,28 @@ Sponsors
.. image:: ../static/cybertec.svg
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
.. container:: img-dark
.. image:: ../static/supabase-dark.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. image:: ../static/supabase.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-dark
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. container:: img-light
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
|
.. container:: img-dark
.. image:: ../static/neon-dark.jpg
@@ -48,30 +70,15 @@ Sponsors
.. image:: ../static/neon.jpg
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
.. image:: ../static/tembo.png
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
.. container:: img-dark
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. image:: ../static/bytebase-dark.svg
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. container:: img-dark
.. image:: ../static/supabase-dark.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. image:: ../static/supabase.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. image:: ../static/bytebase.svg
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
to create space and center the logos. It's not easy to layout with restructuredText.
+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.
+2
View File
@@ -194,3 +194,5 @@ Websockets
webuser
wfts
www
debouncing
deduplicates
@@ -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.
+1 -1
View File
@@ -14,7 +14,7 @@ Custom Queries
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
* Table unions
* SET operators like `UNION, INTERSECT and EXCEPT <https://www.postgresql.org/docs/current/queries-union.html>`_.
* More complicated joins than those provided by :ref:`resource_embedding`.
* Geo-spatial queries that require an argument, like "points near (lat,lon)"
+3 -1
View File
@@ -46,7 +46,9 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
.. note::
Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
- Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
.. _listener_automatic_recovery:
+2 -2
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
@@ -120,7 +120,7 @@ Restart the database and watch the log file in real-time to understand how HTTP
Metrics
=======
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_.
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.
.. code-block:: bash
+13
View File
@@ -53,6 +53,19 @@ To reload the schema cache from within the database, you can use the ``NOTIFY``
NOTIFY pgrst, 'reload schema'
Debouncing
~~~~~~~~~~
PostgREST does not reload the schema cache for each notification when several ``NOTIFY pgrst`` events are generated quickly after one another.
There are two cases to consider: when notifications are sent within a single transaction and when they are sent across multiple transactions.
In the first case, PostgreSQL deduplicates identical ``NOTIFY`` events within the same transaction. This means that even if multiple ``NOTIFY pgrst`` statements are executed before a ``COMMIT``, only a single notification is delivered to PostgREST.
In the second case, when notifications are sent from separate transactions in a short time span, PostgREST applies a debouncing mechanism to avoid excessive schema cache reloads.
Instead of reloading the schema cache for each notification, events are grouped within a small time window of 100 milliseconds. The reload function is executed once immediately when the first notification is received and once more after the burst of events settles, resulting in at most two executions within that time window.
.. _auto_schema_reloading:
Automatic Schema Cache Reloading
+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"
+65
View File
@@ -50,6 +50,16 @@ let
# jailbreak, because hspec limit for tests
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 haskellPackages
configurator-pg =
prev.callHackageDirect
@@ -70,6 +80,61 @@ let
}
{ };
http2 =
prev.callHackageDirect
{
pkg = "http2";
ver = "5.4.0";
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE=";
}
{ };
http-semantics =
prev.callHackageDirect
{
pkg = "http-semantics";
ver = "0.4.0";
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0=";
}
{ };
jose-jwt =
prev.callHackageDirect
{
pkg = "jose-jwt";
ver = "0.9.6";
sha256 = "sha256-FhBz5wzyNrDvmjHWOeNAHuVMyJUVSlm+DeQQuITSjaI=";
}
{ };
time-manager =
prev.callHackageDirect
{
pkg = "time-manager";
ver = "0.2.4";
sha256 = "sha256-sAt/331YLQ2IU3z90aKYSq1nxoazv87irsuJp7ZG3pw=";
}
{ };
network-run =
prev.callHackageDirect
{
pkg = "network-run";
ver = "0.5.0";
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
}
{ };
warp =
lib.dontCheck
(prev.callHackageDirect
{
pkg = "warp";
ver = "3.4.14";
sha256 = "sha256-RnoOUlC6dOP0sK/tYAJCX1oLzVFG1GILUY+yVbmvW8Y=";
}
{ });
# Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.
hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;
+1
View File
@@ -82,6 +82,7 @@ let
}
''
${tests}/bin/postgrest-test-spec
${tests}/bin/postgrest-test-observability
${tests}/bin/postgrest-test-doctests
${tests}/bin/postgrest-test-io
${tests}/bin/postgrest-test-big-schema
+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' ' ')
+2 -2
View File
@@ -62,7 +62,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 +74,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
+25 -2
View File
@@ -10,6 +10,7 @@
, hostPlatform
, jq
, lib
, nginx
, postgrest
, python3
, runtimeShell
@@ -32,6 +33,20 @@ let
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
'';
testObservability =
checkedShellScript
{
name = "postgrest-test-observability";
docs = "Run the Haskell observability test suite.";
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
workingDir = "/";
withEnv = postgrest.env;
}
''
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
'';
testDoctests =
checkedShellScript
{
@@ -80,6 +95,7 @@ let
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
workingDir = "/";
withEnv = postgrest.env;
withPath = [ nginx ];
}
''
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
@@ -142,6 +158,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
@@ -155,7 +172,7 @@ let
rm -rf coverage/*
# build once before running all the tests
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:observability
(
trap 'echo Found dead code: Check file list above.' ERR ;
@@ -179,11 +196,16 @@ let
${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
HPCTIXFILE="$tmpdir"/observability.tix \
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
# collect all the tix files
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix \
"$tmpdir"/observability.tix
# prepare the overlay
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
@@ -250,6 +272,7 @@ buildToolbox
tools = {
inherit
testSpec
testObservability
testDoctests
testSpecIdempotence
testIO
+1 -1
View File
@@ -25,7 +25,7 @@ let
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])"
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])"
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
"ARG_USE_ENV([PGUSER], [Postgrest_Test_Authenticator], [Authenticator PG role])" # user is written in mixed case to implicitly test that it is being properly quoted in schema cache queries
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
+40 -6
View File
@@ -1,5 +1,5 @@
name: postgrest
version: 14.4
version: 14.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
@@ -100,7 +100,7 @@ library
, HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.5
, aeson >= 2.0.3 && < 2.3
, auto-update >= 0.1.4 && < 0.3
, auto-update >= 0.2.7 && < 0.3
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
, case-insensitive >= 1.2 && < 1.3
@@ -120,7 +120,7 @@ library
, http-client >= 0.7.19 && < 0.8
, http-types >= 0.12.2 && < 0.13
, insert-ordered-containers >= 0.2.2 && < 0.3
, jose-jwt >= 0.9.6 && < 0.11
, jose-jwt >= 0.9.6 && < 0.10
, lens >= 4.14 && < 5.4
, lens-aeson >= 1.0.1 && < 1.3
, mtl >= 2.2.2 && < 2.4
@@ -152,11 +152,12 @@ library
-- for unix sockets; this is tested in test/io/test_io.py. See
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
, wai-logger >= 2.4.0
, warp >= 3.3.19 && < 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
@@ -211,7 +212,6 @@ test-suite spec
Feature.Auth.AudienceJwtSecretSpec
Feature.Auth.AuthSpec
Feature.Auth.BinaryJwtSecretSpec
Feature.Auth.JwtCacheSpec
Feature.Auth.NoAnonSpec
Feature.Auth.NoJwtSecretSpec
Feature.ConcurrentSpec
@@ -273,7 +273,7 @@ test-suite spec
, hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.11
, jose-jwt >= 0.9.6 && < 0.10
, lens >= 4.14 && < 5.4
, lens-aeson >= 1.0.1 && < 1.3
, monad-control >= 1.0.1 && < 1.1
@@ -294,6 +294,40 @@ test-suite spec
-- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K
test-suite observability
type: exitcode-stdio-1.0
default-language: Haskell2010
default-extensions: OverloadedStrings
QuasiQuotes
NoImplicitPrelude
hs-source-dirs: test/observability
main-is: Main.hs
other-modules: ObsHelper
Observation.JwtCache
Observation.MetricsSpec
Observation.SchemaCacheSpec
build-depends: base >= 4.9 && < 4.20
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
, hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && < 1.2
, hspec >= 2.3 && < 2.12
, hspec-expectations >= 0.8.4 && < 0.9
, hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.10
, postgrest
, prometheus-client >= 1.1.1 && < 1.2.0
, 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
-fno-spec-constr -optP-Wno-nonportable-include-path
-fwrite-ide-info
-- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K
test-suite doctests
type: exitcode-stdio-1.0
default-language: Haskell2010
+22 -11
View File
@@ -11,7 +11,8 @@ import Control.Monad.Extra (whenJust)
import Network.Socket hiding (addrFamily)
import Network.Socket.ByteString
import PostgREST.AppState (AppState)
import PostgREST.AppState (AppState, getConfig, getMainThreadId)
import PostgREST.Config (AppConfig (..))
import PostgREST.MediaType (MediaType (..), toContentType)
import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveSocketToAddress)
@@ -19,22 +20,32 @@ import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState
import Protolude
import qualified Network.Socket as NS
import Protolude
runAdmin :: AppState -> Warp.Settings -> IO ()
runAdmin appState settings = do
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
runAdmin appState maybeAdminSocket socketREST 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
adminApp = admin appState socketREST
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
killThread (getMainThreadId appState) -- Admin server crash is deemed unrecoverable, so we kill postgrest
-- | PostgREST admin application
admin :: AppState.AppState -> Wai.Application
admin appState req respond = do
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
admin :: AppState.AppState -> NS.Socket -> Wai.Application
admin appState socketREST req respond = do
isMainAppReachable <- isRight <$> reachMainApp socketREST
isLoaded <- AppState.isLoaded appState
isPending <- AppState.isPending appState
+77 -11
View File
@@ -9,18 +9,24 @@ Some of its functionality includes:
- Producing HTTP Headers according to RFCs.
- Content Negotiation
-}
{-# LANGUAGE RecordWildCards #-}
{-# 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 Data.Either.Combinators (mapLeft, whenLeft)
import Data.Maybe (fromJust)
import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
import Network.Wai.Handler.Warp (defaultSettings, setHost,
setOnException, setPort,
setServerName)
import qualified Data.Text.Encoding as T
@@ -54,32 +60,55 @@ 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 qualified Network.HTTP.Types as HTTP
import Protolude hiding (Handler)
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.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket)
import Protolude hiding (Handler)
type Handler = ExceptT Error
run :: AppState -> IO ()
run appState = do
let observer = AppState.getObserver appState
conf@AppConfig{..} <- AppState.getConfig appState
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
(mainSocket, adminSocket) <- initSockets conf
Unix.installSignalHandlers observer (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Listener.runListener appState
Admin.runAdmin appState (serverSettings conf)
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
do
address <- resolveSocketToAddress (AppState.getSocketREST appState)
address <- resolveSocketToAddress mainSocket
observer $ AppServerAddressObs address
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
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{..} =
@@ -206,3 +235,40 @@ addRetryHint delay response = do
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)
+13 -64
View File
@@ -13,11 +13,9 @@ module PostgREST.AppState
, getNextListenerDelay
, getTime
, getJwtCacheState
, getSocketREST
, getSocketAdmin
, init
, initSockets
, initWithPool
, putConfig -- For tests TODO refactoring
, putNextListenerDelay
, putSchemaCache
, putPgVersion
@@ -32,13 +30,11 @@ module PostgREST.AppState
import qualified Data.ByteString.Char8 as BS
import Data.Either.Combinators (whenLeft)
import qualified Data.Text as T (unpack)
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 Network.Socket as NS
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger
@@ -70,10 +66,7 @@ import PostgREST.SchemaCache (SchemaCache (..),
querySchemaCache,
showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi)
import PostgREST.Unix (createAndBindDomainSocket)
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Data.String (IsString (..))
import Protolude
data AppState = AppState
@@ -99,10 +92,6 @@ data AppState = AppState
, stateNextDelay :: IORef Int
-- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int
-- | Network socket for REST API
, stateSocketREST :: NS.Socket
-- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket
-- | Observation handler
, stateObserver :: ObservationHandler
-- | JWT Cache
@@ -117,8 +106,6 @@ data SchemaCacheStatus
| SCPending
deriving Eq
type AppSockets = (NS.Socket, Maybe NS.Socket)
init :: AppConfig -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
loggerState <- Logger.init
@@ -128,12 +115,10 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
observer $ AppStartObs prettyVersion
pool <- initPool conf observer
(sock, adminSock) <- initSockets conf
state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool pool conf loggerState metricsState observer --{ stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool pool conf loggerState metricsState observer = do
appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -146,8 +131,6 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
<*> myThreadId
<*> newIORef 0
<*> newIORef 1
<*> pure sock
<*> pure adminSock
<*> pure observer
<*> JwtCache.init conf observer
<*> pure loggerState
@@ -166,40 +149,6 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
destroy :: AppState -> IO ()
destroy = destroyPool
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)
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool AppConfig{..} observer = do
SQL.acquire $ SQL.settings
@@ -270,10 +219,14 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
-- | 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{..} = SQL.release statePool
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
@@ -313,12 +266,6 @@ getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache
getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST
getSocketAdmin :: AppState -> Maybe NS.Socket
getSocketAdmin = stateSocketAdmin
getMainThreadId :: AppState -> ThreadId
getMainThreadId = stateMainThreadId
@@ -366,8 +313,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer $ ConnectionRetryObs delay
putNextListenerDelay appState delay
flushPool appState
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
)
where
@@ -416,6 +361,10 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
putSCacheStatus appState SCPending
putSchemaCache appState $ Just 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
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
observer $ SchemaCacheLoadedObs t
-4
View File
@@ -119,8 +119,6 @@ data AppConfig = AppConfig
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCQuerySleep :: Maybe Int32
, configInternalSCLoadSleep :: Maybe Int32
, configInternalSCRelLoadSleep :: Maybe Int32
}
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
@@ -304,8 +302,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> pure roleSettings
<*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-query-sleep"
<*> optInt "internal-schema-cache-load-sleep"
<*> optInt "internal-schema-cache-relationship-load-sleep"
where
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
+7 -6
View File
@@ -16,6 +16,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
@@ -32,8 +33,8 @@ 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
@@ -101,7 +102,7 @@ queryDbSettings preConfFunc prepared =
SELECT setdatabase as database,
unnest(setconfig) as setting
FROM pg_catalog.pg_db_role_setting
WHERE setrole = CURRENT_USER::regrole::oid
WHERE setrole = quote_ident(CURRENT_USER)::regrole::oid
AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))
),
kv_settings AS (
@@ -142,13 +143,13 @@ queryRoleSettings pgVer prepared =
select r.rolname, unnest(r.rolconfig) as setting
from pg_auth_members m
join pg_roles r on r.oid = m.roleid
where member = current_user::regrole::oid
where member = quote_ident(current_user)::regrole::oid
),
kv_settings AS (
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 (
@@ -167,7 +168,7 @@ queryRoleSettings pgVer prepared =
|]
hasParameterPrivilege
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
| pgVer >= pgVersion150 = "or has_parameter_privilege(quote_ident(current_user)::regrole::oid, ps.name, 'set')"
| otherwise = ""
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
+19 -5
View File
@@ -386,7 +386,7 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
-- Just "Perhaps you meant to call the function api.test"
--
-- >>> noRpcHint "api" "other" [] procs []
-- Just "Perhaps you meant to call the function api.another"
-- Nothing
--
-- >>> noRpcHint "api" "noclosealternative" [] procs []
-- Nothing
@@ -423,8 +423,8 @@ noRpcHint schema procName params allProcs overloadedProcs =
-- E.g. ["val", "param", "name"] into "(name, param, val)"
listToText = ("(" <>) . (<> ")") . T.intercalate ", " . sort
possibleProcs
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
| null overloadedProcs = getFuzzyHint HintProcedure fuzzySetOfProcs procName
| otherwise = (procName <>) <$> getFuzzyHint HintParams fuzzySetOfParams (listToText params)
-- |
-- Do a fuzzy search in all tables in the same schema and return closest result
@@ -432,7 +432,21 @@ tableNotFoundHint :: Text -> Text -> SchemaCache -> Maybe Text
tableNotFoundHint schema tblName SchemaCache{dbTablesFuzzyIndex}
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
where
perhapsTable = (`Fuzzy.getOne` tblName) =<< HM.lookup schema dbTablesFuzzyIndex
perhapsTable = (\fuzzySet -> getFuzzyHint HintTable fuzzySet tblName) =<< HM.lookup schema dbTablesFuzzyIndex
data HintType
= HintTable
| HintProcedure
| HintParams
-- | Get hint using Fuzzy Search with at least 0.75 similarity score
getFuzzyHint :: HintType -> Fuzzy.FuzzySet -> Text -> Maybe Text
getFuzzyHint hintType =
let minScore = 0.75 :: Double -- used for table and procedure name hints
in case hintType of
HintTable -> Fuzzy.getOneWithMinScore minScore
HintProcedure -> Fuzzy.getOneWithMinScore minScore
HintParams -> Fuzzy.getOne -- For params, we stick to `getOne` which defaults to 0.33 min score, not a security risk to reveal params
compressedRel :: Relationship -> JSON.Value
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
@@ -527,7 +541,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.QueryError _ _ e)) = message e
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
+27 -7
View File
@@ -16,26 +16,35 @@ import PostgREST.Version (prettyVersion)
import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config
import Data.Either.Combinators (whenRight)
import Protolude
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
void . forkIO . void $ retryingListen appState False
-- | 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
retryingListen :: AppState -> Bool -> IO Void
retryingListen appState hasDbListenerBug = do
AppConfig{..} <- AppState.getConfig appState
let
dbChannel = toS configDbChannel
onError err = do
AppState.putIsListenerOn appState False
observer $ DBListenFail dbChannel (Right err)
when (isDbListenerBug err) $
observer DBListenBugCallQueryFix
unless configDbPoolAutomaticRecovery $
killThread mainThreadId
@@ -46,7 +55,7 @@ retryingListen appState = do
unless (delay == maxDelay) $
AppState.putNextListenerDelay appState (delay * 2)
-- loop running the listener
retryingListen appState
retryingListen appState (isDbListenerBug err)
-- Execute the listener with with error handling
handle onError $ do
@@ -59,7 +68,11 @@ retryingListen appState = do
-- use connection
\case
Right db -> do
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
SQL.listen db $ SQL.toPgIdentifier dbChannel
AppState.putIsListenerOn appState True
delay <- AppState.getNextListenerDelay appState
@@ -69,7 +82,7 @@ retryingListen appState = do
-- reset the delay
AppState.putNextListenerDelay appState 1
observer $ DBListenStart dbChannel
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
@@ -94,3 +107,10 @@ retryingListen appState = do
AppState.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();"
+131 -1
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module : PostgREST.Logger
@@ -35,7 +36,14 @@ import PostgREST.Config (LogLevel (..))
import PostgREST.Observation
import PostgREST.Query (MainQuery (..))
import Protolude
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
import qualified Hasql.Connection as SQL
import qualified Hasql.Pool.Observation as SQL
import Numeric (showFFloat)
import PostgREST.Config.PgVersion (pgvName)
import qualified PostgREST.Error as Error
import Protolude
data LoggerState = LoggerState
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
@@ -110,12 +118,18 @@ observationLogger loggerState logLevel obs = case obs of
o@PoolRequestFullfilled ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@PoolFlushed ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@JwtCacheEviction ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@(JwtCacheLookup _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@(WarpServerObs _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o ->
logWithZTime loggerState $ observationMessage o
@@ -140,3 +154,119 @@ renderSnippet snippet =
prepared = False -- unused
in
sql
observationMessage :: Observation -> Text
observationMessage = \case
AdminStartObs address ->
"Admin server listening on " <> address
AdminServerCrashedObs ex ->
"Admin server crashed unexpectedly: " <> (showOnSingleLine '\t' . show) ex
AppStartObs ver ->
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerAddressObs address ->
"API server listening on " <> address
DBConnectedObs ver ->
"Successfully connected to " <> ver
ExitUnsupportedPgVersion pgVer minPgVer ->
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
ExitDBNoRecoveryObs ->
"Automatic recovery disabled, exiting."
ExitDBFatalError ServerAuthError usageErr ->
"Failed to establish a connection. " <> jsonMessage usageErr
ExitDBFatalError ServerPgrstBug usageErr ->
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
ExitDBFatalError ServerError42P05 usageErr ->
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
ExitDBFatalError ServerError08P01 usageErr ->
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
SchemaCacheEmptyObs ->
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
"Failed to load the schema cache using "
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
<> " and "
<> "db-extra-search-path=" <> T.intercalate "," extraPaths <> ". " <> jsonMessage usageErr
SchemaCacheQueriedObs resultTime ->
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
SchemaCacheSummaryObs summary ->
"Schema cache loaded " <> summary
SchemaCacheLoadedObs resultTime ->
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
ConnectionRetryObs delay ->
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
QueryPgVersionError usageErr ->
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
DBListenStart host port fullName channel -> do
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
DBListenFail channel listenErr ->
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
either showListenerConnError showListenerException listenErr
DBListenRetry delay ->
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
DBListenBugCallQueryFix ->
"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 ->
"Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel"
DBListenerConnectionCleanupFail ex ->
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
QueryObs{} ->
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
ConfigReadErrorObs usageErr ->
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr ->
"Failed to query the role settings. " <> jsonMessage usageErr
QueryErrorCodeHighObs usageErr ->
jsonMessage usageErr
ConfigInvalidObs err ->
"Failed reloading config: " <> err
ConfigSucceededObs ->
"Config reloaded"
PoolInit poolSize ->
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
PoolAcqTimeoutObs usageErr ->
jsonMessage usageErr
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
"Connection " <> show uuid <> (
case status of
SQL.ConnectingConnectionStatus -> " is being established"
SQL.ReadyForUseConnectionStatus -> " is available"
SQL.InUseConnectionStatus -> " is used"
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
SQL.AgingConnectionTerminationReason -> "max lifetime"
SQL.IdlenessConnectionTerminationReason -> "max idletime"
SQL.ReleaseConnectionTerminationReason -> "release"
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
)
PoolRequest ->
"Trying to borrow a connection from pool"
PoolRequestFullfilled ->
"Borrowed a connection from the pool"
PoolFlushed ->
"Database connection pool flushed"
JwtCacheLookup _ ->
"Looked up a JWT in JWT cache"
JwtCacheEviction ->
"Evicted entry from JWT cache"
TerminationUnixSignalObs signal ->
"Received termination unix signal " <> signal
WarpServerObs txt ->
"Warp server: " <> txt
where
showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) x ""
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
showListenerConnError :: SQL.ConnectionError -> Text
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
showListenerException :: SomeException -> Text
showListenerException = showOnSingleLine '\t' . show
showOnSingleLine :: Char -> Text -> Text
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
+53 -11
View File
@@ -5,7 +5,10 @@ Description : Metrics based on the Observation module. See Observation.hs.
-}
module PostgREST.Metrics
( init
, ConnTrack
, ConnStats (..)
, MetricsState (..)
, connectionCounts
, observationMetrics
, metricsToText
) where
@@ -17,12 +20,18 @@ import Prometheus
import PostgREST.Observation
import Protolude
import Control.Arrow ((&&&))
import Data.Bitraversable (bisequenceA)
import Data.Tuple.Extra (both)
import Data.UUID (UUID)
import qualified Focus
import Protolude
import qualified StmHamt.SizedHamt as SH
data MetricsState =
MetricsState {
poolTimeouts :: Counter,
poolAvailable :: Gauge,
connTrack :: ConnTrack,
poolWaiting :: Gauge,
poolMaxSize :: Gauge,
schemaCacheLoads :: Vector Label1 Counter,
@@ -36,7 +45,7 @@ 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 (Metric ((identity &&& dbPoolAvailable) <$> connectionTracker)) <*>
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")) <*>
@@ -46,20 +55,28 @@ init configDbPoolSize = do
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
pure metricState
where
dbPoolAvailable = (pure . noLabelsGroup (Info "pgrst_db_pool_available" "Available connections in the pool") GaugeType . calcAvailable <$>) . connectionCounts
where
calcAvailable = liftA2 (-) connected inUse
toSample name labels = Sample name labels . encodeUtf8 . show
noLabelsGroup info sampleType = SampleGroup info sampleType . pure . toSample (metricName info) mempty
-- 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 ()
-- Handle pool observations with connection tracking
-- this is necessary because it is not possible
-- to accurately maintain open/in use conneciton counts
-- statelessly based only on pool observation events.
-- The reason is that hasql-pool emits TerminatedConnectionStatus
-- both for connections successfully established and failed when connecting.
-- When receiving TerminatedConnectionStatus we have to find out
-- if we can decrement established connection count. To do that we have to track
-- established connections.
(HasqlPoolObs sqlObs) -> trackConnections connTrack sqlObs
PoolRequest ->
incGauge poolWaiting
PoolRequestFullfilled ->
@@ -77,3 +94,28 @@ observationMetrics MetricsState{..} obs = case obs of
metricsToText :: IO LBS.ByteString
metricsToText = exportMetricsAsText
data ConnStats = ConnStats {
connected :: Int,
inUse :: Int
} deriving (Eq, Show)
data ConnTrack = ConnTrack { connTrackConnected :: SH.SizedHamt UUID, connTrackInUse :: SH.SizedHamt UUID }
connectionTracker :: IO ConnTrack
connectionTracker = ConnTrack <$> SH.newIO <*> SH.newIO
trackConnections :: ConnTrack -> SQL.Observation -> IO ()
trackConnections ConnTrack{..} (SQL.ConnectionObservation uuid status) = case status of
SQL.ReadyForUseConnectionStatus -> atomically $
SH.insert identity uuid connTrackConnected *>
SH.focus Focus.delete identity uuid connTrackInUse
SQL.TerminatedConnectionStatus _ -> atomically $
SH.focus Focus.delete identity uuid connTrackConnected *>
SH.focus Focus.delete identity uuid connTrackInUse
SQL.InUseConnectionStatus -> atomically $
SH.insert identity uuid connTrackInUse
_ -> mempty
connectionCounts :: ConnTrack -> IO ConnStats
connectionCounts = atomically . fmap (uncurry ConnStats) . bisequenceA . both SH.size . (connTrackConnected &&& connTrackInUse)
+8 -116
View File
@@ -1,4 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveGeneric #-}
{-|
Module : PostgREST.Observation
Description : This module holds an Observation type which is the core of Observability for PostgREST.
@@ -9,28 +9,21 @@ Description : This module holds an Observation type which is the core of Observa
module PostgREST.Observation
( Observation(..)
, ObsFatalError(..)
, observationMessage
, ObservationHandler
, showOnSingleLine
) where
import qualified Data.ByteString.Lazy as LBS
import Data.List.NonEmpty (toList)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Hasql.Connection as SQL
import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Observation as SQL
import Network.HTTP.Types.Status (Status)
import Numeric (showFFloat)
import PostgREST.Config.PgVersion
import qualified PostgREST.Error as Error
import PostgREST.Query (MainQuery)
import Protolude hiding (toList)
data Observation
= AdminStartObs Text
| AdminServerCrashedObs SomeException
| AppStartObs ByteString
| AppServerAddressObs Text
| ExitUnsupportedPgVersion PgVersion PgVersion
@@ -43,9 +36,10 @@ data Observation
| SchemaCacheSummaryObs Text
| SchemaCacheLoadedObs Double
| ConnectionRetryObs Int
| DBListenStart Text
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
| DBListenFail Text (Either SQL.ConnectionError SomeException)
| DBListenRetry Int
| DBListenBugCallQueryFix
| DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString
| DBListenerConnectionCleanupFail SomeException
@@ -61,115 +55,13 @@ data Observation
| HasqlPoolObs SQL.Observation
| PoolRequest
| PoolRequestFullfilled
| PoolFlushed
| JwtCacheLookup Bool
| JwtCacheEviction
| TerminationUnixSignalObs Text
| WarpServerObs Text
deriving (Generic)
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
type ObservationHandler = Observation -> IO ()
observationMessage :: Observation -> Text
observationMessage = \case
AdminStartObs address ->
"Admin server listening on " <> address
AppStartObs ver ->
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerAddressObs address ->
"API server listening on " <> address
DBConnectedObs ver ->
"Successfully connected to " <> ver
ExitUnsupportedPgVersion pgVer minPgVer ->
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
ExitDBNoRecoveryObs ->
"Automatic recovery disabled, exiting."
ExitDBFatalError ServerAuthError usageErr ->
"Failed to establish a connection. " <> jsonMessage usageErr
ExitDBFatalError ServerPgrstBug usageErr ->
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
ExitDBFatalError ServerError42P05 usageErr ->
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
ExitDBFatalError ServerError08P01 usageErr ->
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
SchemaCacheEmptyObs ->
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
"Failed to load the schema cache using "
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
<> " and "
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
<> ". " <> jsonMessage usageErr
SchemaCacheQueriedObs resultTime ->
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
SchemaCacheSummaryObs summary ->
"Schema cache loaded " <> summary
SchemaCacheLoadedObs resultTime ->
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
ConnectionRetryObs delay ->
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
QueryPgVersionError usageErr ->
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
DBListenStart channel -> do
"Listening for database notifications on the " <> show channel <> " channel"
DBListenFail channel listenErr ->
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
either showListenerConnError showListenerException listenErr
DBListenRetry delay ->
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
DBListenerGotSCacheMsg channel ->
"Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel"
DBListenerConnectionCleanupFail ex ->
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
QueryObs{} ->
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
ConfigReadErrorObs usageErr ->
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr ->
"Failed to query the role settings. " <> jsonMessage usageErr
QueryErrorCodeHighObs usageErr ->
jsonMessage usageErr
ConfigInvalidObs err ->
"Failed reloading config: " <> err
ConfigSucceededObs ->
"Config reloaded"
PoolInit poolSize ->
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
PoolAcqTimeoutObs usageErr ->
jsonMessage usageErr
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
"Connection " <> show uuid <> (
case status of
SQL.ConnectingConnectionStatus -> " is being established"
SQL.ReadyForUseConnectionStatus -> " is available"
SQL.InUseConnectionStatus -> " is used"
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
SQL.AgingConnectionTerminationReason -> "max lifetime"
SQL.IdlenessConnectionTerminationReason -> "max idletime"
SQL.ReleaseConnectionTerminationReason -> "release"
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
)
PoolRequest ->
"Trying to borrow a connection from pool"
PoolRequestFullfilled ->
"Borrowed a connection from the pool"
JwtCacheLookup _ ->
"Looked up a JWT in JWT cache"
JwtCacheEviction ->
"Evicted entry from JWT cache"
where
showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) x ""
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
showListenerConnError :: SQL.ConnectionError -> Text
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
showListenerException :: SomeException -> Text
showListenerException = showOnSingleLine '\t' . show
showOnSingleLine :: Char -> Text -> Text
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
+10 -4
View File
@@ -73,6 +73,12 @@ toSwaggerType colType = case T.takeEnd 2 colType of
"[]" -> Just SwaggerArray
_ -> Just SwaggerString
toSwaggerFormat :: Text -> Maybe Text
toSwaggerFormat "smallint" = Just "int32"
toSwaggerFormat "integer" = Just "int32"
toSwaggerFormat "bigint" = Just "int64"
toSwaggerFormat colType = Just colType
typeFromArray :: Text -> Text
typeFromArray = T.dropEnd 2
@@ -141,7 +147,7 @@ makeProperty tbl rels col = (colName col, Inline s)
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
& description .~ d
& enum_ .~ e
& format ?~ colType col
& format .~ toSwaggerFormat (colType col)
& maxLength .~ (fromIntegral <$> colMaxLen col)
& type_ .~ toSwaggerType (colType col)
& items .~ (SwaggerItemsObject <$> makePropertyItems (colType col))
@@ -160,7 +166,7 @@ makeProcProperty (RoutineParam n t _ _ _) = (n, Inline s)
s = (mempty :: Schema)
& type_ .~ toSwaggerType t
& items .~ (SwaggerItemsObject <$> makePropertyItems t)
& format ?~ t
& format .~ toSwaggerFormat t
makePreferParam :: [Text] -> Param
makePreferParam ts =
@@ -192,14 +198,14 @@ makeProcGetParam (RoutineParam n t _ r v) =
baseSchema = (mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
schemaNotMulti = baseSchema
& format ?~ t
& format .~ toSwaggerFormat t
& type_ ?~ toParamType (toSwaggerType t)
schemaMulti = baseSchema
& type_ ?~ fromMaybe SwaggerString (toSwaggerType t)
& items ?~ SwaggerItemsPrimitive (Just CollectionMulti)
((mempty :: ParamSchema x)
& type_ .~ toSwaggerTypeFromArray t
& format ?~ typeFromArray t)
& format .~ toSwaggerFormat (typeFromArray t))
toParamType paramType = case paramType of
-- Array uses {} in query params
Just SwaggerArray -> SwaggerString
+6 -10
View File
@@ -67,10 +67,9 @@ import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
import qualified PostgREST.MediaType as MediaType
import Control.Arrow ((&&&))
import qualified Data.FuzzySet as Fuzzy
import Control.Arrow ((&&&))
import qualified Data.FuzzySet as Fuzzy
import Protolude
import System.IO.Unsafe (unsafePerformIO)
type TablesFuzzyIndex = HM.HashMap Schema Fuzzy.FuzzySet
@@ -102,7 +101,7 @@ showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =
T.intercalate ", "
[ show (HM.size tbls) <> " Relations"
, show (HM.size rels) <> " Relationships"
, show (HM.size routs) <> " Functions"
, show (HM.size routs) <> " RPCs"
, show (HM.size reps) <> " Domain Representations"
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
, show (S.size tzs) <> " Timezones"
@@ -167,11 +166,9 @@ querySchemaCache conf@AppConfig{..} = do
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
rels = addInverseRels $ addM2MRels tabsWViewsPks $ addViewM2OAndO2ORels keyDeps m2oRels
-- Add delay in loading schema cache when internal-schema-cache-load-sleep config is set
return $ delayEval configInternalSCLoadSleep $ removeInternal schemas $ SchemaCache {
return $ removeInternal schemas $ SchemaCache {
dbTables = tabsWViewsPks
-- Add delay in loading relationships when internal-schema-cache-relationship-load-sleep config is set
, dbRelationships = delayEval configInternalSCRelLoadSleep $ getOverrideRelationshipsMap rels cRels
, dbRelationships = getOverrideRelationshipsMap rels cRels
, dbRoutines = funcs
, dbRepresentations = reps
, dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones
@@ -185,7 +182,6 @@ querySchemaCache conf@AppConfig{..} = do
where
schemas = toList configDbSchemas
prepared = configDbPreparedStatements
delayEval confDelay result = maybe result (unsafePerformIO . (($> result) . (threadDelay . (1000 *) . fromIntegral))) confDelay
-- | overrides detected relationships with the computed relationships and gets the RelationshipsMap
getOverrideRelationshipsMap :: [Relationship] -> [Relationship] -> RelationshipsMap
@@ -452,7 +448,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
bt.oid <> bt.base_type as rettype_is_composite_alias,
p.provolatile,
p.provariadic > 0 as hasvariadic,
lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,
(regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1] AS transaction_isolation_level,
coalesce(func_settings.kvs, '{}') as kvs
FROM pg_proc p
LEFT JOIN arguments a ON a.oid = p.oid
+10 -9
View File
@@ -11,26 +11,27 @@ import qualified System.Posix.Signals as Signals
import System.Posix.Types (FileMode)
import System.PosixCompat.Files (setFileMode)
import Data.String (String)
import qualified Network.Socket as NS
import Data.String (String)
import qualified Network.Socket as NS
import qualified PostgREST.Observation as Observation
import Protolude
import System.Directory (removeFile)
import System.IO.Error (isDoesNotExistError)
import System.Directory (removeFile)
import System.IO.Error (isDoesNotExistError)
-- | Set signal handlers, only for systems with signals
installSignalHandlers :: ThreadId -> IO () -> IO () -> IO ()
installSignalHandlers :: Observation.ObservationHandler -> ThreadId -> IO () -> IO () -> IO ()
#ifndef mingw32_HOST_OS
installSignalHandlers tid usr1 usr2 = do
installSignalHandlers observer tid usr1 usr2 = do
let interrupt = throwTo tid UserInterrupt
install Signals.sigINT interrupt
install Signals.sigTERM interrupt
install Signals.sigINT $ observer (Observation.TerminationUnixSignalObs "SIGINT") >> interrupt
install Signals.sigTERM $ observer (Observation.TerminationUnixSignalObs "SIGTERM") >> interrupt
install Signals.sigUSR1 usr1
install Signals.sigUSR2 usr2
where
install signal handler =
void $ Signals.installHandler signal (Signals.Catch handler) Nothing
#else
installSignalHandlers _ _ _ = pass
installSignalHandlers _ _ _ _ = pass
#endif
-- | Create a unix domain socket and bind it to the given path.
+7 -1
View File
@@ -9,9 +9,15 @@ nix:
pure: false
extra-deps:
- auto-update-0.2.7
- configurator-pg-0.2.11
- fuzzyset-0.2.4
- hasql-pool-1.0.1
- jose-jwt-0.10.0
- http-semantics-0.4.0
- http2-5.4.0
- jose-jwt-0.9.6
- network-control-0.1.7
- postgresql-libpq-0.10.1.0
- streaming-commons-0.2.3.1
- time-manager-0.3.2
- warp-3.4.14
+46 -4
View File
@@ -4,6 +4,13 @@
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages:
- completed:
hackage: auto-update-0.2.7@sha256:32ca6ce351604a17ee79e4086939f798f958f8407478288e9adb7adeb194c6ea,1670
pantry-tree:
sha256: 641faa7d5ee516195ecd4b538f170722d56f84e3bc5714c6bbe8123849b49983
size: 1105
original:
hackage: auto-update-0.2.7
- completed:
hackage: configurator-pg-0.2.11@sha256:de0c56386591e85159436b0af04a8f15a4f4e156354e99709676c2c2ee959505,2850
pantry-tree:
@@ -26,12 +33,33 @@ packages:
original:
hackage: hasql-pool-1.0.1
- completed:
hackage: jose-jwt-0.10.0@sha256:6ed175a01c721e317ceea15eb251a81de145c03711a977517935633a5cdec1d4,3546
hackage: http-semantics-0.4.0@sha256:da8a98d542b2032cc12590847179577b0208a52bb3b9aa9a07c08d27d2a1714c,1513
pantry-tree:
sha256: 58649e68e2d1adb47d8ed8741bd27ac23a2f19e3ee62bc28a68ac8642b3e0858
size: 1231
sha256: d0e08875907c0fbff71813747fd93ce7bfd4e3d4a6b979df5c137430a9130f1d
size: 1188
original:
hackage: jose-jwt-0.10.0
hackage: http-semantics-0.4.0
- completed:
hackage: http2-5.4.0@sha256:1e9f6f5f32bfb3176136f35e041aa279bc456e81d4674ddaaeaa7c0d091be0c7,10624
pantry-tree:
sha256: 5c89815392d85d854efe75cf2279f58ff9f5e4b8fc1f83c55de93191edd128da
size: 44864
original:
hackage: http2-5.4.0
- completed:
hackage: jose-jwt-0.9.6@sha256:cc234805da58fc75bc4c11af3db2dabf920f2c7d5d8b9a2b73bdb7c024b8d087,3557
pantry-tree:
sha256: 0bcaa403f0d6f3f7ad993d4d9e5d8f3dfd7e88c4e5e64b0219d7a1a03cb31e31
size: 1288
original:
hackage: jose-jwt-0.9.6
- completed:
hackage: network-control-0.1.7@sha256:bfe3318c5cf6573dd585b126ec1197eae291a9a6df178006bf19da5c8aa14d68,1255
pantry-tree:
sha256: 6deb24c404f6e592be1f391f04c97f66169f97cacb0dd37403a6aa1e781846ef
size: 619
original:
hackage: network-control-0.1.7
- completed:
hackage: postgresql-libpq-0.10.1.0@sha256:6b580c9d5068e78eecc13e655b2885c8e79cdacfca513c5d1e5a6b9dc61d9758,3166
pantry-tree:
@@ -46,6 +74,20 @@ packages:
size: 2374
original:
hackage: streaming-commons-0.2.3.1
- completed:
hackage: time-manager-0.3.2@sha256:74c16026c8592802d8a1cd9510c0223dad247b2a9ca791fa0153d243b56cc09e,1290
pantry-tree:
sha256: f282e2630df833732bde29944d4b0ac0d86399bcce80c1a3d523c2a5651b9e49
size: 461
original:
hackage: time-manager-0.3.2
- completed:
hackage: warp-3.4.14@sha256:66b82af637f79ae4d39f7373b39491067f6d059b2581cb4caf18760f1d82e686,10066
pantry-tree:
sha256: 6db2e6d37acebd24d4e73d506481a4a2e0c53b35099756131a3852c8c8b84b45
size: 4175
original:
hackage: warp-3.4.14
snapshots:
- completed:
sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9
+7
View File
@@ -0,0 +1,7 @@
<svg width="1024" height="220" viewBox="0 0 1024 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M267.945 167.731H314.905C342.254 167.731 359.527 154.103 359.527 133.123C359.527 117.522 352.69 106.404 332.178 101.563C347.652 95.8246 352.51 87.2173 352.51 75.203C352.51 55.6573 337.756 42.2085 310.407 42.2085H267.945V167.731ZM293.134 145.137V114.474H312.566C326.241 114.474 333.978 120.032 333.978 129.178C333.978 140.116 326.241 145.137 312.566 145.137H293.134ZM293.134 94.0314V64.6232H308.068C321.743 64.6232 326.96 70.5407 326.96 79.148C326.96 89.1898 321.743 94.0314 308.068 94.0314H293.134ZM378.999 205.208C400.95 205.208 412.645 192.477 420.562 173.649L461.945 74.4857H433.157L414.984 127.743C413.365 132.226 411.925 137.785 411.026 142.627C410.126 137.785 408.687 132.226 407.067 127.743L388.895 74.4857H359.567L396.632 163.427L395.372 167.372C392.673 175.621 386.736 181.539 377.38 181.539C373.421 181.539 370.003 181.18 366.404 180.283L362.806 203.415C367.664 204.85 374.141 205.208 378.999 205.208Z" fill="white"/>
<path d="M509.624 167.881H522.918V146.401H512.858C508.187 146.401 505.312 143.716 505.312 137.809V95.3848H525.074V74.7993H505.312V53.4979L481.059 56.0039V74.7993H467.945V95.3848H481.059V139.062C481.059 159.289 491.12 167.881 509.624 167.881ZM575.459 170.208C593.963 170.208 609.592 161.974 617.138 150.339L598.274 136.198C593.963 143.358 585.878 148.728 575.459 148.728C563.242 148.728 554.978 141.031 552.283 130.112H622.168C622.707 127.069 622.887 124.204 622.887 121.519C622.887 94.6688 603.484 72.4723 575.459 72.4723C547.433 72.4723 527.132 94.6688 527.132 121.519C527.132 148.37 546.535 170.208 575.459 170.208ZM552.283 112.032C554.978 100.934 563.602 93.2368 575.099 93.2368C586.956 93.2368 595.759 101.65 598.454 112.032H552.283ZM687.249 170.208C712.939 170.208 733.42 148.549 733.42 121.698C733.42 94.8478 712.939 72.8303 687.249 72.8303C671.26 72.8303 662.817 79.2744 657.786 89.1197V37.2085H633.533V167.881H657.068V152.666C662.098 163.227 670.542 170.208 687.249 170.208ZM657.068 121.698C657.068 107.199 667.308 94.6688 683.297 94.6688C698.747 94.6688 709.167 107.02 709.167 121.519C709.167 135.661 698.747 148.37 683.297 148.37C667.308 148.37 657.068 136.198 657.068 121.698ZM785.937 170.208C802.645 170.208 811.088 163.227 816.119 152.666V167.881H839.653V74.7993H815.58V89.4777C810.549 79.4534 802.106 72.8303 785.937 72.8303C760.247 72.8303 739.767 94.8478 739.767 121.698C739.767 148.549 760.247 170.208 785.937 170.208ZM764.02 121.519C764.02 107.02 774.439 94.6688 789.889 94.6688C805.878 94.6688 816.119 107.199 816.119 121.698C816.119 136.198 805.878 148.37 789.889 148.37C774.439 148.37 764.02 135.661 764.02 121.519ZM886.04 170.208C906.7 170.208 922.33 157.32 922.33 140.136C922.33 126.173 916.042 116.149 888.915 109.168C877.776 106.304 876.339 103.44 876.339 100.218C876.339 94.4898 881.369 91.9837 888.016 91.9837C894.304 91.9837 900.053 94.8478 904.544 102.008L922.15 88.5827C912.629 76.2314 902.568 72.4723 887.477 72.4723C865.56 72.4723 852.625 84.6446 852.625 100.934C852.625 112.748 856.398 124.025 881.369 129.933C896.101 133.334 898.436 136.019 898.436 140.852C898.436 146.401 893.765 150.518 885.501 150.518C877.597 150.518 870.411 146.759 863.584 139.062L847.235 153.919C857.476 164.838 870.231 170.208 886.04 170.208ZM976.517 170.208C995.021 170.208 1010.65 161.974 1018.2 150.339L999.332 136.198C995.021 143.358 986.936 148.728 976.517 148.728C964.3 148.728 956.036 141.031 953.342 130.112H1023.23C1023.77 127.069 1023.94 124.204 1023.94 121.519C1023.94 94.6688 1004.54 72.4723 976.517 72.4723C948.491 72.4723 928.19 94.6688 928.19 121.519C928.19 148.37 947.593 170.208 976.517 170.208ZM953.342 112.032C956.036 100.934 964.66 93.2368 976.157 93.2368C988.014 93.2368 996.817 101.65 999.512 112.032H953.342Z" fill="white"/>
<path d="M63.3182 209.286C40.9935 198.863 22.8237 181.221 11.7365 159.201C0.649395 137.179 -2.70929 112.063 2.20168 87.8978C7.11264 63.733 20.0073 41.927 38.8077 25.9936C57.6082 10.0604 81.2196 0.927572 105.837 0.0669197C130.455 -0.793732 154.645 6.66803 174.511 21.2495C194.375 35.8312 208.758 56.6835 215.339 80.4466C221.924 104.21 220.322 129.499 210.797 152.241C201.271 174.982 184.374 193.851 162.831 205.806L109.664 109.786L63.3182 209.286Z" fill="white"/>
<path d="M162.726 205.864C147.617 214.226 130.748 218.901 113.494 219.504C96.24 220.108 79.0873 216.625 63.4319 209.338L109.665 109.785L162.726 205.864Z" fill="#111827"/>
<path d="M47.9785 64.5504H172.557V117.247C172.557 130.949 169.34 141.254 162.907 148.162C156.59 155.19 148.402 158.703 138.342 158.703C131.908 158.703 126.293 157.123 121.497 153.961C116.818 150.915 113.251 146.7 110.794 141.314C108.572 147.167 105.062 151.793 100.266 155.19C95.4705 158.703 89.7972 160.46 83.2466 160.46C72.2509 160.46 63.5947 156.947 57.2781 149.92C51.0784 142.894 47.9785 132.764 47.9785 119.531V64.5504ZM149.572 89.4936H122.55V117.072C122.55 122.107 123.661 125.972 125.884 128.665C128.224 131.476 131.616 132.88 136.061 132.88C140.038 132.88 143.255 131.535 145.711 128.842C148.284 126.265 149.572 122.341 149.572 117.072V89.4936ZM99.3891 89.4936H70.9642V118.828C70.9642 124.098 72.1924 128.022 74.6489 130.598C77.1054 133.291 80.6146 134.637 85.1766 134.637C89.5047 134.637 92.9555 133.291 95.529 130.598C98.1024 128.022 99.3891 124.098 99.3891 118.828V89.4936Z" fill="#111827"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg width="1024" height="220" viewBox="0 0 1024 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M268 167.731H314.961C342.309 167.731 359.582 154.103 359.582 133.123C359.582 117.522 352.745 106.404 332.234 101.563C347.707 95.8246 352.565 87.2173 352.565 75.203C352.565 55.6573 337.811 42.2085 310.463 42.2085H268V167.731ZM293.19 145.137V114.474H312.622C326.296 114.474 334.033 120.032 334.033 129.178C334.033 140.116 326.296 145.137 312.622 145.137H293.19ZM293.19 94.0314V64.6232H308.123C321.798 64.6232 327.016 70.5407 327.016 79.148C327.016 89.1898 321.798 94.0314 308.123 94.0314H293.19ZM379.054 205.208C401.005 205.208 412.7 192.477 420.617 173.649L462 74.4857H433.212L415.039 127.743C413.42 132.226 411.981 137.785 411.081 142.627C410.181 137.785 408.742 132.226 407.123 127.743L388.95 74.4857H359.622L396.687 163.427L395.427 167.372C392.729 175.621 386.791 181.539 377.435 181.539C373.476 181.539 370.058 181.18 366.459 180.283L362.861 203.415C367.719 204.85 374.196 205.208 379.054 205.208Z" fill="#111827"/>
<path d="M509.679 167.881H522.973V146.401H512.913C508.242 146.401 505.368 143.716 505.368 137.809V95.3848H525.129V74.7993H505.368V53.4979L481.115 56.0039V74.7993H468V95.3848H481.115V139.062C481.115 159.289 491.175 167.881 509.679 167.881ZM575.514 170.208C594.018 170.208 609.648 161.974 617.193 150.339L598.33 136.198C594.018 143.358 585.934 148.728 575.514 148.728C563.298 148.728 555.034 141.031 552.339 130.112H622.223C622.762 127.069 622.942 124.204 622.942 121.519C622.942 94.6688 603.539 72.4723 575.514 72.4723C547.488 72.4723 527.188 94.6688 527.188 121.519C527.188 148.37 546.59 170.208 575.514 170.208ZM552.339 112.032C555.034 100.934 563.657 93.2368 575.155 93.2368C587.012 93.2368 595.814 101.65 598.509 112.032H552.339ZM687.305 170.208C712.995 170.208 733.475 148.549 733.475 121.698C733.475 94.8478 712.995 72.8303 687.305 72.8303C671.316 72.8303 662.872 79.2744 657.842 89.1197V37.2085H633.589V167.881H657.123V152.666C662.153 163.227 670.597 170.208 687.305 170.208ZM657.123 121.698C657.123 107.199 667.363 94.6688 683.352 94.6688C698.802 94.6688 709.222 107.02 709.222 121.519C709.222 135.661 698.802 148.37 683.352 148.37C667.363 148.37 657.123 136.198 657.123 121.698ZM785.992 170.208C802.7 170.208 811.144 163.227 816.174 152.666V167.881H839.708V74.7993H815.635V89.4777C810.605 79.4534 802.161 72.8303 785.992 72.8303C760.302 72.8303 739.822 94.8478 739.822 121.698C739.822 148.549 760.302 170.208 785.992 170.208ZM764.075 121.519C764.075 107.02 774.495 94.6688 789.945 94.6688C805.934 94.6688 816.174 107.199 816.174 121.698C816.174 136.198 805.934 148.37 789.945 148.37C774.495 148.37 764.075 135.661 764.075 121.519ZM886.096 170.208C906.756 170.208 922.385 157.32 922.385 140.136C922.385 126.173 916.097 116.149 888.97 109.168C877.832 106.304 876.394 103.44 876.394 100.218C876.394 94.4898 881.425 91.9837 888.072 91.9837C894.36 91.9837 900.108 94.8478 904.6 102.008L922.206 88.5827C912.684 76.2314 902.624 72.4723 887.533 72.4723C865.615 72.4723 852.68 84.6446 852.68 100.934C852.68 112.748 856.453 124.025 881.425 129.933C896.156 133.334 898.492 136.019 898.492 140.852C898.492 146.401 893.821 150.518 885.557 150.518C877.652 150.518 870.466 146.759 863.639 139.062L847.291 153.919C857.531 164.838 870.286 170.208 886.096 170.208ZM976.572 170.208C995.076 170.208 1010.71 161.974 1018.25 150.339L999.388 136.198C995.076 143.358 986.992 148.728 976.572 148.728C964.356 148.728 956.092 141.031 953.397 130.112H1023.28C1023.82 127.069 1024 124.204 1024 121.519C1024 94.6688 1004.6 72.4723 976.572 72.4723C948.546 72.4723 928.246 94.6688 928.246 121.519C928.246 148.37 947.648 170.208 976.572 170.208ZM953.397 112.032C956.092 100.934 964.715 93.2368 976.213 93.2368C988.07 93.2368 996.873 101.65 999.567 112.032H953.397Z" fill="#111827"/>
<path d="M63.3735 209.286C41.0488 198.863 22.879 181.221 11.7918 159.201C0.704692 137.179 -2.65399 112.063 2.25697 87.8978C7.16794 63.733 20.0626 41.927 38.863 25.9936C57.6635 10.0604 81.2749 0.927572 105.892 0.0669197C130.51 -0.793732 154.7 6.66803 174.566 21.2495C194.43 35.8312 208.813 56.6835 215.395 80.4466C221.979 104.21 220.378 129.499 210.852 152.241C201.326 174.982 184.429 193.851 162.886 205.806L109.72 109.786L63.3735 209.286Z" fill="#111827"/>
<path d="M162.781 205.864C147.672 214.226 130.804 218.901 113.549 219.504C96.2953 220.108 79.1426 216.625 63.4872 209.338L109.72 109.785L162.781 205.864Z" fill="white"/>
<path d="M48.0338 64.5504H172.612V117.247C172.612 130.949 169.396 141.254 162.962 148.162C156.645 155.19 148.458 158.703 138.397 158.703C131.964 158.703 126.349 157.123 121.553 153.961C116.874 150.915 113.306 146.7 110.85 141.314C108.627 147.167 105.118 151.793 100.322 155.19C95.5258 158.703 89.8525 160.46 83.3019 160.46C72.3062 160.46 63.65 156.947 57.3333 149.92C51.1337 142.894 48.0338 132.764 48.0338 119.531V64.5504ZM149.627 89.4936H122.606V117.072C122.606 122.107 123.717 125.972 125.939 128.665C128.279 131.476 131.671 132.88 136.116 132.88C140.093 132.88 143.31 131.535 145.767 128.842C148.34 126.265 149.627 122.341 149.627 117.072V89.4936ZM99.4444 89.4936H71.0194V118.828C71.0194 124.098 72.2477 128.022 74.7042 130.598C77.1607 133.291 80.6699 134.637 85.2319 134.637C89.56 134.637 93.0108 133.291 95.5843 130.598C98.1577 128.022 99.4444 124.098 99.4444 118.828V89.4936Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg width="581" height="113" viewBox="0 0 581 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z" fill="white"/>
<path d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z" fill="white"/>
<path d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z" fill="white"/>
<path d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z" fill="white"/>
<path d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z" fill="white"/>
<path d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z" fill="white"/>
<path d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z" fill="white"/>
<path d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z" fill="white"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg width="581" height="113" viewBox="0 0 581 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z" fill="#1F1F1F"/>
<path d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z" fill="#1F1F1F"/>
<path d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z" fill="#1F1F1F"/>
<path d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z" fill="#1F1F1F"/>
<path d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z" fill="#1F1F1F"/>
<path d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z" fill="#1F1F1F"/>
<path d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z" fill="#1F1F1F"/>
<path d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z" fill="#1F1F1F"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -565,6 +565,23 @@
pdSchema: public
pdVolatility: Volatile
- - qiName: get_work_mem
qiSchema: public
- - pdDescription: null
pdFuncSettings: []
pdHasVariadic: false
pdName: get_work_mem
pdParams: []
pdReturnType:
contents:
contents:
qiName: text
qiSchema: pg_catalog
tag: Scalar
tag: Single
pdSchema: public
pdVolatility: Volatile
- - qiName: notify_do_nothing
qiSchema: public
- - pdDescription: null
+1
View File
@@ -10,6 +10,7 @@ CONFIGSDIR = BASEDIR / "configs"
FIXTURES = yaml.load(
(BASEDIR / "fixtures/fixtures.yaml").read_text(), Loader=yaml.Loader
)
NGINX_BIN = shutil.which("nginx")
POSTGREST_BIN = shutil.which("postgrest")
SECRET = "reallyreallyreallyreallyverysafe"
+1 -1
View File
@@ -11399,7 +11399,7 @@ $$;
DROP ROLE IF EXISTS postgrest_test_anonymous;
CREATE ROLE postgrest_test_anonymous;
GRANT postgrest_test_anonymous TO :PGUSER;
GRANT postgrest_test_anonymous TO :"PGUSER";
GRANT USAGE ON SCHEMA apflora TO postgrest_test_anonymous;
GRANT USAGE ON SCHEMA fuzzysearch TO postgrest_test_anonymous;
+3
View File
@@ -0,0 +1,3 @@
SET client_min_messages = WARNING; -- suppress "NOTICE: ..." messages which pollute the log
SET check_function_bodies = false; -- to allow conditionals based on the pg version
SET search_path = public;
+8 -299
View File
@@ -1,300 +1,9 @@
-- Load all IO tests fixtures for PostgREST
\set ON_ERROR_STOP on
\ir database.sql
\ir db_config.sql
set check_function_bodies = false; -- to allow conditionals based on the pg version
set search_path to public;
CREATE ROLE postgrest_test_anonymous;
ALTER ROLE :PGUSER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
CREATE ROLE postgrest_test_author;
CREATE ROLE postgrest_test_serializable;
alter role postgrest_test_serializable set default_transaction_isolation = 'serializable';
CREATE ROLE postgrest_test_repeatable_read;
alter role postgrest_test_repeatable_read set default_transaction_isolation = 'REPEATABLE READ';
CREATE ROLE postgrest_test_w_superuser_settings;
alter role postgrest_test_w_superuser_settings set log_min_duration_statement = 1;
alter role postgrest_test_w_superuser_settings set log_min_messages = 'fatal';
DO $do$BEGIN
IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;
GRANT SET ON PARAMETER log_min_duration_sample to postgrest_test_authenticator;
END IF;
END$do$;
GRANT
postgrest_test_anonymous, postgrest_test_author,
postgrest_test_serializable, postgrest_test_repeatable_read,
postgrest_test_w_superuser_settings TO :PGUSER;
CREATE SCHEMA v1;
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
CREATE SCHEMA test;
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
CREATE TABLE authors_only ();
GRANT SELECT ON authors_only TO postgrest_test_author;
CREATE TABLE projects AS SELECT FROM generate_series(1,5);
GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
create function get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text;
$$ language sql;
create function v1.get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text;
$$ language sql;
create function uses_prepared_statements() returns bool as $$
select count(name) > 0 from pg_catalog.pg_prepared_statements
$$ language sql;
create function change_max_rows_config(val int, notify bool default false) returns void as $_$
begin
execute format($$
alter role postgrest_test_authenticator set pgrst.db_max_rows = %L;
$$, val);
if notify then
perform pg_notify('pgrst', 'reload config');
end if;
end $_$ volatile security definer language plpgsql ;
create function reset_max_rows_config() returns void as $_$
begin
alter role postgrest_test_authenticator reset pgrst.db_max_rows;
end $_$ volatile security definer language plpgsql ;
create function change_db_schema_and_full_reload(schemas text) returns void as $_$
begin
execute format($$
alter role postgrest_test_authenticator set pgrst.db_schemas = %L;
$$, schemas);
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ;
create function v1.reset_db_schema_config() returns void as $_$
begin
alter role postgrest_test_authenticator reset pgrst.db_schemas;
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ;
create function invalid_role_claim_key_reload() returns void as $_$
begin
alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = 'test';
perform pg_notify('pgrst', 'reload config');
end $_$ volatile security definer language plpgsql ;
create function notify_do_nothing() returns void as $_$
notify pgrst, 'nothing';
$_$ language sql;
create function do_nothing() returns void as $_$
$_$ language sql;
create function reset_invalid_role_claim_key() returns void as $_$
begin
alter role postgrest_test_authenticator reset pgrst.jwt_role_claim_key;
perform pg_notify('pgrst', 'reload config');
end $_$ volatile security definer language plpgsql ;
create function reload_pgrst_config() returns void as $_$
begin
perform pg_notify('pgrst', 'reload config');
end $_$ language plpgsql ;
create or replace function sleep(seconds double precision) returns void as $$
select pg_sleep(seconds);
$$ language sql;
create or replace function hello() returns text as $$
select 'hello'::text;
$$ language sql;
create table cats(id uuid primary key, name text);
grant all on cats to postgrest_test_anonymous;
create function drop_change_cats() returns void
language sql security definer
as $$
drop table cats;
create table cats(id bigint primary key, name text);
grant all on table cats to postgrest_test_anonymous;
notify pgrst, 'reload schema';
$$;
alter role postgrest_test_anonymous set statement_timeout to '2s';
alter role postgrest_test_author set statement_timeout to '10s';
create function change_role_statement_timeout(timeout text) returns void as $_$
begin
execute format($$
alter role current_user set statement_timeout = %L;
$$, timeout);
end $_$ volatile language plpgsql ;
create table items as select x as id from generate_series(1,5) x;
create view items_w_isolation_level as
select
id,
current_setting('transaction_isolation', true) as isolation_level
from items;
grant all on items_w_isolation_level to postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
create function default_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql;
create function serializable_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql set default_transaction_isolation = 'serializable';
create function repeatable_read_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql set default_transaction_isolation = 'REPEATABLE READ';
create or replace function create_function() returns void as $_$
drop function if exists mult_them(int, int);
create or replace function mult_them(a int, b int) returns int as $$
select a*b;
$$ language sql;
notify pgrst, 'reload schema';
$_$ language sql security definer;
create or replace function migrate_function() returns void as $_$
drop function if exists mult_them(int, int);
create or replace function mult_them(c int, d int) returns int as $$
select c*d;
$$ language sql;
notify pgrst, 'reload schema';
$_$ language sql security definer;
create or replace function get_pgrst_version() returns text
language sql
as $$
select application_name
from pg_stat_activity
where application_name ilike 'postgrest%'
limit 1;
$$;
create function terminate_pgrst(appname text) returns setof record as $$
select pg_terminate_backend(pid) from pg_stat_activity where application_name iLIKE '%' || appname || '%';
$$ language sql security definer;
create or replace function one_sec_timeout() returns void as $$
select pg_sleep(3);
$$ language sql set statement_timeout = '1s';
create or replace function four_sec_timeout() returns void as $$
select pg_sleep(3);
$$ language sql set statement_timeout = '4s';
create function get_postgres_version() returns int as $$
select current_setting('server_version_num')::int;
$$ language sql;
create or replace function rpc_work_mem() returns items as $$
select 1
$$ language sql
set work_mem = '6000';
create or replace function rpc_with_one_hoisted() returns items as $$
select 1
$$ language sql
set work_mem = '3000'
set statement_timeout = '7s';
create or replace function rpc_with_two_hoisted() returns items as $$
select 1
$$ language sql
set work_mem = '5000'
set statement_timeout = '10s';
create function get_work_mem(items) returns text as $$
select current_setting('work_mem', true) as work_mem
$$ language sql;
create function get_statement_timeout(items) returns text as $$
select current_setting('statement_timeout', true) as statement_timeout
$$ language sql;
create function change_db_schemas_config() returns void as $_$
begin
alter role postgrest_test_authenticator set pgrst.db_schemas = 'test';
end $_$ volatile security definer language plpgsql;
create function reset_db_schemas_config() returns void as $_$
begin
alter role postgrest_test_authenticator reset pgrst.db_schemas;
end $_$ volatile security definer language plpgsql ;
create function test.get_current_schema() returns text as $$
select current_schema()::text;
$$ language sql;
create or replace function root() returns json as $_$
select '{"swagger": "2.0"}'::json;
$_$ language sql;
create view infinite_recursion as
select * from projects;
create or replace view infinite_recursion as
select * from infinite_recursion;
create or replace function "true"() returns boolean as $_$
select true;
$_$ language sql;
create or replace function notify_pgrst() returns void as $$
notify pgrst;
$$ language sql;
-- directors and films table can be used for resource embedding tests
create table directors (
id int primary key,
name text
);
create table films (
id int primary key,
title text,
director_id int,
constraint fk_director
foreign key (director_id) references directors (id)
on update cascade
on delete cascade
);
-- data to test resource embedding
truncate table directors cascade;
insert into directors
values (1, 'quentin tarantino'),
(2, 'christopher nolan'),
(3, 'yorgos lathinmos');
truncate table films cascade;
insert into films
values (1, 'pulp fiction', 1),
(2, 'intersteller',2),
(3, 'dogtooth',3),
(4, 'reservoir dogs', 1);
GRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
\ir roles.sql
\ir schema.sql
\ir privileges.sql
+9
View File
@@ -0,0 +1,9 @@
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
GRANT SELECT ON authors_only TO postgrest_test_author;
GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
GRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
GRANT ALL ON cats TO postgrest_test_anonymous;
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
+1 -1
View File
@@ -13,7 +13,7 @@ create table replica.items as select x as id from generate_series(1, 10) x;
DROP ROLE IF EXISTS postgrest_test_anonymous;
CREATE ROLE postgrest_test_anonymous;
GRANT postgrest_test_anonymous TO :PGUSER;
GRANT postgrest_test_anonymous TO :"PGUSER";
GRANT USAGE ON SCHEMA replica TO postgrest_test_anonymous;
+30
View File
@@ -0,0 +1,30 @@
DROP ROLE IF EXISTS
postgrest_test_anonymous, postgrest_test_author,
postgrest_test_serializable, postgrest_test_repeatable_read,
postgrest_test_w_superuser_settings;
CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_author;
CREATE ROLE postgrest_test_serializable;
CREATE ROLE postgrest_test_repeatable_read;
CREATE ROLE postgrest_test_w_superuser_settings;
CREATE ROLE postgrest_test_work_mem;
GRANT
postgrest_test_anonymous, postgrest_test_author,
postgrest_test_serializable, postgrest_test_repeatable_read,
postgrest_test_w_superuser_settings, postgrest_test_work_mem TO :"PGUSER";
GRANT postgrest_test_anonymous TO timeout_authenticator;
ALTER ROLE :"PGUSER" SET pgrst.db_anon_role = 'postgrest_test_anonymous';
ALTER ROLE postgrest_test_serializable SET default_transaction_isolation = 'serializable';
ALTER ROLE postgrest_test_repeatable_read SET default_transaction_isolation = 'REPEATABLE READ';
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_statement = 1;
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_messages = 'fatal';
ALTER ROLE postgrest_test_anonymous SET statement_timeout TO '2s';
ALTER ROLE postgrest_test_author SET statement_timeout TO '10s';
ALTER ROLE postgrest_test_work_mem SET work_mem TO '3MB';
+264
View File
@@ -0,0 +1,264 @@
DROP SCHEMA IF EXISTS v1, test;
CREATE SCHEMA v1;
CREATE SCHEMA test;
CREATE TABLE authors_only ();
CREATE TABLE projects AS SELECT FROM generate_series(1,5);
CREATE TABLE cats(id uuid primary key, name text);
CREATE TABLE items AS SELECT x AS id FROM generate_series(1,5) x;
-- directors and films table can be used for resource embedding tests
CREATE TABLE directors (
id int primary key,
name text
);
CREATE TABLE films (
id int primary key,
title text,
director_id int,
constraint fk_director
foreign key (director_id) references directors (id)
on update cascade
on delete cascade
);
-- data to test resource embedding
TRUNCATE TABLE directors CASCADE;
INSERT INTO directors
VALUES (1, 'quentin tarantino'),
(2, 'christopher nolan'),
(3, 'yorgos lathinmos');
TRUNCATE TABLE films CASCADE;
INSERT INTO films
VALUES (1, 'pulp fiction', 1),
(2, 'intersteller',2),
(3, 'dogtooth',3),
(4, 'reservoir dogs', 1);
DO $do$BEGIN
IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;
GRANT SET ON PARAMETER log_min_duration_sample to "Postgrest_Test_Authenticator";
END IF;
END$do$;
create function get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text;
$$ language sql;
create function v1.get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text;
$$ language sql;
create function uses_prepared_statements() returns bool as $$
select count(name) > 0 from pg_catalog.pg_prepared_statements
$$ language sql;
create function change_max_rows_config(val int, notify bool default false) returns void as $_$
begin
execute format($$
alter role "Postgrest_Test_Authenticator" set pgrst.db_max_rows = %L;
$$, val);
if notify then
perform pg_notify('pgrst', 'reload config');
end if;
end $_$ volatile security definer language plpgsql ;
create function reset_max_rows_config() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" reset pgrst.db_max_rows;
end $_$ volatile security definer language plpgsql ;
create function change_db_schema_and_full_reload(schemas text) returns void as $_$
begin
execute format($$
alter role "Postgrest_Test_Authenticator" set pgrst.db_schemas = %L;
$$, schemas);
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ;
create function v1.reset_db_schema_config() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" reset pgrst.db_schemas;
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ;
create function invalid_role_claim_key_reload() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" set pgrst.jwt_role_claim_key = 'test';
perform pg_notify('pgrst', 'reload config');
end $_$ volatile security definer language plpgsql ;
create function notify_do_nothing() returns void as $_$
notify pgrst, 'nothing';
$_$ language sql;
create function do_nothing() returns void as $_$
$_$ language sql;
create function reset_invalid_role_claim_key() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" reset pgrst.jwt_role_claim_key;
perform pg_notify('pgrst', 'reload config');
end $_$ volatile security definer language plpgsql ;
create function reload_pgrst_config() returns void as $_$
begin
perform pg_notify('pgrst', 'reload config');
end $_$ language plpgsql ;
create or replace function sleep(seconds double precision) returns void as $$
select pg_sleep(seconds);
$$ language sql;
create or replace function hello() returns text as $$
select 'hello'::text;
$$ language sql;
create function drop_change_cats() returns void
language sql security definer
as $$
drop table cats;
create table cats(id bigint primary key, name text);
grant all on table cats to postgrest_test_anonymous;
notify pgrst, 'reload schema';
$$;
create function change_role_statement_timeout(timeout text) returns void as $_$
begin
execute format($$
alter role current_user set statement_timeout = %L;
$$, timeout);
end $_$ volatile language plpgsql ;
create view items_w_isolation_level as
select
id,
current_setting('transaction_isolation', true) as isolation_level
from items;
create function default_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql;
create function serializable_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql set default_transaction_isolation = 'serializable';
create function repeatable_read_isolation_level()
returns text as $$
select current_setting('transaction_isolation', true);
$$
language sql set default_transaction_isolation = 'REPEATABLE READ';
create or replace function create_function() returns void as $_$
drop function if exists mult_them(int, int);
create or replace function mult_them(a int, b int) returns int as $$
select a*b;
$$ language sql;
notify pgrst, 'reload schema';
$_$ language sql security definer;
create or replace function migrate_function() returns void as $_$
drop function if exists mult_them(int, int);
create or replace function mult_them(c int, d int) returns int as $$
select c*d;
$$ language sql;
notify pgrst, 'reload schema';
$_$ language sql security definer;
create or replace function get_pgrst_version() returns text
language sql
as $$
select application_name
from pg_stat_activity
where application_name ilike 'postgrest%'
limit 1;
$$;
create function terminate_pgrst(appname text) returns setof record as $$
select pg_terminate_backend(pid) from pg_stat_activity where application_name iLIKE '%' || appname || '%';
$$ language sql security definer;
create or replace function one_sec_timeout() returns void as $$
select pg_sleep(3);
$$ language sql set statement_timeout = '1s';
create or replace function four_sec_timeout() returns void as $$
select pg_sleep(3);
$$ language sql set statement_timeout = '4s';
create function get_postgres_version() returns int as $$
select current_setting('server_version_num')::int;
$$ language sql;
create or replace function rpc_work_mem() returns items as $$
select 1
$$ language sql
set work_mem = '6000';
create or replace function rpc_with_one_hoisted() returns items as $$
select 1
$$ language sql
set work_mem = '3000'
set statement_timeout = '7s';
create or replace function rpc_with_two_hoisted() returns items as $$
select 1
$$ language sql
set work_mem = '5000'
set statement_timeout = '10s';
create function get_work_mem(items) returns text as $$
select current_setting('work_mem', true) as work_mem
$$ language sql;
create function get_statement_timeout(items) returns text as $$
select current_setting('statement_timeout', true) as statement_timeout
$$ language sql;
create function change_db_schemas_config() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" set pgrst.db_schemas = 'test';
end $_$ volatile security definer language plpgsql;
create function reset_db_schemas_config() returns void as $_$
begin
alter role "Postgrest_Test_Authenticator" reset pgrst.db_schemas;
end $_$ volatile security definer language plpgsql ;
create function test.get_current_schema() returns text as $$
select current_schema()::text;
$$ language sql;
create or replace function root() returns json as $_$
select '{"swagger": "2.0"}'::json;
$_$ language sql;
create view infinite_recursion as
select * from projects;
create or replace view infinite_recursion as
select * from infinite_recursion;
create or replace function "true"() returns boolean as $_$
select true;
$_$ language sql;
create or replace function notify_pgrst() returns void as $$
notify pgrst;
$$ language sql;
create or replace function get_work_mem() returns text as $$
select current_setting('work_mem', true);
$$ language sql;
+13
View File
@@ -0,0 +1,13 @@
# the PG* variables are replaced by preprocessing, not done by nginx itself
daemon off;
pid ./nginx.pid;
events {}
stream {
server {
listen unix:$PGPROXYHOST/.s.PGSQL.5432;
proxy_timeout $PGPROXY_TIMEOUT;
proxy_pass unix:$PGHOST/.s.PGSQL.5432;
}
}
+60 -3
View File
@@ -2,18 +2,20 @@
import contextlib
import dataclasses
import enum
import os
import pathlib
import socket
import subprocess
import tempfile
import time
import string
import urllib.parse
import requests
import requests_unixsocket
from config import POSTGREST_BIN, hpctixfile
from config import POSTGREST_BIN, NGINX_BIN, hpctixfile
def sleep_until_postgrest_scache_reload():
@@ -35,6 +37,13 @@ class PostgrestTimedOut(Exception):
"Connecting to PostgREST endpoint timed out."
class Admin(str, enum.Enum):
"Admin endpoint to wait for before yielding a PostgREST process."
live = "live"
ready = "ready"
class PostgrestSession(requests_unixsocket.Session):
"HTTP client session directed at a PostgREST endpoint."
@@ -86,7 +95,7 @@ def run(
env=None,
port=None,
host=None,
wait_for_readiness=True,
wait_for=Admin.ready,
wait_max_seconds=1,
no_pool_connection_available=False,
no_startup_stdout=True,
@@ -138,8 +147,10 @@ def run(
process.stdin.write(stdin or b"")
process.stdin.close()
if wait_for_readiness:
if wait_for == Admin.ready:
wait_until_status_code(adminurl + "/ready", wait_max_seconds, 200)
elif wait_for == Admin.live:
wait_until_status_code(adminurl + "/live", wait_max_seconds, 200)
if no_startup_stdout:
process.stdout.read()
@@ -165,6 +176,52 @@ def run(
process.wait()
@contextlib.contextmanager
def run_pgproxy(env=None, proxy_timeout="1s"):
"Run nginx as a unix socket proxy for PostgreSQL and expose PGPROXYHOST."
env = dict(os.environ if env is None else env)
with tempfile.TemporaryDirectory() as tmpdir:
# build a <tmpdir>/conf/ so `nginx -p` picks the config automatically
tmpdir = pathlib.Path(tmpdir)
conf_dir = tmpdir / "conf"
conf_dir.mkdir(parents=True)
nginx_env = dict(env)
nginx_env["PGPROXYHOST"] = str(tmpdir)
nginx_env["PGPROXY_TIMEOUT"] = proxy_timeout
source_conf = pathlib.Path("test/io/nginx/nginx.conf")
out_conf = conf_dir / "nginx.conf"
out_conf.write_text(
string.Template(source_conf.read_text()).substitute(nginx_env)
)
process = subprocess.Popen(
[NGINX_BIN, "-p", str(tmpdir), "-e", "stderr"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=nginx_env,
)
if process.poll() is not None:
(_, stderr_output) = process.communicate(timeout=1)
raise RuntimeError(
f"{NGINX_BIN} exited with {process.returncode}: {stderr_output}"
)
try:
yield str(tmpdir)
finally:
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
def freeport(used_ports=None):
"Find an unused free port on localhost."
while True:
+1 -1
View File
@@ -167,7 +167,7 @@ def test_fail_with_invalid_password(defaultenv):
"Connecting with an invalid password should fail without retries."
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
env = {**defaultenv, "PGRST_DB_URI": uri}
with run(env=env, wait_for_readiness=False) as postgrest:
with run(env=env, wait_for=None) as postgrest:
exitCode = wait_until_exit(postgrest)
assert exitCode == 1
+29 -1
View File
@@ -3,6 +3,7 @@
import re
import pytest
import requests
from postgrest import run
@@ -54,6 +55,33 @@ def test_openapi_in_big_schema(defaultenv):
assert response.status_code == 200
@pytest.mark.parametrize("level", ["crit", "error", "warn", "info", "debug"])
def test_stackoverflow_is_logged(level, defaultenv):
"Stack overflow should be logged with the Warp message only on log-level=debug"
env = {
**defaultenv,
"PGRST_DB_SCHEMAS": "apflora",
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
"PGRST_LOG_LEVEL": level,
}
with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:
with pytest.raises(requests.exceptions.ConnectionError):
postgrest.session.get("/")
output = postgrest.read_stdout(nlines=10)
for _ in range(3):
output.extend(postgrest.read_stdout(nlines=10))
found = any("Warp server: stack overflow" in line for line in output)
if level == "debug":
assert found
else:
assert not found
# See: https://github.com/PostgREST/postgrest/issues/3329
def test_should_not_fail_with_stack_overflow(defaultenv):
"requesting a non-existent relationship should not fail with stack overflow due to fuzzy search of candidates"
@@ -89,4 +117,4 @@ def test_second_request_for_non_existent_table_should_be_quick(defaultenv):
assert data["code"] == "PGRST205"
first_duration = response.elapsed.total_seconds()
response = postgrest.session.get("/unknown-table")
assert response.elapsed.total_seconds() < first_duration / 10
assert response.elapsed.total_seconds() < first_duration / 2
+239 -128
View File
@@ -3,16 +3,24 @@
import os
import re
import signal
import subprocess
import time
import pytest
from config import CONFIGSDIR, FIXTURES, SECRET
from util import Thread, jwtauthheader, parse_server_timings_header
from util import (
Thread,
jwtauthheader,
parse_server_timings_header,
match_log,
)
from postgrest import (
Admin,
freeport,
is_ipv6,
reset_statement_timeout,
run,
run_pgproxy,
set_statement_timeout,
sleep_until_postgrest_config_reload,
sleep_until_postgrest_full_reload,
@@ -21,6 +29,20 @@ from postgrest import (
)
def psql_as_superuser(query):
subprocess.check_call(
[
"psql",
"--username",
"postgres",
"--set",
"ON_ERROR_STOP=1",
"-c",
query,
]
)
def test_connect_with_dburi(dburi, defaultenv):
"Connecting with db-uri instead of LIPQ* environment variables should work."
defaultenv_without_libpq = {
@@ -105,6 +127,24 @@ def test_flush_pool_no_interrupt(defaultenv):
t.join()
def test_termination_unix_signal_logging(defaultenv):
"Server logs when handling termination unix signals."
with run(env=defaultenv) as postgrest:
postgrest.process.send_signal(signal.SIGTERM)
lines = postgrest.read_stdout(nlines=1)
wait_until_exit(postgrest)
assert any("SIGTERM" in line for line in lines)
with run(env=defaultenv) as postgrest:
postgrest.process.send_signal(signal.SIGINT)
lines = postgrest.read_stdout(nlines=1)
wait_until_exit(postgrest)
assert any("SIGINT" in line for line in lines)
def test_random_port_bound(defaultenv):
"PostgREST should bind to a random port when PGRST_SERVER_PORT is 0."
@@ -511,6 +551,39 @@ def test_admin_ready_w_channel(defaultenv):
assert response.status_code == 200
def test_listener_query_is_visible_in_pg_stat_activity(defaultenv):
"The listener connection should show the LISTEN pgrst statement in pg_stat_activity"
env = {
**defaultenv,
"PGRST_DB_CHANNEL_ENABLED": "true",
"PGAPPNAME": "listener-query-test",
}
with run(env=env):
query = """
select query
from pg_stat_activity
where application_name = 'listener-query-test'
and query = 'LISTEN "pgrst"'
limit 1;
"""
output = subprocess.check_output(
[
"psql",
"--set",
"ON_ERROR_STOP=1",
"--tuples-only",
"--no-align",
"-c",
query,
],
text=True,
).strip()
assert output == 'LISTEN "pgrst"'
def test_admin_ready_wo_channel(defaultenv):
"Should get a success response from the admin server ready endpoint when the LISTEN channel is disabled"
@@ -532,7 +605,7 @@ def test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest):
env = {
**defaultenv,
"PGUSER": role,
"PGRST_DB_ANON_ROLE": role,
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "500",
}
@@ -661,55 +734,45 @@ def test_log_level(level, defaultenv):
response = postgrest.session.get("/")
assert response.status_code == 200
output = sorted(postgrest.read_stdout(nlines=7))
output = postgrest.read_stdout(nlines=9)
if level == "crit":
assert len(output) == 0
elif level == "error":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
match_log(
output,
[r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"'],
)
assert len(output) == 1
elif level == "warn":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[1],
match_log(
output,
[
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
],
)
assert len(output) == 2
elif level == "info":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
match_log(
output,
[
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
],
)
assert len(output) == 3
elif level == "debug":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
match_log(
output,
[
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
)
assert len(output) == 7
assert len(output) == 9
assert any("Connection" and "is available" in line for line in output)
assert any("Connection" and "is used" in line for line in output)
@@ -909,6 +972,61 @@ def test_notify_reloading_catalog_cache(defaultenv):
assert response.status_code == 200
def test_stale_schema_cache_dropped_table_returns_database_error(defaultenv):
"dropped table should return a database error while schema cache is stale"
internal_sleep = 2
env = {
**defaultenv,
"PGRST_DB_POOL": "2",
"PGRST_DB_CHANNEL_ENABLED": "true",
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": str(internal_sleep * 1000),
}
try:
psql_as_superuser(
"""
drop table if exists stale_schema_cache_items;
create table stale_schema_cache_items(id int primary key);
insert into stale_schema_cache_items values (1);
grant select on stale_schema_cache_items to postgrest_test_anonymous;
"""
)
with run(env=env, wait_max_seconds=10) as postgrest:
response = postgrest.session.get("/stale_schema_cache_items")
assert response.status_code == 200
psql_as_superuser(
"""
drop table stale_schema_cache_items;
notify pgrst, 'reload schema';
"""
)
response = postgrest.session.get("/stale_schema_cache_items")
payload = response.json()
assert response.status_code == 404
assert payload["code"] == "42P01"
assert (
payload["message"]
== 'relation "public.stale_schema_cache_items" does not exist'
)
time.sleep(internal_sleep + 0.3)
response = postgrest.session.get("/stale_schema_cache_items")
payload = response.json()
assert response.status_code == 404
assert payload["code"] == "PGRST205"
assert (
payload["message"]
== "Could not find the table 'public.stale_schema_cache_items' in the schema cache"
)
finally:
psql_as_superuser("drop table if exists stale_schema_cache_items;")
def test_role_settings(defaultenv):
"statement_timeout should be set per role"
@@ -1031,7 +1149,7 @@ def test_schema_cache_concurrent_notifications(slow_schema_cache_env):
int(slow_schema_cache_env["PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP"]) / 1000
)
with run(env=slow_schema_cache_env, wait_for_readiness=False) as postgrest:
with run(env=slow_schema_cache_env, wait_for=None) as postgrest:
time.sleep(2 * internal_sleep + 0.1) # wait for readiness manually
# first request, create a function and set a schema cache reload in progress
@@ -1083,31 +1201,6 @@ def test_schema_cache_query_sleep_logs(defaultenv):
assert 1000 < observed_ms < 2000
def test_schema_cache_load_sleep_logs(defaultenv):
"""Schema cache load sleep should be reflected in the logged load duration."""
env = {
**defaultenv,
"PGRST_INTERNAL_SCHEMA_CACHE_LOAD_SLEEP": "1000",
}
log_pattern = re.compile(r"Schema cache loaded in ([\d.]+) milliseconds")
with run(env=env, wait_max_seconds=3, no_startup_stdout=False) as postgrest:
observed_ms = None
collected = []
lines = postgrest.read_stdout(nlines=10)
collected.extend(lines)
for line in lines:
match = log_pattern.search(line)
if match:
observed_ms = float(match.group(1))
break
assert observed_ms is not None
assert 1000 < observed_ms < 2000
@pytest.mark.parametrize("dburi_type", ["no_params", "no_params_qmark", "with_params"])
def test_get_pgrst_version_with_uri_connection_string(dburi_type, dburi, defaultenv):
"The fallback_application_name should be added to the db-uri if it has a URI format"
@@ -1222,7 +1315,7 @@ def test_fail_with_invalid_dbname_and_automatic_recovery_disabled(defaultenv):
"PGRST_DB_POOL_AUTOMATIC_RECOVERY": "false",
}
with run(env=env, wait_for_readiness=False) as postgrest:
with run(env=env, wait_for=None) as postgrest:
exitCode = wait_until_exit(postgrest)
assert exitCode == 1
@@ -1346,16 +1439,21 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):
assert response.status_code == 500
# ensure the message appears on the logs
output = sorted(postgrest.read_stdout(nlines=6))
output = postgrest.read_stdout(nlines=8)
if level == "crit":
assert len(output) == 0
elif level == "debug":
assert " 500 " in output[0]
assert "canceling statement due to statement timeout" in output[5]
match_log(
output,
[
r".*canceling statement due to statement timeout.*",
r".*500.*",
],
)
else:
assert " 500 " in output[0]
assert "canceling statement due to statement timeout" in output[1]
assert " 500 " in output[1]
assert "canceling statement due to statement timeout" in output[0]
reset_statement_timeout(metapostgrest, role)
@@ -1506,7 +1604,7 @@ def test_log_error_when_empty_schema_cache_on_startup_to_stderr(defaultenv):
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
}
with run(env=env, wait_for_readiness=False) as postgrest:
with run(env=env, wait_for=None) as postgrest:
postgrest.wait_until_scache_starts_loading()
response = postgrest.session.get("/projects")
@@ -1527,7 +1625,7 @@ def test_no_double_schema_cache_reload_on_empty_schema(defaultenv):
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
}
with run(env=env, port=freeport(), wait_for_readiness=False) as postgrest:
with run(env=env, port=freeport(), wait_for=None) as postgrest:
postgrest.wait_until_scache_starts_loading()
response = postgrest.session.get("/projects")
@@ -1549,18 +1647,17 @@ def test_log_pool_req_observation(level, defaultenv):
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
pool_req = "Trying to borrow a connection from pool"
pool_req_fullfill = "Borrowed a connection from the pool"
pool_req = r".*Trying to borrow a connection from pool.*"
pool_req_fullfill = r".*Borrowed a connection from the pool.*"
with run(env=env) as postgrest:
postgrest.session.get("/authors_only", headers=headers)
if level == "debug":
output = postgrest.read_stdout(nlines=5)
assert pool_req in output[1]
assert pool_req_fullfill in output[4]
assert len(output) == 5
output = postgrest.read_stdout(nlines=7)
assert len(output) == 7
match_log(output, [pool_req, pool_req_fullfill])
elif level == "info":
output = postgrest.read_stdout(nlines=4)
assert len(output) == 1
@@ -1610,7 +1707,7 @@ def test_schema_cache_error_observation(defaultenv):
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
}
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
# TODO: postgrest should exit here, instead it keeps retrying
# exitCode = wait_until_exit(postgrest)
# assert exitCode == 1
@@ -1631,7 +1728,7 @@ def test_log_listener_connection_errors(defaultenv):
"PGRST_DB_CHANNEL_ENABLED": "true",
}
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
output = postgrest.read_stdout(nlines=5)
assert any(
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
@@ -1640,6 +1737,34 @@ def test_log_listener_connection_errors(defaultenv):
)
def test_log_listener_connection_start(defaultenv):
"The logs should show the listener connection start message in a single line"
env = {
**defaultenv,
"PGRST_DB_CHANNEL_ENABLED": "true",
}
with run(env=env, no_startup_stdout=False, wait_for=Admin.ready) as postgrest:
output = postgrest.read_stdout(nlines=10)
# Check for the listener start message containing host and port
# Do not check if pg version is displayed properly as it is tricky to test it
assert any(
f'"{defaultenv["PGHOST"]}:5432" and listening for database notifications on the "pgrst" channel'
in line
for line in output
)
def test_connection_error_message_does_not_claim_retry(defaultenv):
"The connection error message should not claim retrying, since PostgREST stops on fatal errors."
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
env = {**defaultenv, "PGRST_DB_URI": uri}
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
output = postgrest.read_stdout(nlines=8)
assert any('"message":"Database connection error."' in line for line in output)
def test_db_pre_config_with_pg_reserved_words(defaultenv):
"The db-pre-config should not fail unexpectedly when function name is a postgres reserved word"
@@ -1657,7 +1782,7 @@ def test_db_pre_config_with_pg_reserved_words(defaultenv):
"PGRST_DB_PRE_CONFIG": "select", # no "select" function in our fixtures, fail gracefully at startup
}
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
output = postgrest.read_stdout(nlines=8)
assert any(
'Failed to query database settings for the config parameters.{"code":"42883","details":null,"hint":"No function matches the given name and argument types. You might need to add explicit type casts.","message":"function select() does not exist"}'
@@ -1666,54 +1791,6 @@ def test_db_pre_config_with_pg_reserved_words(defaultenv):
)
def test_requests_with_resource_embedding_wait_for_schema_cache_reload(defaultenv):
"requests that use the schema cache with resource embedding wait long for the schema cache to reload"
env = {
**defaultenv,
"PGRST_DB_POOL": "2",
"PGRST_INTERNAL_SCHEMA_CACHE_RELATIONSHIP_LOAD_SLEEP": "5100",
}
with run(env=env, wait_max_seconds=30) as postgrest:
# reload the schema cache
response = postgrest.session.get("/rpc/notify_pgrst")
assert response.status_code == 204
postgrest.wait_until_scache_starts_loading()
response = postgrest.session.get("/directors?select=id,name,films(title)")
assert response.status_code == 200
assert response.elapsed.total_seconds() > 5
def test_requests_without_resource_embedding_wait_for_schema_cache_reload(defaultenv):
"requests that use the schema cache without resource embedding wait less for the schema cache to reload"
env = {
**defaultenv,
"PGRST_DB_POOL": "2",
"PGRST_INTERNAL_SCHEMA_CACHE_LOAD_SLEEP": "1100",
"PGRST_INTERNAL_SCHEMA_CACHE_RELATIONSHIP_LOAD_SLEEP": "5000",
}
with run(env=env, wait_max_seconds=30) as postgrest:
# reload the schema cache
response = postgrest.session.get("/rpc/notify_pgrst")
assert response.status_code == 204
postgrest.wait_until_scache_starts_loading()
response = postgrest.session.get("/films")
assert response.status_code == 200
assert (
response.elapsed.total_seconds() > 1
and response.elapsed.total_seconds() < 5
)
def test_server_timing_transaction_duration(defaultenv, metapostgrest):
"server-timing transaction duration should be accurate"
@@ -1738,3 +1815,37 @@ def test_server_timing_transaction_duration(defaultenv, metapostgrest):
]
assert 2000 <= response_dur < 3000
def test_positive_pool_metric(defaultenv):
"When a network failure is caused on the pg connection, pgrst_db_pool_available stays positive"
with run_pgproxy(defaultenv, proxy_timeout="1ms") as pgproxyhost:
env = {**defaultenv, "PGHOST": pgproxyhost}
with run(env=env, wait_for=Admin.live) as postgrest:
response = postgrest.admin.get("/metrics", timeout=1)
assert response.status_code == 200
metrics = float(
re.search(
r"pgrst_db_pool_available (-?\d+(?:\.\d+)?)", response.text
).group(1)
)
assert metrics >= 0
def test_work_mem_in_role_settings(defaultenv):
"Should work when setting work_mem on a role. See https://github.com/PostgREST/postgrest/issues/4955"
env = {
**defaultenv,
"PGRST_JWT_SECRET": SECRET,
}
headers = jwtauthheader({"role": "postgrest_test_work_mem"}, SECRET)
with run(env=env) as postgrest:
response = postgrest.session.post("/rpc/get_work_mem", headers=headers)
assert response.status_code == 200
assert response.text == '"3MB"'
+14
View File
@@ -1,3 +1,4 @@
import re
import threading
import jwt
@@ -21,6 +22,19 @@ class Thread(threading.Thread):
raise self._exception
def match_log(output, matchers):
ito = iter(output)
itm = iter(matchers)
nextMatcher = next(itm, None)
while nextMatcher is not None and (line := next(ito, None)) is not None:
if re.match(nextMatcher, line) is not None:
nextMatcher = next(itm, None)
if nextMatcher is not None:
raise AssertionError(
f"Expected log line matching {nextMatcher} not found in output"
)
def authheader(token):
"Bearer token HTTP authorization header."
return {"Authorization": f"Bearer {token}"}
+2 -2
View File
@@ -1,7 +1,7 @@
CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_author;
GRANT postgrest_test_anonymous TO :PGUSER;
GRANT postgrest_test_author TO :PGUSER;
GRANT postgrest_test_anonymous TO :"PGUSER";
GRANT postgrest_test_author TO :"PGUSER";
CREATE SCHEMA test;
-- PUT+PATCH target needs one record and column to modify
+3 -3
View File
@@ -110,9 +110,9 @@ jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M"
jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M"
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1&columns=blob" "50M"
jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "73M"
jsonKeyTest "50M" "POST" "/leak?columns=blob" "73M"
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "73M"
jsonKeyTest "50M" "POST" "/rpc/leak?columns=blob" "77M"
jsonKeyTest "50M" "POST" "/leak?columns=blob" "77M"
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1&columns=blob" "77M"
postJsonArrayTest "1000" "/perf_articles?columns=id,body" "21M"
postJsonArrayTest "10000" "/perf_articles?columns=id,body" "22M"
+73
View File
@@ -0,0 +1,73 @@
module Main where
import qualified Hasql.Pool as P
import qualified Hasql.Pool.Config as P
import qualified Hasql.Transaction.Sessions as HT
import Data.Function (id)
import PostgREST.App (postgrest)
import qualified PostgREST.AppState as AppState
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.Database (queryPgVersion)
import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.SchemaCache (querySchemaCache)
import qualified Observation.JwtCache
import qualified Observation.MetricsSpec
import qualified Observation.SchemaCacheSpec
import ObsHelper
import PostgREST.Observation (Observation (HasqlPoolObs))
import Protolude hiding (toList, toS)
import Test.Hspec
main :: IO ()
main = do
poolChan <- newChan
-- make sure poolChan is not growing indefinitely
-- start a thread that drains the channel
-- this is necessary because test cases operate on
-- copies so poolChan is never read from
-- this means we have another thread running for the entire duration of the spec but this shouldn't be a problem since Haskell green threads are lightweight
void $ forkIO $ forever $ readChan poolChan
metricsState <- Metrics.init (configDbPoolSize testCfg)
pool <- P.acquire $ P.settings
[ P.size 3
, P.acquisitionTimeout 10
, P.agingTimeout 60
, P.idlenessTimeout 60
, P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)
-- make sure metrics are updated and pool observations published to poolChan
, P.observationHandler $ (writeChan poolChan <> Metrics.observationMetrics metricsState) . HasqlPoolObs
]
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
-- cached schema cache so most tests run fast
baseSchemaCache <- loadSCache pool testCfg
loggerState <- Logger.init
let
initApp sCache config = do
-- duplicate poolChan as a starting point
obsChan <- dupChan poolChan
stateObsChan <- newObsChan obsChan
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan)
AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just sCache)
return (SpecState appState metricsState stateObsChan, postgrest (configLogLevel config) appState (pure ()))
-- Run all test modules
hspec $ do
before (initApp baseSchemaCache testCfgJwtCache) $
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
before (initApp baseSchemaCache testCfg) $
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
before (initApp baseSchemaCache testCfg) $
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
where
loadSCache pool conf =
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)
+208
View File
@@ -0,0 +1,208 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module ObsHelper where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as BL
import qualified Data.List as DL
import Data.List.NonEmpty (fromList)
import Data.String (String)
import qualified Data.Text as T
import qualified Jose.Jwa as JWT
import qualified Jose.Jws as JWT
import qualified Jose.Jwt as JWT
import Network.HTTP.Types
import qualified PostgREST.AppState as AppState
import PostgREST.Config (AppConfig (..),
JSPathExp (..),
LogLevel (..),
OpenAPIMode (..),
parseSecret)
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation (Observation (..))
import Prometheus (Counter, getCounter)
import Protolude hiding (get, toS)
import System.Timeout (timeout)
import Test.Hspec
import Test.Hspec.Expectations.Contrib (annotate)
-- helpers used to produce observation diagnostics in waitForObs
-- Implementing the Show instance for Observation is hard due to having many different parameters so instead we use generic programming (`conName`) to obtain the constructor name as `Text`
class HasConstructor f where
genericConstrName :: f x -> Text
instance HasConstructor f => HasConstructor (D1 c f) where
genericConstrName (M1 x) = genericConstrName x
instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where
genericConstrName (L1 l) = genericConstrName l
genericConstrName (R1 r) = genericConstrName r
instance Constructor c => HasConstructor (C1 c f) where
genericConstrName = T.pack . conName
data SpecState = SpecState {
specAppState :: AppState.AppState,
specMetrics :: Metrics.MetricsState,
specObsChan :: ObsChan
}
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
data TimeoutException = TimeoutException deriving (Show, Exception)
data ObsChan = ObsChan (Chan Observation) (Chan Observation)
constrName :: (HasConstructor (Rep a), Generic a)=> a -> Text
constrName = genericConstrName . from
baseCfg :: AppConfig
baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig {
configAppSettings = []
, configDbAggregates = False
, configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty
, configDbChannelEnabled = True
, configDbExtraSearchPath = []
, configDbHoistedTxSettings = ["default_transaction_isolation","plan_filter.statement_cost_limit","statement_timeout"]
, configDbMaxRows = Nothing
, configDbPlanEnabled = False
, configDbPoolSize = 10
, configDbPoolAcquisitionTimeout = 10
, configDbPoolMaxLifetime = 1800
, configDbPoolMaxIdletime = 600
, configDbPoolAutomaticRecovery = True
, configDbPreRequest = Nothing
, configDbPreparedStatements = True
, configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"]
, configDbConfig = False
, configDbPreConfig = Nothing
, configDbUri = "postgresql://"
, configFilePath = Nothing
, configJWKS = rightToMaybe $ parseSecret secret
, configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"]
, configJwtSecret = Just secret
, configJwtSecretIsBase64 = False
, configJwtCacheMaxEntries = 10
, configLogLevel = LogCrit
, configLogQuery = False
, configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing
, configServerCorsAllowedOrigins = Nothing
, configServerHost = "localhost"
, configServerPort = 3000
, configServerTraceHeader = Nothing
, configServerUnixSocket = Nothing
, configServerUnixSocketMode = 432
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerHost = "localhost"
, configAdminServerPort = Nothing
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCQuerySleep = Nothing
, configServerTimingEnabled = True
}
testCfg :: AppConfig
testCfg = baseCfg
testCfgJwtCache :: AppConfig
testCfgJwtCache =
baseCfg {
configJwtSecret = Just generateSecret
, configJWKS = rightToMaybe $ parseSecret generateSecret
, configJwtCacheMaxEntries = 2
}
authHeader :: BS.ByteString -> BS.ByteString -> Header
authHeader typ creds =
(hAuthorization, typ <> " " <> creds)
authHeaderJWT :: BS.ByteString -> Header
authHeaderJWT = authHeader "Bearer"
generateSecret :: ByteString
generateSecret = B64.decodeLenient "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
generateJWT :: BL.ByteString -> ByteString
generateJWT claims =
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
-- state check helpers
stateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m
stateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)
expectField :: forall s st a c m. (KnownSymbol s, Show a, Eq a, HasField s st c) => (c -> m a) -> (a -> a) -> StateCheck st m
expectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)
checkState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()
checkState' initialState checks act = do
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks
void act
sequenceA_ expectations
where
createExpectation msg metrics expect = pure $ metrics >>= liftIO . annotate msg . expect
expectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m) => (Int -> Int) -> StateCheck st m
expectCounter = expectField @s intCounter
where
intCounter = ((round @Double @Int) <$>) . getCounter
accumulateUntilTimeout :: Int -> (s -> a -> s) -> s -> IO a -> IO s
accumulateUntilTimeout t f start act = do
tid <- myThreadId
-- mask to make sure TimeoutException is not thrown before starting the loop
mask $ \unmask -> do
-- start timeout thread unmasking exceptions
ttid <- forkIOWithUnmask ($ (threadDelay t *> throwTo tid TimeoutException))
-- unmask effect
unmask (fix (\loop accum -> (act >>= loop . f accum) `onTimeout` pure accum) start)
-- make sure we catch timeout if happens before entering the loop
`onTimeout` pure start
-- make sure timer thread is killed on other exceptions
-- so that it won't throw TimeoutException later
`onException` killThread ttid
where
onTimeout m a = m `catch` \TimeoutException -> a
newObsChan :: Chan Observation -> IO ObsChan
newObsChan = fmap <$> ObsChan <*> dupChan
-- read messages from copy chan and once condition is met drain original to the same point
-- upon timeout report error and messages remaining in the original chan
-- that way we report messages since last successful read
waitForObs :: HasCallStack => ObsChan -> Int -> Text -> (Observation -> Maybe a) -> IO ()
waitForObs (ObsChan orig copy) t msg f =
timeout t (readUntil copy *> readUntil orig) >>= maybe failTimeout mempty
where
failTimeout = takeUntilTimeout decisecond (readChan orig)
>>= expectationFailure . DL.unlines . fmap show . (failureMessageHeader :) . fmap obsDiagMessage
failureMessageHeader = "Timeout waiting for " <> msg <> " at " <> loc <> ". Remaining observations:"
readUntil = void . untilM (pure . not . null . f) . readChan
loc = fromMaybe "(unknown)" . head $ (T.pack . prettySrcLoc . snd <$> getCallStack callStack)
-- execute effectful computation until result meets provided condition
untilM cond m = fix $ \loop -> m >>= \a -> ifM (cond a) (pure a) loop
-- duplicate the provided channel and construct wairFor function binding both channels
-- accumulate effecful computation results into a list for specified time
takeUntilTimeout t' = fmap reverse . accumulateUntilTimeout t' (flip (:)) []
decisecond = 100000
obsDiagMessage :: Observation -> Text
obsDiagMessage = \case
(HasqlPoolObs o) -> show o
o@(DBListenStart host port name channel) -> constrName o <> show (host, port, name, channel)
o -> constrName o
@@ -1,33 +1,24 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Feature.Auth.JwtCacheSpec
where
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
module Observation.JwtCache where
import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec (Expectation, SpecWith, describe, it,
shouldBe)
import Test.Hspec (SpecWith, describe, it)
import Test.Hspec.Wai
import Data.String (String)
import PostgREST.Metrics (MetricsState (..))
import Prometheus (getCounter)
import ObsHelper
import PostgREST.Metrics (MetricsState (..))
import Protolude
import SpecHelper
import Test.Hspec.Expectations.Contrib (annotate)
import Test.Hspec.Wai.JSON (json)
import Test.Hspec.Wai.JSON (json)
spec :: SpecWith (MetricsState, Application)
spec :: SpecWith (SpecState, Application)
spec = describe "Server started with JWT and metrics enabled" $ do
it "Should not have JWT in cache" $ do
expectCounters <- checkState' . specMetrics <$> getState
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe1"}|]
expectCounters
@@ -36,9 +27,11 @@ spec = describe "Server started with JWT and metrics enabled" $ do
, hits (+ 0)
] $
request methodGet "/authors_only" [auth] ""
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
it "Should have JWT in cache" $ do
expectCounters <- checkState' . specMetrics <$> getState
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe2"}|]
expectCounters
@@ -51,6 +44,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
it "Should not cache invalid JWTs" $ do
expectCounters <- checkState' . specMetrics <$> getState
let auth = authHeaderJWT "some random bytes"
expectCounters
@@ -63,6 +58,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
it "Should cache expired JWTs" $ do
expectCounters <- checkState' . specMetrics <$> getState
let auth = genToken [json|{"exp": 1, "role": "postgrest_test_author", "id": "jdoe2"}|]
expectCounters
@@ -75,6 +72,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
it "Should evict entries from the JWT cache (jwt cache max is 2)" $ do
expectCounters <- checkState' . specMetrics <$> getState
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe3"}|]
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe4"}|]
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe5"}|]
@@ -94,6 +93,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
*> request methodGet "/authors_only" [jwt3] ""
it "Should not evict entries from the JWT cache in FIFO order" $ do
expectCounters <- checkState' . specMetrics <$> getState
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe6"}|]
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe7"}|]
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe8"}|]
@@ -120,6 +121,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
-- The test case was added based on coverage report
-- showing this scenario was not covered by previous tests
it "Should evict entries even though all were hit" $ do
expectCounters <- checkState' . specMetrics <$> getState
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe9"}|]
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe10"}|]
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe11"}|]
@@ -143,25 +146,7 @@ spec = describe "Server started with JWT and metrics enabled" $ do
*> request methodGet "/authors_only" [jwt3] ""
where
counterToInt = second (fmap (round @Double @Int) . getCounter)
expectCounters = stateCheck . fmap (\(g, h) -> StateCheck (counterToInt . g) (flip shouldBe . h))
genToken = authHeaderJWT . generateJWT
requests = (,) (getF @"jwtCacheRequests")
hits = (,) (getF @"jwtCacheHits")
evictions = (,) (getF @"jwtCacheEvictions")
-- should be moved to helpers???
getF :: forall s r a. (KnownSymbol s, HasField s r a) => r -> (String, a)
getF r = (symbolVal (Proxy @s), getField @s r)
data StateCheck st = forall a. (Show a, Eq a) => StateCheck (st -> (String, WaiSession st a)) (a -> a -> Expectation)
stateCheck :: (Traversable t) => t (StateCheck st) -> WaiSession st a -> WaiSession st ()
stateCheck checks act = do
metrics <- getState
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g metrics in m >>= createExpectation msg m . expect) checks
void act
sequenceA_ expectations
where
createExpectation msg metrics expect = pure $ metrics >>= liftIO . annotate msg . expect
requests = expectCounter @"jwtCacheRequests"
hits = expectCounter @"jwtCacheHits"
evictions = expectCounter @"jwtCacheEvictions"
@@ -0,0 +1,114 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
module Observation.MetricsSpec where
import Data.List (lookup)
import qualified Hasql.Pool.Observation as SQL
import Network.Wai (Application)
import ObsHelper
import qualified PostgREST.AppState as AppState
import PostgREST.Config (AppConfig (configDbSchemas))
import PostgREST.Metrics (ConnStats (..),
MetricsState (..),
connectionCounts)
import PostgREST.Observation
import Prometheus (getCounter, getVectorWith)
import Test.Hspec (SpecWith, describe, it)
import Test.Hspec.Wai (getState)
import Protolude
spec :: SpecWith (SpecState, Application)
spec = describe "Server started with metrics enabled" $ do
it "Should update pgrst_schema_cache_loads_total[SUCCESS]" $ do
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ checkState' metrics [
schemaCacheLoads "SUCCESS" (+1)
] $ do
AppState.schemaCacheLoader appState
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
it "Should update pgrst_schema_cache_loads_total[ERROR]" $ do
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ checkState' metrics [
schemaCacheLoads "FAIL" (+1),
schemaCacheLoads "SUCCESS" (+1)
] $ do
AppState.getConfig appState >>= \prev -> do
AppState.putConfig appState $ prev { configDbSchemas = pure "bad_schema" }
AppState.schemaCacheLoader appState
waitFor (1 * sec) "SchemaCacheErrorObs" $ \x -> [ o | o@(SchemaCacheErrorObs{}) <- pure x]
AppState.putConfig appState prev
-- wait up to 2 secs so that retry can happen
waitFor (2 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
it "Should debounce schema cache loads" $ do
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ checkState' metrics [
-- we expect exactly 2 successful schema cache loads
schemaCacheLoads "FAIL" (+0),
schemaCacheLoads "SUCCESS" (+2)
] $ do
AppState.schemaCacheLoader appState
-- at this moment there is no dedicated observation emited
-- when schema cache load starts
-- so we wait for DBConnectedObs which is emited right after successful
-- PostgreSQL version query during schema cache loading
waitFor (1 * sec) "DBConnectedObs" $ \x -> [ o | o@(DBConnectedObs{}) <- pure x]
-- request schema cache load multiple times
-- all of them should be handled by a single schema cache load
replicateM_ 100 (AppState.schemaCacheLoader appState)
-- wait for two expected SchemaCacheLoadedObs events
replicateM_ 2 $ waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
-- wait 1 sec to make sure we capture all potential schema cache loads
-- (there should be none but we need to verify that)
threadDelay $ 1 * sec
-- The test verifies we properly count in use connections
-- The idea is to fork a worker thread that
-- borrows connection from the pool and waits for a signal to release it
-- Main thread checks that
-- in use connections counter is incremented by worker
-- then it signals the worker to release the connection
-- and finally verifies that in use connection counter is back to original value
it "Should track in use connections" $ do
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ checkState' metrics [
-- we expect in use connections to be the same once finished
inUseConnections (+ 0)
] $ do
signal <- newEmptyMVar
-- make sure waiting thread is signaled
(`finally` tryPutMVar signal ()) $
-- expecting one more connection in use
checkState' metrics [
inUseConnections (+ 1)
] $ do
-- start a thread hanging on a single connection until signaled
void $ forkIO $ void $ AppState.usePool appState $ liftIO (readMVar signal)
-- main thread waits for ConnectionObservation with InUseConnectionStatus
-- after which used connections count should be incremented
waitFor (1 * sec) "InUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ SQL.InUseConnectionStatus)) <- pure x]
-- hanging thread was signaled and should return the connection
waitFor (1 * sec) "ReadyForUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ SQL.ReadyForUseConnectionStatus)) <- pure x]
where
-- prometheus-client api to handle vectors is convoluted
schemaCacheLoads label = expectField @"schemaCacheLoads" $
fmap (maybe (0::Int) round . lookup label) . (`getVectorWith` getCounter)
inUseConnections = expectField @"connTrack" ((inUse <$>) . connectionCounts)
sec = 1000000
@@ -0,0 +1,49 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE NamedFieldPuns #-}
module Observation.SchemaCacheSpec where
import Network.Wai (Application)
import ObsHelper
import qualified PostgREST.AppState as AppState
import PostgREST.Config (configDbSchemas)
import PostgREST.Observation
import Protolude
import Test.Hspec (SpecWith, describe, it)
import Test.Hspec.Wai (getState)
spec :: SpecWith (SpecState, Application)
spec = describe "Server started with metrics enabled" $ do
it "Should emit PoolFlushed, SchemaCacheQueriedObs and SchemaCacheLoadedObs when schema cache is reloaded" $ do
SpecState{specAppState = appState, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ do
AppState.schemaCacheLoader appState
waitFor (1 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
it "Should flush pool once when schema reloading retries" $ do
SpecState{specAppState = appState, specObsChan} <- getState
let waitFor = waitForObs specObsChan
liftIO $ do
AppState.getConfig appState >>= \cfg -> do
AppState.putConfig appState $ cfg { configDbSchemas = pure "bad_schema" }
AppState.schemaCacheLoader appState
waitFor (1 * sec) "SchemaCacheErrorObs" $ \x -> [ o | o@SchemaCacheErrorObs{} <- pure x ]
-- Restore configuration
AppState.putConfig appState cfg
-- Wait for 2 seconds so that retry can happen
waitFor (2 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
where
sec = 1000000
+2
View File
@@ -0,0 +1,2 @@
-- Suppress NOTICE: ... messages
SET client_min_messages TO warning;
+8
View File
@@ -0,0 +1,8 @@
-- Loads all fixtures for the PostgREST observability tests
\set ON_ERROR_STOP on
\ir database.sql
\ir roles.sql
\ir schema.sql
\ir privileges.sql
@@ -0,0 +1,9 @@
-- Schema test objects
SET search_path = test, pg_catalog;
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test TO postgrest_test_anonymous;
REVOKE ALL PRIVILEGES ON TABLE authors_only FROM postgrest_test_anonymous;
GRANT USAGE ON SCHEMA test TO postgrest_test_author;
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
+5
View File
@@ -0,0 +1,5 @@
DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_author;
CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_author;
GRANT postgrest_test_anonymous, postgrest_test_author TO :"PGUSER";
+21
View File
@@ -0,0 +1,21 @@
DROP SCHEMA IF EXISTS test;
CREATE SCHEMA test;
SET search_path = test, pg_catalog;
--
-- Name: authors_only; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE authors_only (
owner character varying NOT NULL,
secret character varying NOT NULL
);
--
-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY authors_only
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
+1 -1
View File
@@ -25,7 +25,7 @@ spec =
raceTest 10 $
get "/fakefake"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fakefake' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.fakefake' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
}
+12 -12
View File
@@ -157,7 +157,7 @@ spec = describe "OpenAPI" $ do
"properties": {
"id": {
"description": "child_entities id comment\n\nNote:\nThis is a Primary Key.<pk/>",
"format": "integer",
"format": "int32",
"type": "integer"
},
"name": {
@@ -167,7 +167,7 @@ spec = describe "OpenAPI" $ do
},
"parent_id": {
"description": "Note:\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>",
"format": "integer",
"format": "int32",
"type": "integer"
}
},
@@ -192,7 +192,7 @@ spec = describe "OpenAPI" $ do
"properties": {
"id": {
"description": "child_entities_view id comment\n\nNote:\nThis is a Primary Key.<pk/>",
"format": "integer",
"format": "int32",
"type": "integer"
},
"name": {
@@ -202,7 +202,7 @@ spec = describe "OpenAPI" $ do
},
"parent_id": {
"description": "Note:\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>",
"format": "integer",
"format": "int32",
"type": "integer"
}
}
@@ -232,7 +232,7 @@ spec = describe "OpenAPI" $ do
referralLink `shouldBe` Just
[aesonQQ|
{
"format": "integer",
"format": "int32",
"type": "integer",
"description": "Note:\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>"
}
@@ -337,7 +337,7 @@ spec = describe "OpenAPI" $ do
referralLink `shouldBe` Just
[aesonQQ|
{
"format": "integer",
"format": "int32",
"type": "integer",
"description": "Note:\nThis is a Foreign Key to `pages.link`.<fk table='pages' column='link'/>"
}
@@ -354,7 +354,7 @@ spec = describe "OpenAPI" $ do
referralLink `shouldBe` Just
[aesonQQ|
{
"format": "integer",
"format": "int32",
"type": "integer",
"description": "Note:\nThis is a Foreign Key to `clients.id`.<fk table='clients' column='id'/>"
}
@@ -432,7 +432,7 @@ spec = describe "OpenAPI" $ do
types `shouldBe` Just
[aesonQQ|
{
"format": "smallint",
"format": "int32",
"type": "integer"
}
|]
@@ -447,7 +447,7 @@ spec = describe "OpenAPI" $ do
types `shouldBe` Just
[aesonQQ|
{
"format": "integer",
"format": "int32",
"type": "integer"
}
|]
@@ -462,7 +462,7 @@ spec = describe "OpenAPI" $ do
types `shouldBe` Just
[aesonQQ|
{
"format": "bigint",
"format": "int64",
"type": "integer"
}
|]
@@ -850,7 +850,7 @@ spec = describe "OpenAPI" $ do
"type": "string"
},
{
"format": "integer",
"format": "int32",
"in": "query",
"name": "integer",
"required": false,
@@ -992,7 +992,7 @@ spec = describe "OpenAPI" $ do
"items": {}
},
"integer": {
"format": "integer",
"format": "int32",
"type": "integer"
},
"json": {
+1 -1
View File
@@ -116,7 +116,7 @@ spec =
it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] ""
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.foo'","message":"Could not find the table 'test.foozle' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.foozle' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
}
+29 -2
View File
@@ -42,10 +42,10 @@ pgErrorCodeMapping = do
it "works with SchemaCache error" $
get "/non_existent_table"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.collision_test_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "182" ]
, "Content-Length" <:> "129" ]
}
it "works with Jwt error" $ do
@@ -75,3 +75,30 @@ pgErrorCodeMapping = do
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=123"
, "Content-Length" <:> "59" ]
}
context "show hint on PGRST205 table not found error" $ do
it "show hint when similarity score is at least 75%" $ do
get "/projectx" -- at least 75% similar to "projects"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.projects'","message":"Could not find the table 'test.projectx' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "160" ]
}
get "/projecxx" -- at least 75% similar to "projects"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.projects'","message":"Could not find the table 'test.projecxx' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "160" ]
}
it "don't show hint when similarity score is less than 75%" $
get "/projxxxx" -- less than 75% similar to "projects"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.projxxxx' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "119" ]
}
+1 -1
View File
@@ -483,7 +483,7 @@ spec actualPgVersion = do
{"id": 204, "body": "yyy"},
{"id": 205, "body": "zzz"}]|]
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.garlic' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
}
@@ -281,7 +281,7 @@ spec =
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"format" : "int32",
"type" : "integer"
},
"name" : {
@@ -310,7 +310,7 @@ spec =
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"format" : "int32",
"type" : "integer"
},
"name" : {
@@ -339,7 +339,7 @@ spec =
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"format" : "int32",
"type" : "integer"
},
"name" : {
@@ -368,7 +368,7 @@ spec =
"properties" : {
"id" : {
"description" : "Note:\nThis is a Primary Key.<pk/>",
"format" : "integer",
"format" : "int32",
"type" : "integer"
},
"another_value" : {
+4 -4
View File
@@ -26,9 +26,9 @@ spec = do
it "causes a 404" $
get "/faketable"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.private_table'","message":"Could not find the table 'test.faketable' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.faketable' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = ["Content-Length" <:> "166"]
, matchHeaders = ["Content-Length" <:> "120"]
}
describe "Filtering response" $ do
@@ -852,7 +852,7 @@ spec = do
-- the existence of first table, #3869
it "table not found error if first table does not exist" $
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.car_model_sales'","message":"Could not find the table 'test.car_model_sales_202101' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.car_model_sales_202101' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
@@ -870,7 +870,7 @@ spec = do
it "table not found error if first table does not exist" $
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.car_model_sales'","message":"Could not find the table 'test.car_models_default' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.car_models_default' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
+4 -4
View File
@@ -18,9 +18,9 @@ spec = do
request methodPatch "/fake" []
[json| { "real": false } |]
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fake' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.fake' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = ["Content-Length" <:> "157"]
, matchHeaders = ["Content-Length" <:> "115"]
}
@@ -363,9 +363,9 @@ spec = do
{"id": 204, "body": "yyy"},
{"id": 205, "body": "zzz"}]|]
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
[json| {"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'test.garlic' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = ["Content-Length" <:> "158"]
, matchHeaders = ["Content-Length" <:> "117"]
}
context "apply defaults on missing values" $ do
+1 -6
View File
@@ -23,7 +23,6 @@ import qualified Feature.Auth.AsymmetricJwtSpec
import qualified Feature.Auth.AudienceJwtSecretSpec
import qualified Feature.Auth.AuthSpec
import qualified Feature.Auth.BinaryJwtSecretSpec
import qualified Feature.Auth.JwtCacheSpec
import qualified Feature.Auth.NoAnonSpec
import qualified Feature.Auth.NoJwtSecretSpec
import qualified Feature.ConcurrentSpec
@@ -84,13 +83,12 @@ main = do
-- cached schema cache so most tests run fast
baseSchemaCache <- loadSCache pool testCfg
sockets <- AppState.initSockets testCfg
loggerState <- Logger.init
metricsState <- Metrics.init (configDbPoolSize testCfg)
let
initApp sCache st config = do
appState <- AppState.initWithPool sockets pool config loggerState metricsState (Metrics.observationMetrics metricsState)
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState)
AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just sCache)
return (st, postgrest (configLogLevel config) appState (pure ()))
@@ -275,9 +273,6 @@ main = do
before pgSafeUpdateApp $
describe "Feature.Query.PgSafeUpdateSpec.spec" Feature.Query.PgSafeUpdateSpec.spec
before (initApp baseSchemaCache metricsState testCfgJwtCache) $
describe "Feature.Auth.JwtCacheSpec" Feature.Auth.JwtCacheSpec.spec
where
loadSCache pool conf =
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)
-11
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE FlexibleContexts #-}
module SpecHelper where
import Control.Lens ((^?))
@@ -158,8 +157,6 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCQuerySleep = Nothing
, configInternalSCLoadSleep = Nothing
, configInternalSCRelLoadSleep = Nothing
, configServerTimingEnabled = True
}
@@ -206,14 +203,6 @@ testCfgBinaryJWT =
, configJWKS = rightToMaybe $ parseSecret generateSecret
}
testCfgJwtCache :: AppConfig
testCfgJwtCache =
baseCfg {
configJwtSecret = Just generateSecret
, configJWKS = rightToMaybe $ parseSecret generateSecret
, configJwtCacheMaxEntries = 2
}
testCfgAudienceJWT :: AppConfig
testCfgAudienceJWT =
baseCfg {

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