Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e07807deab | ||
|
|
bd50b1e4d3 | ||
|
|
f26cdd5151 | ||
|
|
b869dd7be9 | ||
|
|
97a4402911 | ||
|
|
a101d27c9c | ||
|
|
519dbc75f3 | ||
|
|
ae3c784921 | ||
|
|
f56bed2a75 | ||
|
|
af8e436732 | ||
|
|
98a29bee04 | ||
|
|
557285b659 | ||
|
|
81501aefa0 | ||
|
|
12c1d4a8e4 | ||
|
|
8aa7368786 | ||
|
|
9d4ff812c9 | ||
|
|
171dd313d9 | ||
|
|
fd24a7374b | ||
|
|
a525790c4c |
@@ -12,6 +12,8 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: cachix/install-nix-action@v18
|
- uses: cachix/install-nix-action@v18
|
||||||
|
with:
|
||||||
|
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: cachix/cachix-action@v12
|
||||||
with:
|
with:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io
|
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io
|
||||||
|
|
||||||
- name: Run query cost tests
|
|
||||||
if: always()
|
|
||||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-querycost
|
|
||||||
|
|
||||||
|
|
||||||
Test-Memory-Nix:
|
Test-Memory-Nix:
|
||||||
name: Test memory (Nix)
|
name: Test memory (Nix)
|
||||||
@@ -253,7 +249,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- id: Remote-Dir
|
- id: Remote-Dir
|
||||||
name: Unique directory name for the remote build
|
name: Unique directory name for the remote build
|
||||||
run: echo "::set-output name=remotepath::postgrest-build-$(uuidgen)"
|
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
||||||
- name: Copy script files to the remote server
|
- name: Copy script files to the remote server
|
||||||
uses: appleboy/scp-action@master
|
uses: appleboy/scp-action@master
|
||||||
with:
|
with:
|
||||||
@@ -326,14 +322,14 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Version to be released is $cabal_version"
|
echo "Version to be released is $cabal_version"
|
||||||
echo "::set-output name=version::$cabal_version"
|
echo "version=$cabal_version" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$cabal_version" != *.*.*.* ]]; then
|
if [[ "$cabal_version" != *.*.*.* ]]; then
|
||||||
echo "Version is for a full release (version does not have four components)"
|
echo "Version is for a full release (version does not have four components)"
|
||||||
else
|
else
|
||||||
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
||||||
echo "::set-output name=isprerelease::1"
|
echo "isprerelease=1" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
- name: Identify changes from CHANGELOG.md
|
- name: Identify changes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+26
-1
@@ -3,7 +3,32 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## Unreleased
|
## [10.2.0] - 2023-04-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #2663, Limit maximal postgresql connection lifetime - @robx
|
||||||
|
+ New option `db-pool-max-lifetime` (default 30m)
|
||||||
|
+ `db-pool-acquisition-timeout` is no longer optional and defaults to 10s
|
||||||
|
+ Fixes postgresql resource leak with long-lived connections (#2638)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2667, Fix `db-pool-acquisition-timeout` not logging to stderr when the timeout is reached - @steve-chavez
|
||||||
|
|
||||||
|
## [10.1.2] - 2023-02-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2565, Fix bad M2M embedding on RPC - @steve-chavez
|
||||||
|
- #2575, Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions - @laurenceisla
|
||||||
|
- #2582, Move explanation about "single parameters" from the `message` to the `details` in the error output - @laurenceisla
|
||||||
|
- #2569, Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions - @laurenceisla
|
||||||
|
- #1405, Add the required OpenAPI items object when the parameter is an array - @laurenceisla
|
||||||
|
- #2592, Add upsert headers for POST requests to the OpenAPI output - @laurenceisla
|
||||||
|
- #2623, Fix FK pointing to VIEW instead of TABLE in OpenAPI output - @laurenceisla
|
||||||
|
- #2622, Consider any PostgreSQL authentication failure as fatal and exit immediately - @michivi
|
||||||
|
- #2620, Fix `NOTIFY pgrst` not reloading the db connections catalog cache - @steve-chavez
|
||||||
|
|
||||||
## [10.1.1] - 2022-11-08
|
## [10.1.1] - 2022-11-08
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ packages: .
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/PostgREST/postgresql-libpq.git
|
location: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
tag: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
tag: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
|
|||||||
@@ -208,12 +208,6 @@ postgrest-loadtest-against master
|
|||||||
postgrest-loadtest-report
|
postgrest-loadtest-report
|
||||||
```
|
```
|
||||||
|
|
||||||
Our query cost tests ensure that our generated queries don't surpass a threshold EXPLAIN cost.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
postgrest-test-querycost
|
|
||||||
```
|
|
||||||
|
|
||||||
doctests for some of our modules are also available:
|
doctests for some of our modules are also available:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||||
{
|
{
|
||||||
date = "2022-10-28";
|
date = "2023-01-12";
|
||||||
rev = "f44ba1be526c8da9e79a5759feca2365204003f6";
|
rev = "92f9580a4c369b4b51a7b6a5e77da43720134c9f";
|
||||||
tarballHash = "0npbwsdjw88py5w2pjflwh94wgi4jmnmls0k1n7q8m6h94w1y1ps";
|
tarballHash = "0w9bz4f2bmkj4a59n4z279zcgs9clyc40a4ny312rafyaknzghvw";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,33 +29,33 @@ let
|
|||||||
# To fill in the sha256:
|
# To fill in the sha256:
|
||||||
# update-nix-fetchgit nix/overlays/haskell-packages.nix
|
# update-nix-fetchgit nix/overlays/haskell-packages.nix
|
||||||
|
|
||||||
hashtables = lib.dontCheck prev.hashtables_1_3_1;
|
|
||||||
hasql = lib.dontCheck prev.hasql_1_6_1_4;
|
|
||||||
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_2;
|
|
||||||
hasql-pool = lib.dontCheck
|
|
||||||
(prev.callHackageDirect
|
|
||||||
{
|
|
||||||
pkg = "hasql-pool";
|
|
||||||
ver = "0.8.0.6";
|
|
||||||
sha256 = "sha256-2u/cwPk8XfXffaDRzGeyzhL+9k2+2T4b8bGOZwz8AX0=";
|
|
||||||
}
|
|
||||||
{ });
|
|
||||||
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_0_1_2;
|
|
||||||
isomorphism-class = lib.unmarkBroken prev.isomorphism-class;
|
|
||||||
lens = lib.dontCheck prev.lens_5_2;
|
|
||||||
postgresql-binary = lib.dontCheck prev.postgresql-binary_0_13_1;
|
|
||||||
text-builder = lib.dontCheck prev.text-builder_0_6_7;
|
|
||||||
text-builder-dev = lib.dontCheck prev.text-builder-dev_0_3_3;
|
|
||||||
|
|
||||||
postgresql-libpq = lib.dontCheck
|
postgresql-libpq = lib.dontCheck
|
||||||
(prev.callCabal2nix "postgresql-libpq"
|
(prev.callCabal2nix "postgresql-libpq"
|
||||||
(super.fetchFromGitHub {
|
(super.fetchFromGitHub {
|
||||||
owner = "PostgREST";
|
owner = "PostgREST";
|
||||||
repo = "postgresql-libpq";
|
repo = "postgresql-libpq";
|
||||||
rev = "cef92cb4c07b56568dffdbf4b719258b82183119"; # master
|
rev = "890a0a16cf57dd401420fdc6c7d576fb696003bc"; # master
|
||||||
sha256 = "0r59klrz47qcnd22s47h612mlz3jbg40wwalfj3f6djwg0cdyr85";
|
sha256 = "1wmyhldk0k14y8whp1p4akrkqxf5snh8qsbm7fv5f7kz95nyffd0";
|
||||||
})
|
})
|
||||||
{ });
|
{ });
|
||||||
|
|
||||||
|
hasql-notifications = lib.dontCheck
|
||||||
|
(prev.callHackageDirect
|
||||||
|
{
|
||||||
|
pkg = "hasql-notifications";
|
||||||
|
ver = "0.2.0.4";
|
||||||
|
sha256 = "sha256-fm1xiDyvDkb5WLOJ73/s8wrWEW23XFS7luAv2brfr8I=";
|
||||||
|
}
|
||||||
|
{ });
|
||||||
|
|
||||||
|
hasql-pool = lib.dontCheck
|
||||||
|
(prev.callHackageDirect
|
||||||
|
{
|
||||||
|
pkg = "hasql-pool";
|
||||||
|
ver = "0.9";
|
||||||
|
sha256 = "sha256-5UshbbaBVY8eJ/9VagNVVxonRwMcd7UmGqDc35pJNFY=";
|
||||||
|
}
|
||||||
|
{ });
|
||||||
} // extraOverrides final prev;
|
} // extraOverrides final prev;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
${tests}/bin/postgrest-test-spec
|
${tests}/bin/postgrest-test-spec
|
||||||
${tests}/bin/postgrest-test-querycost
|
|
||||||
${tests}/bin/postgrest-test-doctests
|
${tests}/bin/postgrest-test-doctests
|
||||||
${tests}/bin/postgrest-test-io
|
${tests}/bin/postgrest-test-io
|
||||||
${style}/bin/postgrest-lint
|
${style}/bin/postgrest-lint
|
||||||
@@ -165,6 +164,7 @@ let
|
|||||||
# The following unsets all GIT_ variables.
|
# The following unsets all GIT_ variables.
|
||||||
unset "''${!GIT_@}"
|
unset "''${!GIT_@}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2317
|
||||||
function restore () {
|
function restore () {
|
||||||
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||||
# this will avoid merge conflicts when applying the stash
|
# this will avoid merge conflicts when applying the stash
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ let
|
|||||||
echo "Tagging ..."
|
echo "Tagging ..."
|
||||||
git tag "v$new_version" > /dev/null
|
git tag "v$new_version" > /dev/null
|
||||||
|
|
||||||
trap "Couldn't find remote. Please push manually ..." ERR
|
trap "echo Remote not found. Please push manually ..." ERR
|
||||||
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
||||||
trap "" ERR
|
trap "" ERR
|
||||||
|
|
||||||
|
|||||||
+2
-18
@@ -32,18 +32,6 @@ let
|
|||||||
test:spec -- "''${_arg_leftovers[@]}"
|
test:spec -- "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
testQuerycost =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-test-querycost";
|
|
||||||
docs = "Run the Haskell test suite for query costs";
|
|
||||||
inRootDir = true;
|
|
||||||
withEnv = postgrest.env;
|
|
||||||
}
|
|
||||||
''
|
|
||||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
|
||||||
'';
|
|
||||||
|
|
||||||
testDoctests =
|
testDoctests =
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
@@ -140,7 +128,7 @@ let
|
|||||||
rm -rf coverage/*
|
rm -rf coverage/*
|
||||||
|
|
||||||
# build once before running all the tests
|
# build once before running all the tests
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:querycost
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
||||||
|
|
||||||
(
|
(
|
||||||
trap 'echo Found dead code: Check file list above.' ERR ;
|
trap 'echo Found dead code: Check file list above.' ERR ;
|
||||||
@@ -155,14 +143,11 @@ let
|
|||||||
HPCTIXFILE="$tmpdir"/spec.tix \
|
HPCTIXFILE="$tmpdir"/spec.tix \
|
||||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||||
|
|
||||||
HPCTIXFILE="$tmpdir"/querycost.tix \
|
|
||||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
|
||||||
|
|
||||||
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
|
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
|
||||||
|
|
||||||
# collect all the tix files
|
# collect all the tix files
|
||||||
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
|
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
|
||||||
"$tmpdir"/io*.tix "$tmpdir"/spec.tix "$tmpdir"/querycost.tix
|
"$tmpdir"/io*.tix "$tmpdir"/spec.tix
|
||||||
|
|
||||||
# prepare the overlay
|
# prepare the overlay
|
||||||
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
||||||
@@ -234,7 +219,6 @@ buildToolbox
|
|||||||
tools =
|
tools =
|
||||||
[
|
[
|
||||||
testSpec
|
testSpec
|
||||||
testQuerycost
|
|
||||||
testDoctests
|
testDoctests
|
||||||
testSpecIdempotence
|
testSpecIdempotence
|
||||||
testIO
|
testIO
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ let
|
|||||||
export PGDATABASE
|
export PGDATABASE
|
||||||
export PGRST_DB_SCHEMAS
|
export PGRST_DB_SCHEMAS
|
||||||
|
|
||||||
|
HBA_FILE="$tmpdir/pg_hba.conf"
|
||||||
|
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
|
||||||
|
echo "local $PGDATABASE all trust" >> "$HBA_FILE"
|
||||||
|
|
||||||
log "Initializing database cluster..."
|
log "Initializing database cluster..."
|
||||||
# We try to make the database cluster as independent as possible from the host
|
# We try to make the database cluster as independent as possible from the host
|
||||||
# by specifying the timezone, locale and encoding.
|
# by specifying the timezone, locale and encoding.
|
||||||
@@ -62,9 +66,10 @@ let
|
|||||||
|
|
||||||
log "Starting the database cluster..."
|
log "Starting the database cluster..."
|
||||||
# Instead of listening on a local port, we will listen on a unix domain socket.
|
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||||
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -k $PGHOST -c log_statement=\"all\"" \
|
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\"" \
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2317
|
||||||
stop () {
|
stop () {
|
||||||
log "Stopping the database cluster..."
|
log "Stopping the database cluster..."
|
||||||
pg_ctl stop -m i >> "$setuplog"
|
pg_ctl stop -m i >> "$setuplog"
|
||||||
@@ -256,6 +261,7 @@ let
|
|||||||
echo -n "Starting postgrest... "
|
echo -n "Starting postgrest... "
|
||||||
./result/bin/postgrest ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
./result/bin/postgrest ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
cleanup() {
|
cleanup() {
|
||||||
kill "$pid" || true
|
kill "$pid" || true
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-47
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 10.1.1
|
version: 10.2.0
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -72,7 +72,7 @@ library
|
|||||||
PostgREST.Workers
|
PostgREST.Workers
|
||||||
other-modules: Paths_postgrest
|
other-modules: Paths_postgrest
|
||||||
build-depends: base >= 4.9 && < 4.17
|
build-depends: base >= 4.9 && < 4.17
|
||||||
, HTTP >= 4000.3.7 && < 4000.4
|
, HTTP >= 4000.3.7 && < 4000.5
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 2.0.3 && < 2.2
|
, aeson >= 2.0.3 && < 2.2
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
@@ -85,11 +85,12 @@ library
|
|||||||
, contravariant-extras >= 0.3.3 && < 0.4
|
, contravariant-extras >= 0.3.3 && < 0.4
|
||||||
, cookie >= 0.4.2 && < 0.5
|
, cookie >= 0.4.2 && < 0.5
|
||||||
, either >= 4.4.1 && < 5.1
|
, either >= 4.4.1 && < 5.1
|
||||||
|
, fuzzyset >= 0.2.3
|
||||||
, gitrev >= 1.2 && < 1.4
|
, gitrev >= 1.2 && < 1.4
|
||||||
, hasql >= 1.6.1.1 && < 1.7
|
, hasql >= 1.6.1.1 && < 1.7
|
||||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-notifications >= 0.1 && < 0.3
|
, hasql-notifications >= 0.1 && < 0.3
|
||||||
, hasql-pool >= 0.8.0.6 && < 0.9
|
, hasql-pool >= 0.9 && < 0.10
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
@@ -97,11 +98,11 @@ library
|
|||||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||||
, jose >= 0.8.5.1 && < 0.11
|
, jose >= 0.8.5.1 && < 0.11
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, mtl >= 2.2.2 && < 2.3
|
, mtl >= 2.2.2 && < 2.3
|
||||||
, network >= 2.6 && < 3.2
|
, network >= 2.6 && < 3.2
|
||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.17
|
, optparse-applicative >= 0.13 && < 0.18
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
@@ -227,69 +228,32 @@ test-suite spec
|
|||||||
, bytestring >= 0.10.8 && < 0.12
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, hasql-pool >= 0.8.0.2 && < 0.9
|
, hasql-pool >= 0.9 && < 0.10
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.9
|
, hspec >= 2.3 && < 2.10
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
, monad-control >= 1.0.1 && < 1.1
|
||||||
, postgrest
|
, postgrest
|
||||||
, process >= 1.4.2 && < 1.7
|
, process >= 1.4.2 && < 1.7
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, text >= 1.2.2 && < 1.3
|
, text >= 1.2.2 && < 1.3
|
||||||
, transformers-base >= 0.4.4 && < 0.5
|
, transformers-base >= 0.4.4 && < 0.5
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
, wai-extra >= 3.0.19 && < 3.2
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
ghc-options: -O0 -Werror -Wall -fwarn-identities
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
-fno-warn-missing-signatures
|
-fno-warn-missing-signatures
|
||||||
-fwrite-ide-info
|
-fwrite-ide-info
|
||||||
-- https://github.com/PostgREST/postgrest/issues/387
|
-- https://github.com/PostgREST/postgrest/issues/387
|
||||||
-with-rtsopts=-K33K
|
-with-rtsopts=-K33K
|
||||||
|
|
||||||
test-suite querycost
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
default-language: Haskell2010
|
|
||||||
default-extensions: OverloadedStrings
|
|
||||||
QuasiQuotes
|
|
||||||
NoImplicitPrelude
|
|
||||||
hs-source-dirs: test/spec
|
|
||||||
main-is: QueryCost.hs
|
|
||||||
other-modules: SpecHelper
|
|
||||||
build-depends: base >= 4.9 && < 4.17
|
|
||||||
, aeson >= 2.0.3 && < 2.2
|
|
||||||
, base64-bytestring >= 1 && < 1.3
|
|
||||||
, bytestring >= 0.10.8 && < 0.12
|
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
|
||||||
, containers >= 0.5.7 && < 0.7
|
|
||||||
, contravariant >= 1.4 && < 1.6
|
|
||||||
, hasql >= 1.6 && < 1.7
|
|
||||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
|
||||||
, hasql-pool >= 0.8.0.2 && < 0.9
|
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
|
||||||
, heredoc >= 0.2 && < 0.3
|
|
||||||
, hspec >= 2.3 && < 2.9
|
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
|
||||||
, http-types >= 0.12.3 && < 0.13
|
|
||||||
, lens >= 4.14 && < 5.3
|
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
|
||||||
, postgrest
|
|
||||||
, process >= 1.4.2 && < 1.7
|
|
||||||
, protolude >= 0.3.1 && < 0.4
|
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
|
||||||
, wai-extra >= 3.0.19 && < 3.2
|
|
||||||
ghc-options: -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=-K1K
|
|
||||||
|
|
||||||
test-suite doctests
|
test-suite doctests
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ requestMediaTypes conf action path =
|
|||||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
||||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||||
case matchProc of
|
case matchProc of
|
||||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost
|
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
||||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||||
([], [proc]) -> Right proc
|
([], [proc]) -> Right proc
|
||||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||||
@@ -461,7 +461,9 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPo
|
|||||||
([proc], _) -> Right proc
|
([proc], _) -> Right proc
|
||||||
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
||||||
where
|
where
|
||||||
matchProc = overloadedProcPartition $ HM.lookupDefault mempty qi allProcs -- first find the proc by name
|
matchProc = overloadedProcPartition lookupProcName
|
||||||
|
-- First find the proc by name
|
||||||
|
lookupProcName = HM.lookupDefault mempty qi allProcs
|
||||||
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
||||||
-- where fallbackProcs are functions with a single unnamed parameter
|
-- where fallbackProcs are functions with a single unnamed parameter
|
||||||
overloadedProcPartition = foldr select ([],[])
|
overloadedProcPartition = foldr select ([],[])
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ parse qs =
|
|||||||
<*> (fmap snd <$> (pRequestFilter `traverse` filtersRoot))
|
<*> (fmap snd <$> (pRequestFilter `traverse` filtersRoot))
|
||||||
<*> pRequestFilter `traverse` filtersNotRoot
|
<*> pRequestFilter `traverse` filtersNotRoot
|
||||||
<*> pure (S.fromList (fst <$> filters))
|
<*> pure (S.fromList (fst <$> filters))
|
||||||
<*> sequenceA (pRequestOnConflict <$> onConflict)
|
<*> pRequestOnConflict `traverse` onConflict
|
||||||
where
|
where
|
||||||
logic = filter (endingIn ["and", "or"] . fst) nonemptyParams
|
logic = filter (endingIn ["and", "or"] . fst) nonemptyParams
|
||||||
select = fromMaybe "*" $ lookupParam "select"
|
select = fromMaybe "*" $ lookupParam "select"
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ module PostgREST.ApiRequest.Types
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier)
|
||||||
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Relationship)
|
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||||
|
RelationshipsMap)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -64,8 +66,8 @@ data ApiRequestError
|
|||||||
| InvalidRpcMethod ByteString
|
| InvalidRpcMethod ByteString
|
||||||
| LimitNoOrderError
|
| LimitNoOrderError
|
||||||
| NotFound
|
| NotFound
|
||||||
| NoRelBetween Text Text Text
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
| NoRpc Text Text [Text] Bool MediaType Bool
|
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [ProcDescription]
|
||||||
| NotEmbedded Text
|
| NotEmbedded Text
|
||||||
| ParseRequestError Text Text
|
| ParseRequestError Text Text
|
||||||
| PutRangeNotAllowedError
|
| PutRangeNotAllowedError
|
||||||
|
|||||||
+10
-4
@@ -9,6 +9,7 @@ Some of its functionality includes:
|
|||||||
- Producing HTTP Headers according to RFCs.
|
- Producing HTTP Headers according to RFCs.
|
||||||
- Content Negotiation
|
- Content Negotiation
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.App
|
module PostgREST.App
|
||||||
( SignalHandlerInstaller
|
( SignalHandlerInstaller
|
||||||
@@ -19,13 +20,14 @@ module PostgREST.App
|
|||||||
|
|
||||||
|
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||||
setServerName)
|
setServerName)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Handler.Warp as Warp
|
import qualified Network.Wai.Handler.Warp as Warp
|
||||||
@@ -153,9 +155,13 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jsonDbS pgVer aut
|
|||||||
|
|
||||||
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||||
runDbHandler appState mode authenticated prepared handler = do
|
runDbHandler appState mode authenticated prepared handler = do
|
||||||
dbResp <-
|
dbResp <- lift $ do
|
||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
||||||
lift . AppState.usePool appState . transaction SQL.ReadCommitted mode $ runExceptT handler
|
res <- AppState.usePool appState . transaction SQL.ReadCommitted mode $ runExceptT handler
|
||||||
|
whenLeft res (\case
|
||||||
|
SQL.AcquisitionTimeoutUsageError -> AppState.debounceLogAcquisitionTimeout appState -- this can happen rapidly for many requests, so we debounce
|
||||||
|
_ -> pure ())
|
||||||
|
return res
|
||||||
|
|
||||||
resp <-
|
resp <-
|
||||||
liftEither . mapLeft Error.PgErr $
|
liftEither . mapLeft Error.PgErr $
|
||||||
|
|||||||
+43
-20
@@ -16,6 +16,7 @@ module PostgREST.AppState
|
|||||||
, init
|
, init
|
||||||
, initWithPool
|
, initWithPool
|
||||||
, logWithZTime
|
, logWithZTime
|
||||||
|
, logPgrstError
|
||||||
, putConfig
|
, putConfig
|
||||||
, putSchemaCache
|
, putSchemaCache
|
||||||
, putIsListenerOn
|
, putIsListenerOn
|
||||||
@@ -25,13 +26,18 @@ module PostgREST.AppState
|
|||||||
, signalListener
|
, signalListener
|
||||||
, usePool
|
, usePool
|
||||||
, waitListener
|
, waitListener
|
||||||
|
, debounceLogAcquisitionTimeout
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Hasql.Session as SQL
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Hasql.Pool as SQL
|
||||||
|
import qualified Hasql.Session as SQL
|
||||||
|
import qualified PostgREST.Error as Error
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
|
import Control.Debounce
|
||||||
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
||||||
readIORef)
|
readIORef)
|
||||||
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||||
@@ -47,29 +53,31 @@ import Protolude
|
|||||||
|
|
||||||
data AppState = AppState
|
data AppState = AppState
|
||||||
-- | Database connection pool
|
-- | Database connection pool
|
||||||
{ statePool :: SQL.Pool
|
{ statePool :: SQL.Pool
|
||||||
-- | Database server version, will be updated by the connectionWorker
|
-- | Database server version, will be updated by the connectionWorker
|
||||||
, statePgVersion :: IORef PgVersion
|
, statePgVersion :: IORef PgVersion
|
||||||
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
||||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||||
-- | Cached SchemaCache in json
|
-- | Cached SchemaCache in json
|
||||||
, stateJsonDbS :: IORef ByteString
|
, stateJsonDbS :: IORef ByteString
|
||||||
-- | Binary semaphore to make sure just one connectionWorker can run at a time
|
-- | Binary semaphore to make sure just one connectionWorker can run at a time
|
||||||
, stateWorkerSem :: MVar ()
|
, stateWorkerSem :: MVar ()
|
||||||
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
||||||
, stateListener :: MVar ()
|
, stateListener :: MVar ()
|
||||||
-- | State of the LISTEN channel, used for the admin server checks
|
-- | State of the LISTEN channel, used for the admin server checks
|
||||||
, stateIsListenerOn :: IORef Bool
|
, stateIsListenerOn :: IORef Bool
|
||||||
-- | Config that can change at runtime
|
-- | Config that can change at runtime
|
||||||
, stateConf :: IORef AppConfig
|
, stateConf :: IORef AppConfig
|
||||||
-- | Time used for verifying JWT expiration
|
-- | Time used for verifying JWT expiration
|
||||||
, stateGetTime :: IO UTCTime
|
, stateGetTime :: IO UTCTime
|
||||||
-- | Time with time zone used for worker logs
|
-- | Time with time zone used for worker logs
|
||||||
, stateGetZTime :: IO ZonedTime
|
, stateGetZTime :: IO ZonedTime
|
||||||
-- | Used for killing the main thread in case a subthread fails
|
-- | Used for killing the main thread in case a subthread fails
|
||||||
, stateMainThreadId :: ThreadId
|
, stateMainThreadId :: ThreadId
|
||||||
-- | Keeps track of when the next retry for connecting to database is scheduled
|
-- | Keeps track of when the next retry for connecting to database is scheduled
|
||||||
, stateRetryNextIn :: IORef Int
|
, stateRetryNextIn :: IORef Int
|
||||||
|
-- | Logs a pool error with a debounce
|
||||||
|
, debounceLogAcquisitionTimeout :: IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
init :: AppConfig -> IO AppState
|
init :: AppConfig -> IO AppState
|
||||||
@@ -78,8 +86,8 @@ init conf = do
|
|||||||
initWithPool pool conf
|
initWithPool pool conf
|
||||||
|
|
||||||
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||||
initWithPool pool conf =
|
initWithPool pool conf = do
|
||||||
AppState pool
|
appState <- AppState pool
|
||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
<*> newIORef Nothing
|
<*> newIORef Nothing
|
||||||
<*> newIORef mempty
|
<*> newIORef mempty
|
||||||
@@ -91,16 +99,28 @@ initWithPool pool conf =
|
|||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
<*> myThreadId
|
<*> myThreadId
|
||||||
<*> newIORef 0
|
<*> newIORef 0
|
||||||
|
<*> pure (pure ())
|
||||||
|
|
||||||
|
deb <-
|
||||||
|
let oneSecond = 1000000 in
|
||||||
|
mkDebounce defaultDebounceSettings
|
||||||
|
{ debounceAction = logPgrstError appState SQL.AcquisitionTimeoutUsageError
|
||||||
|
, debounceFreq = 5*oneSecond
|
||||||
|
, debounceEdge = leadingEdge -- logs at the start and the end
|
||||||
|
}
|
||||||
|
|
||||||
|
return appState { debounceLogAcquisitionTimeout = deb }
|
||||||
|
|
||||||
destroy :: AppState -> IO ()
|
destroy :: AppState -> IO ()
|
||||||
destroy = destroyPool
|
destroy = destroyPool
|
||||||
|
|
||||||
initPool :: AppConfig -> IO SQL.Pool
|
initPool :: AppConfig -> IO SQL.Pool
|
||||||
initPool AppConfig{..} =
|
initPool AppConfig{..} =
|
||||||
SQL.acquire configDbPoolSize timeoutMilliseconds $ toUtf8 configDbUri
|
SQL.acquire
|
||||||
where
|
configDbPoolSize
|
||||||
timeoutMilliseconds = (* oneSecond) <$> configDbPoolAcquisitionTimeout
|
(fromIntegral configDbPoolAcquisitionTimeout)
|
||||||
oneSecond = 1000000
|
(fromIntegral configDbPoolMaxLifetime)
|
||||||
|
(toUtf8 configDbUri)
|
||||||
|
|
||||||
-- | Run an action with a database connection.
|
-- | Run an action with a database connection.
|
||||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||||
@@ -157,6 +177,9 @@ logWithZTime appState txt = do
|
|||||||
zTime <- stateGetZTime appState
|
zTime <- stateGetZTime appState
|
||||||
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||||
|
|
||||||
|
logPgrstError :: AppState -> SQL.UsageError -> IO ()
|
||||||
|
logPgrstError appState e = logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ Error.errorPayload $ Error.PgError False e
|
||||||
|
|
||||||
getMainThreadId :: AppState -> ThreadId
|
getMainThreadId :: AppState -> ThreadId
|
||||||
getMainThreadId = stateMainThreadId
|
getMainThreadId = stateMainThreadId
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ exampleConfigFile =
|
|||||||
|## Time in seconds to wait to acquire a slot from the connection pool
|
|## Time in seconds to wait to acquire a slot from the connection pool
|
||||||
|# db-pool-acquisition-timeout = 10
|
|# db-pool-acquisition-timeout = 10
|
||||||
|
|
|
|
||||||
|
|## Time in seconds after which to recycle pool connections
|
||||||
|
|# db-pool-max-lifetime = 1800
|
||||||
|
|
|
||||||
|## Stored proc to exec immediately after auth
|
|## Stored proc to exec immediately after auth
|
||||||
|# db-pre-request = "stored_proc_name"
|
|# db-pre-request = "stored_proc_name"
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ data AppConfig = AppConfig
|
|||||||
, configDbMaxRows :: Maybe Integer
|
, configDbMaxRows :: Maybe Integer
|
||||||
, configDbPlanEnabled :: Bool
|
, configDbPlanEnabled :: Bool
|
||||||
, configDbPoolSize :: Int
|
, configDbPoolSize :: Int
|
||||||
, configDbPoolAcquisitionTimeout :: Maybe Int
|
, configDbPoolAcquisitionTimeout :: Int
|
||||||
|
, configDbPoolMaxLifetime :: Int
|
||||||
, configDbPreRequest :: Maybe QualifiedIdentifier
|
, configDbPreRequest :: Maybe QualifiedIdentifier
|
||||||
, configDbPreparedStatements :: Bool
|
, configDbPreparedStatements :: Bool
|
||||||
, configDbRootSpec :: Maybe QualifiedIdentifier
|
, configDbRootSpec :: Maybe QualifiedIdentifier
|
||||||
@@ -130,7 +131,8 @@ toText conf =
|
|||||||
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
||||||
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
|
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
|
||||||
,("db-pool", show . configDbPoolSize)
|
,("db-pool", show . configDbPoolSize)
|
||||||
,("db-pool-acquisition-timeout", maybe "\"\"" show . configDbPoolAcquisitionTimeout)
|
,("db-pool-acquisition-timeout", show . configDbPoolAcquisitionTimeout)
|
||||||
|
,("db-pool-max-lifetime", show . configDbPoolMaxLifetime)
|
||||||
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
|
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
|
||||||
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
||||||
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
||||||
@@ -219,7 +221,8 @@ parser optPath env dbSettings =
|
|||||||
(optInt "max-rows")
|
(optInt "max-rows")
|
||||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||||
<*> (fromMaybe 10 <$> optInt "db-pool")
|
<*> (fromMaybe 10 <$> optInt "db-pool")
|
||||||
<*> optInt "db-pool-acquisition-timeout"
|
<*> (fromMaybe 10 <$> optInt "db-pool-acquisition-timeout")
|
||||||
|
<*> (fromMaybe 1800 <$> optInt "db-pool-max-lifetime")
|
||||||
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
||||||
(optString "pre-request"))
|
(optString "pre-request"))
|
||||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||||
@@ -355,7 +358,8 @@ parser optPath env dbSettings =
|
|||||||
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||||
if dbSettingName `notElem` [
|
if dbSettingName `notElem` [
|
||||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
|
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
|
||||||
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout", "db_config"]
|
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout",
|
||||||
|
"db_pool_max_lifetime", "db_config"]
|
||||||
then lookup dbSettingName dbSettings
|
then lookup dbSettingName dbSettings
|
||||||
else Nothing
|
else Nothing
|
||||||
|
|
||||||
|
|||||||
+133
-22
@@ -17,6 +17,8 @@ module PostgREST.Error
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.FuzzySet as Fuzzy
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Text.Encoding.Error as T
|
import qualified Data.Text.Encoding.Error as T
|
||||||
@@ -35,12 +37,14 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
|||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import qualified PostgREST.MediaType as MediaType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
|
Schema)
|
||||||
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
ProcParam (..))
|
ProcParam (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..),
|
||||||
|
RelationshipsMap)
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
@@ -151,36 +155,143 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON (NoRelBetween parent child schema) = JSON.object [
|
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode00,
|
"code" .= SchemaCacheErrorCode00,
|
||||||
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= ("Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."),
|
||||||
"hint" .= ("Verify that '" <> parent <> "' and '" <> child <> "' exist in the schema '" <> schema <> "' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache." :: Text)]
|
"hint" .= noRelBetweenHint parent child schema allRels]
|
||||||
|
|
||||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode01,
|
"code" .= SchemaCacheErrorCode01,
|
||||||
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||||
"details" .= (compressedRel <$> rels),
|
"details" .= (compressedRel <$> rels),
|
||||||
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
|
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
|
||||||
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
|
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
|
||||||
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
|
let func = schema <> "." <> procName
|
||||||
|
prms = T.intercalate ", " argumentKeys
|
||||||
|
prmsMsg = "(" <> prms <> ")"
|
||||||
|
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
||||||
|
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||||
|
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
|
||||||
|
in JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode02,
|
"code" .= SchemaCacheErrorCode02,
|
||||||
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
"message" .= ("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"),
|
||||||
|
"details" .= ("Searched for the function " <> func <>
|
||||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||||
(True, _, _) -> " function with a single json or jsonb parameter"
|
(True, _, _) -> " with a single json/jsonb parameter"
|
||||||
(_, True, MTTextPlain) -> " function with a single unnamed text parameter"
|
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||||
(_, True, MTTextXML) -> " function with a single unnamed xml parameter"
|
(_, True, MTTextXML) -> " with a single unnamed xml parameter"
|
||||||
(_, True, MTOctetStream) -> " function with a single unnamed bytea parameter"
|
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||||
(_, True, MTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||||
_ -> prms <> " function") <>
|
_ -> fmtPrms prmsDet) <>
|
||||||
" in the schema cache"),
|
", but no matches were found in the schema cache."),
|
||||||
"details" .= JSON.Null,
|
-- The hint will be null in the case of single unnamed parameter functions
|
||||||
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text)]
|
"hint" .= if onlySingleParams
|
||||||
|
then Nothing
|
||||||
|
else noRpcHint schema procName argumentKeys allProcs overloadedProcs ]
|
||||||
toJSON (AmbiguousRpc procs) = JSON.object [
|
toJSON (AmbiguousRpc procs) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode03,
|
"code" .= SchemaCacheErrorCode03,
|
||||||
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
|
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- If no relationship is found then:
|
||||||
|
--
|
||||||
|
-- Looks for parent suggestions if parent not found
|
||||||
|
-- Looks for child suggestions if parent is found but child is not
|
||||||
|
-- Gives no suggestions if both are found (it means that there is a problem with the embed hint)
|
||||||
|
--
|
||||||
|
-- >>> :set -Wno-missing-fields
|
||||||
|
-- >>> let qi t = QualifiedIdentifier "api" t
|
||||||
|
-- >>> let rel ft = Relationship{relForeignTable = qi ft}
|
||||||
|
-- >>> let rels = HM.fromList [((qi "films", "api"), [rel "directors", rel "roles", rel "actors"])]
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "film" "directors" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'films' instead of 'film'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "noclosealternative" "roles" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "noclosealternative" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "noclosealternative" "noclosealternative" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
noRelBetweenHint :: Text -> Text -> Schema -> RelationshipsMap -> Maybe Text
|
||||||
|
noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
|
||||||
|
if isJust findParent
|
||||||
|
then (<> "' instead of '" <> child <> "'.") <$> suggestChild
|
||||||
|
else (<> "' instead of '" <> parent <> "'.") <$> suggestParent
|
||||||
|
where
|
||||||
|
findParent = HM.lookup (QualifiedIdentifier schema parent, schema) allRels
|
||||||
|
fuzzySetOfParents = Fuzzy.fromList [qiName (fst p) | p <- HM.keys allRels, snd p == schema]
|
||||||
|
fuzzySetOfChildren = Fuzzy.fromList [qiName (relForeignTable c) | c <- fromMaybe [] findParent]
|
||||||
|
suggestParent = Fuzzy.getOne fuzzySetOfParents parent
|
||||||
|
-- Do not give suggestion if the child is found in the relations (weight = 1.0)
|
||||||
|
suggestChild = headMay [snd k | k <- Fuzzy.get fuzzySetOfChildren child, fst k < 1.0]
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- If no function is found with the given name, it does a fuzzy search to all the functions
|
||||||
|
-- in the same schema and shows the best match as hint.
|
||||||
|
--
|
||||||
|
-- >>> :set -Wno-missing-fields
|
||||||
|
-- >>> let procs = [(QualifiedIdentifier "api" "test"), (QualifiedIdentifier "api" "another"), (QualifiedIdentifier "private" "other")]
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "testt" ["val", "param", "name"] procs []
|
||||||
|
-- Just "Perhaps you meant to call the function api.test"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "other" [] procs []
|
||||||
|
-- Just "Perhaps you meant to call the function api.another"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "noclosealternative" [] procs []
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- If a function is found with the given name, but no params match, then it does a fuzzy search
|
||||||
|
-- to all the overloaded functions' params using the form "param1, param2, param3, ..."
|
||||||
|
-- and shows the best match as hint.
|
||||||
|
--
|
||||||
|
-- >>> let procsDesc = [ProcDescription {pdParams = [ProcParam {ppName="val"}, ProcParam {ppName="param"}, ProcParam {ppName="name"}]}, ProcDescription {pdParams = [ProcParam {ppName="id"}, ProcParam {ppName="attr"}]}]
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "test" ["vall", "pqaram", "nam"] procs procsDesc
|
||||||
|
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "test" ["val", "param"] procs procsDesc
|
||||||
|
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "test" ["id", "attrs"] procs procsDesc
|
||||||
|
-- Just "Perhaps you meant to call the function api.test(attr, id)"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "test" ["id"] procs procsDesc
|
||||||
|
-- Just "Perhaps you meant to call the function api.test(attr, id)"
|
||||||
|
--
|
||||||
|
-- >>> noRpcHint "api" "test" ["noclosealternative"] procs procsDesc
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
noRpcHint :: Text -> Text -> [Text] -> [QualifiedIdentifier] -> [ProcDescription] -> Maybe Text
|
||||||
|
noRpcHint schema procName params allProcs overloadedProcs =
|
||||||
|
fmap (("Perhaps you meant to call the function " <> schema <> ".") <>) possibleProcs
|
||||||
|
where
|
||||||
|
fuzzySetOfProcs = Fuzzy.fromList [qiName k | k <- allProcs, qiSchema k == schema]
|
||||||
|
fuzzySetOfParams = Fuzzy.fromList $ listToText <$> [[ppName prm | prm <- pdParams ov] | ov <- overloadedProcs]
|
||||||
|
-- Cannot do a fuzzy search like: Fuzzy.getOne [[Text]] [Text], where [[Text]] is the list of params for each
|
||||||
|
-- overloaded function and [Text] the given params. This converts those lists to text to make fuzzy search possible.
|
||||||
|
-- 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)
|
||||||
|
|
||||||
compressedRel :: Relationship -> JSON.Value
|
compressedRel :: Relationship -> JSON.Value
|
||||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||||
compressedRel ComputedRelationship{} = JSON.object mempty
|
compressedRel ComputedRelationship{} = JSON.object mempty
|
||||||
@@ -193,7 +304,7 @@ compressedRel Relationship{..} =
|
|||||||
: case relCardinality of
|
: case relCardinality of
|
||||||
M2M Junction{..} -> [
|
M2M Junction{..} -> [
|
||||||
"cardinality" .= ("many-to-many" :: Text)
|
"cardinality" .= ("many-to-many" :: Text)
|
||||||
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColumns2))
|
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColsSource) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColsTarget))
|
||||||
]
|
]
|
||||||
M2O cons relColumns -> [
|
M2O cons relColumns -> [
|
||||||
"cardinality" .= ("many-to-one" :: Text)
|
"cardinality" .= ("many-to-one" :: Text)
|
||||||
@@ -313,13 +424,13 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
|||||||
|
|
||||||
_ -> HTTP.status500
|
_ -> HTTP.status500
|
||||||
|
|
||||||
checkIsFatal :: PgError -> Maybe Text
|
checkIsFatal :: SQL.UsageError -> Maybe Text
|
||||||
checkIsFatal (PgError _ (SQL.ConnectionUsageError e))
|
checkIsFatal (SQL.ConnectionUsageError e)
|
||||||
| isAuthFailureMessage = Just $ toS failureMessage
|
| isAuthFailureMessage = Just $ toS failureMessage
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` failureMessage
|
where isAuthFailureMessage = "FATAL: password authentication failed" `isInfixOf` failureMessage
|
||||||
failureMessage = BS.unpack $ fromMaybe mempty e
|
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||||
checkIsFatal (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError))))
|
checkIsFatal(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError)))
|
||||||
= case serverError of
|
= case serverError of
|
||||||
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
||||||
SQL.ServerError "42601" _ _ _ _
|
SQL.ServerError "42601" _ _ _ _
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
|
|||||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||||
findRel schema allRels origin target hint =
|
findRel schema allRels origin target hint =
|
||||||
case rels of
|
case rels of
|
||||||
[] -> Left $ NoRelBetween origin target schema
|
[] -> Left $ NoRelBetween origin target hint schema allRels
|
||||||
[r] -> Right r
|
[r] -> Right r
|
||||||
rs -> Left $ AmbiguousRelBetween origin target rs
|
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||||
where
|
where
|
||||||
@@ -357,7 +357,7 @@ mutatePlan mutation qi ApiRequest{..} sCache readReq = mapLeft ApiRequestError $
|
|||||||
returnings =
|
returnings =
|
||||||
if iPreferRepresentation == None
|
if iPreferRepresentation == None
|
||||||
then []
|
then []
|
||||||
else returningCols readReq pkCols
|
else inferColsEmbedNeeds readReq pkCols
|
||||||
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
||||||
logic = map snd qsLogic
|
logic = map snd qsLogic
|
||||||
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
||||||
@@ -371,7 +371,7 @@ callPlan proc apiReq readReq = FunctionCall {
|
|||||||
, funCArgs = payRaw <$> iPayload apiReq
|
, funCArgs = payRaw <$> iPayload apiReq
|
||||||
, funCScalar = procReturnsScalar proc
|
, funCScalar = procReturnsScalar proc
|
||||||
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||||
, funCReturning = returningCols readReq []
|
, funCReturning = inferColsEmbedNeeds readReq []
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||||
@@ -382,14 +382,15 @@ callPlan proc apiReq readReq = FunctionCall {
|
|||||||
prms -> KeyParams $ specifiedParams prms
|
prms -> KeyParams $ specifiedParams prms
|
||||||
specifiedParams = filter (\x -> ppName x `S.member` iColumns apiReq)
|
specifiedParams = filter (\x -> ppName x `S.member` iColumns apiReq)
|
||||||
|
|
||||||
returningCols :: ReadPlanTree -> [FieldName] -> [FieldName]
|
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
||||||
returningCols rr@(Node _ forest) pkCols
|
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
|
||||||
|
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||||
-- if * is part of the select, we must not add pk or fk columns manually -
|
-- if * is part of the select, we must not add pk or fk columns manually -
|
||||||
-- otherwise those would be selected and output twice
|
-- otherwise those would be selected and output twice
|
||||||
| "*" `elem` fldNames = ["*"]
|
| "*" `elem` fldNames = ["*"]
|
||||||
| otherwise = returnings
|
| otherwise = returnings
|
||||||
where
|
where
|
||||||
fldNames = fstFieldNames rr
|
fldNames = (\((fld, _), _, _) -> fld) <$> select
|
||||||
-- Without fkCols, when a mutatePlan to
|
-- Without fkCols, when a mutatePlan to
|
||||||
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||||
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||||
@@ -403,8 +404,8 @@ returningCols rr@(Node _ forest) pkCols
|
|||||||
Just $ fst <$> cols
|
Just $ fst <$> cols
|
||||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2O _ cols}} _ ->
|
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2O _ cols}} _ ->
|
||||||
Just $ fst <$> cols
|
Just $ fst <$> cols
|
||||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2M Junction{junColumns1, junColumns2}}} _ ->
|
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2M Junction{junColsSource=cols}}} _ ->
|
||||||
Just $ (fst <$> junColumns1) ++ (fst <$> junColumns2)
|
Just $ fst <$> cols
|
||||||
Node ReadPlan{relToParent=Just ComputedRelationship{}} _ ->
|
Node ReadPlan{relToParent=Just ComputedRelationship{}} _ ->
|
||||||
Nothing
|
Nothing
|
||||||
Node ReadPlan{relToParent=Nothing} _ ->
|
Node ReadPlan{relToParent=Nothing} _ ->
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
module PostgREST.Plan.ReadPlan
|
module PostgREST.Plan.ReadPlan
|
||||||
( ReadPlanTree
|
( ReadPlanTree
|
||||||
, ReadPlan(..)
|
, ReadPlan(..)
|
||||||
, fstFieldNames
|
|
||||||
, JoinCondition(..)
|
, JoinCondition(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -45,8 +43,3 @@ data ReadPlan = ReadPlan
|
|||||||
-- ^ used for aliasing
|
-- ^ used for aliasing
|
||||||
}
|
}
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
|
||||||
fstFieldNames :: ReadPlanTree -> [FieldName]
|
|
||||||
fstFieldNames (Node ReadPlan{select} _) =
|
|
||||||
fst . (\(f, _, _) -> f) <$> select
|
|
||||||
|
|||||||
@@ -66,10 +66,16 @@ toSwaggerType "bigint" = Just SwaggerInteger
|
|||||||
toSwaggerType "numeric" = Just SwaggerNumber
|
toSwaggerType "numeric" = Just SwaggerNumber
|
||||||
toSwaggerType "real" = Just SwaggerNumber
|
toSwaggerType "real" = Just SwaggerNumber
|
||||||
toSwaggerType "double precision" = Just SwaggerNumber
|
toSwaggerType "double precision" = Just SwaggerNumber
|
||||||
toSwaggerType "ARRAY" = Just SwaggerArray
|
|
||||||
toSwaggerType "json" = Nothing
|
toSwaggerType "json" = Nothing
|
||||||
toSwaggerType "jsonb" = Nothing
|
toSwaggerType "jsonb" = Nothing
|
||||||
toSwaggerType _ = Just SwaggerString
|
toSwaggerType colType = case T.takeEnd 2 colType of
|
||||||
|
"[]" -> Just SwaggerArray
|
||||||
|
_ -> Just SwaggerString
|
||||||
|
|
||||||
|
makeSwaggerItemType :: Maybe (SwaggerType t) -> Text -> Maybe (Referenced Schema)
|
||||||
|
makeSwaggerItemType itemType colType = case itemType of
|
||||||
|
Just SwaggerArray -> Just $ Inline (mempty & type_ .~ toSwaggerType (T.dropEnd 2 colType))
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
parseDefault :: Text -> Text -> Text
|
parseDefault :: Text -> Text -> Text
|
||||||
parseDefault colType colDefault =
|
parseDefault colType colDefault =
|
||||||
@@ -97,11 +103,14 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
fk :: Maybe Text
|
fk :: Maybe Text
|
||||||
fk =
|
fk =
|
||||||
let
|
let
|
||||||
|
searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
||||||
|
-- Sorts the relationship list to get tables first
|
||||||
|
relsSortedByIsView = sortOn relFTableIsView [ r | r@Relationship{} <- searchedRels]
|
||||||
-- Finds the relationship that has a single column foreign key
|
-- Finds the relationship that has a single column foreign key
|
||||||
rel = find (\case
|
rel = find (\case
|
||||||
Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
|
Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
|
||||||
_ -> False
|
_ -> False
|
||||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
) relsSortedByIsView
|
||||||
fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
|
fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
|
||||||
fTbl = qiName . relForeignTable <$> rel
|
fTbl = qiName . relForeignTable <$> rel
|
||||||
fTblCol = (,) <$> fTbl <*> fCol
|
fTblCol = (,) <$> fTbl <*> fCol
|
||||||
@@ -119,6 +128,7 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||||
else
|
else
|
||||||
colDescription col
|
colDescription col
|
||||||
|
pType = toSwaggerType (colType col)
|
||||||
s =
|
s =
|
||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
||||||
@@ -126,7 +136,8 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
& enum_ .~ e
|
& enum_ .~ e
|
||||||
& format ?~ colType col
|
& format ?~ colType col
|
||||||
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
||||||
& type_ .~ toSwaggerType (colType col)
|
& type_ .~ pType
|
||||||
|
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType pType (colType col))
|
||||||
|
|
||||||
makeProcSchema :: ProcDescription -> Schema
|
makeProcSchema :: ProcDescription -> Schema
|
||||||
makeProcSchema pd =
|
makeProcSchema pd =
|
||||||
@@ -141,6 +152,7 @@ makeProcProperty (ProcParam n t _ _) = (n, Inline s)
|
|||||||
where
|
where
|
||||||
s = (mempty :: Schema)
|
s = (mempty :: Schema)
|
||||||
& type_ .~ toSwaggerType t
|
& type_ .~ toSwaggerType t
|
||||||
|
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType (toSwaggerType t) t)
|
||||||
& format ?~ t
|
& format ?~ t
|
||||||
|
|
||||||
makePreferParam :: [Text] -> Param
|
makePreferParam :: [Text] -> Param
|
||||||
@@ -152,7 +164,15 @@ makePreferParam ts =
|
|||||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
& in_ .~ ParamHeader
|
& in_ .~ ParamHeader
|
||||||
& type_ ?~ SwaggerString
|
& type_ ?~ SwaggerString
|
||||||
& enum_ .~ JSON.decode (JSON.encode ts))
|
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
|
||||||
|
where
|
||||||
|
val :: Text -> [Text]
|
||||||
|
val = \case
|
||||||
|
"count" -> ["count=none"]
|
||||||
|
"params" -> ["params=single-object"]
|
||||||
|
"return" -> ["return=representation", "return=minimal", "return=none"]
|
||||||
|
"resolution" -> ["resolution=ignore-duplicates", "resolution=merge-duplicates"]
|
||||||
|
_ -> []
|
||||||
|
|
||||||
makeProcParam :: ProcDescription -> [Referenced Param]
|
makeProcParam :: ProcDescription -> [Referenced Param]
|
||||||
makeProcParam pd =
|
makeProcParam pd =
|
||||||
@@ -165,9 +185,11 @@ makeProcParam pd =
|
|||||||
|
|
||||||
makeParamDefs :: [Table] -> [(Text, Param)]
|
makeParamDefs :: [Table] -> [(Text, Param)]
|
||||||
makeParamDefs ti =
|
makeParamDefs ti =
|
||||||
[ ("preferParams", makePreferParam ["params=single-object"])
|
-- TODO: create Prefer for each method (GET, PATCH, etc.)
|
||||||
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
|
[ ("preferParams", makePreferParam ["params"])
|
||||||
, ("preferCount", makePreferParam ["count=none"])
|
, ("preferReturn", makePreferParam ["return"])
|
||||||
|
, ("preferCount", makePreferParam ["count"])
|
||||||
|
, ("preferPost", makePreferParam ["return", "resolution"])
|
||||||
, ("select", (mempty :: Param)
|
, ("select", (mempty :: Param)
|
||||||
& name .~ "select"
|
& name .~ "select"
|
||||||
& description ?~ "Filtering Columns"
|
& description ?~ "Filtering Columns"
|
||||||
@@ -267,7 +289,7 @@ makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
postOp = tOp
|
postOp = tOp
|
||||||
& parameters .~ fmap ref ["body." <> tn, "select", "preferReturn"]
|
& parameters .~ fmap ref ["body." <> tn, "select", "preferPost"]
|
||||||
& at 201 ?~ "Created"
|
& at 201 ?~ "Created"
|
||||||
patchOp = tOp
|
patchOp = tOp
|
||||||
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"])
|
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"])
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ test | personnages_view | test | actors_view | personnage
|
|||||||
-}
|
-}
|
||||||
addViewM2OAndO2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
|
addViewM2OAndO2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
|
||||||
addViewM2OAndO2ORels keyDeps rels =
|
addViewM2OAndO2ORels keyDeps rels =
|
||||||
rels ++ concat (viewRels <$> rels)
|
rels ++ concatMap viewRels rels
|
||||||
where
|
where
|
||||||
isM2O card = case card of {M2O _ _ -> True; _ -> False;}
|
isM2O card = case card of {M2O _ _ -> True; _ -> False;}
|
||||||
isO2O card = case card of {O2O _ _ -> True; _ -> False;}
|
isO2O card = case card of {O2O _ _ -> True; _ -> False;}
|
||||||
@@ -449,7 +449,7 @@ addViewM2OAndO2ORels keyDeps rels =
|
|||||||
, keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]
|
, keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]
|
||||||
else []
|
else []
|
||||||
viewRels _ = []
|
viewRels _ = []
|
||||||
expandKeyDepCols kdc = zip (fst <$> kdc) <$> sequenceA (snd <$> kdc)
|
expandKeyDepCols kdc = zip (fst <$> kdc) <$> traverse snd kdc
|
||||||
|
|
||||||
addInverseRels :: [Relationship] -> [Relationship]
|
addInverseRels :: [Relationship] -> [Relationship]
|
||||||
addInverseRels rels =
|
addInverseRels rels =
|
||||||
@@ -485,7 +485,7 @@ addViewPrimaryKeys tabs keyDeps =
|
|||||||
-- * We don't have any logic that requires the client to name a PK column (compared to the column hints in embedding for FKs),
|
-- * We don't have any logic that requires the client to name a PK column (compared to the column hints in embedding for FKs),
|
||||||
-- so we don't need to know about the other references.
|
-- so we don't need to know about the other references.
|
||||||
-- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc.
|
-- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc.
|
||||||
takeFirstPK pkCols = catMaybes $ head . snd <$> pkCols
|
takeFirstPK = mapMaybe (head . snd)
|
||||||
|
|
||||||
allTables :: PgVersion -> Bool -> SQL.Statement [Schema] TablesMap
|
allTables :: PgVersion -> Bool -> SQL.Statement [Schema] TablesMap
|
||||||
allTables pgVer =
|
allTables pgVer =
|
||||||
@@ -512,13 +512,11 @@ tablesSqlQuery pgVer =
|
|||||||
CASE
|
CASE
|
||||||
WHEN t.typtype = 'd' THEN
|
WHEN t.typtype = 'd' THEN
|
||||||
CASE
|
CASE
|
||||||
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text
|
|
||||||
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
|
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
|
||||||
ELSE format_type(a.atttypid, a.atttypmod)
|
ELSE format_type(a.atttypid, a.atttypmod)
|
||||||
END
|
END
|
||||||
ELSE
|
ELSE
|
||||||
CASE
|
CASE
|
||||||
WHEN t.typelem <> 0::oid AND t.typlen = (-1) THEN 'ARRAY'::text
|
|
||||||
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
|
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
|
||||||
ELSE format_type(a.atttypid, a.atttypmod)
|
ELSE format_type(a.atttypid, a.atttypmod)
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ data Junction = Junction
|
|||||||
{ junTable :: QualifiedIdentifier
|
{ junTable :: QualifiedIdentifier
|
||||||
, junConstraint1 :: FKConstraint
|
, junConstraint1 :: FKConstraint
|
||||||
, junConstraint2 :: FKConstraint
|
, junConstraint2 :: FKConstraint
|
||||||
, junColumns1 :: [(FieldName, FieldName)]
|
, junColsSource :: [(FieldName, FieldName)]
|
||||||
, junColumns2 :: [(FieldName, FieldName)]
|
, junColsTarget :: [(FieldName, FieldName)]
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
|||||||
+16
-26
@@ -13,7 +13,6 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
|
||||||
import qualified Hasql.Notifications as SQL
|
import qualified Hasql.Notifications as SQL
|
||||||
import qualified Hasql.Session as SQL
|
import qualified Hasql.Session as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
@@ -32,8 +31,7 @@ import PostgREST.AppState (AppState)
|
|||||||
import PostgREST.Config (AppConfig (..), readAppConfig)
|
import PostgREST.Config (AppConfig (..), readAppConfig)
|
||||||
import PostgREST.Config.Database (queryDbSettings, queryPgVersion)
|
import PostgREST.Config.Database (queryDbSettings, queryPgVersion)
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
||||||
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
import PostgREST.Error (checkIsFatal)
|
||||||
errorPayload)
|
|
||||||
import PostgREST.SchemaCache (querySchemaCache)
|
import PostgREST.SchemaCache (querySchemaCache)
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
@@ -131,9 +129,8 @@ establishConnection appState =
|
|||||||
pgVersion <- AppState.usePool appState queryPgVersion
|
pgVersion <- AppState.usePool appState queryPgVersion
|
||||||
case pgVersion of
|
case pgVersion of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let err = PgError False e
|
AppState.logPgrstError appState e
|
||||||
AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
case checkIsFatal e of
|
||||||
case checkIsFatal err of
|
|
||||||
Just reason ->
|
Just reason ->
|
||||||
return $ FatalConnectionError reason
|
return $ FatalConnectionError reason
|
||||||
Nothing ->
|
Nothing ->
|
||||||
@@ -168,19 +165,16 @@ loadSchemaCache appState = do
|
|||||||
querySchemaCache (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements
|
querySchemaCache (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let
|
case checkIsFatal e of
|
||||||
err = PgError False e
|
|
||||||
putErr = AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
|
||||||
case checkIsFatal err of
|
|
||||||
Just hint -> do
|
Just hint -> do
|
||||||
AppState.logWithZTime appState "A fatal error ocurred when loading the schema cache"
|
AppState.logWithZTime appState "A fatal error ocurred when loading the schema cache"
|
||||||
putErr
|
AppState.logPgrstError appState e
|
||||||
AppState.logWithZTime appState hint
|
AppState.logWithZTime appState hint
|
||||||
return SCFatalFail
|
return SCFatalFail
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
AppState.putSchemaCache appState Nothing
|
AppState.putSchemaCache appState Nothing
|
||||||
AppState.logWithZTime appState "An error ocurred when loading the schema cache"
|
AppState.logWithZTime appState "An error ocurred when loading the schema cache"
|
||||||
putErr
|
AppState.logPgrstError appState e
|
||||||
return SCOnRetry
|
return SCOnRetry
|
||||||
|
|
||||||
Right sCache -> do
|
Right sCache -> do
|
||||||
@@ -230,16 +224,15 @@ listener appState = do
|
|||||||
listener appState
|
listener appState
|
||||||
|
|
||||||
handleNotification _ msg
|
handleNotification _ msg
|
||||||
| BS.null msg = scLoader -- reload the schema cache
|
| BS.null msg = cacheReloader
|
||||||
| msg == "reload schema" = scLoader -- reload the schema cache
|
| msg == "reload schema" = cacheReloader
|
||||||
| msg == "reload config" = reReadConfig False appState -- reload the config
|
| msg == "reload config" = reReadConfig False appState
|
||||||
| otherwise = pure () -- Do nothing if anything else than an empty message is sent
|
| otherwise = pure () -- Do nothing if anything else than an empty message is sent
|
||||||
|
|
||||||
scLoader =
|
cacheReloader =
|
||||||
-- It's not necessary to check the loadSchemaCache success
|
-- reloads the schema cache + restarts pool connections
|
||||||
-- here. If the connection drops, the thread will die and
|
-- it's necessary to restart the pg connections because they cache the pg catalog(see #2620)
|
||||||
-- proceed to recover.
|
connectionWorker appState
|
||||||
void $ loadSchemaCache appState
|
|
||||||
|
|
||||||
-- | Re-reads the config plus config options from the db
|
-- | Re-reads the config plus config options from the db
|
||||||
reReadConfig :: Bool -> AppState -> IO ()
|
reReadConfig :: Bool -> AppState -> IO ()
|
||||||
@@ -250,18 +243,15 @@ reReadConfig startingUp appState = do
|
|||||||
qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements
|
qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements
|
||||||
case qDbSettings of
|
case qDbSettings of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let
|
|
||||||
err = PgError False e
|
|
||||||
putErr = AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
|
||||||
AppState.logWithZTime appState
|
AppState.logWithZTime appState
|
||||||
"An error ocurred when trying to query database settings for the config parameters"
|
"An error ocurred when trying to query database settings for the config parameters"
|
||||||
case checkIsFatal err of
|
case checkIsFatal e of
|
||||||
Just hint -> do
|
Just hint -> do
|
||||||
putErr
|
AppState.logPgrstError appState e
|
||||||
AppState.logWithZTime appState hint
|
AppState.logWithZTime appState hint
|
||||||
killThread (AppState.getMainThreadId appState)
|
killThread (AppState.getMainThreadId appState)
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
putErr
|
AppState.logPgrstError appState e
|
||||||
pure []
|
pure []
|
||||||
Right x -> pure x
|
Right x -> pure x
|
||||||
else
|
else
|
||||||
|
|||||||
+4
-20
@@ -1,4 +1,4 @@
|
|||||||
resolver: lts-19.14 # 2022-07-01, GHC 9.0.2
|
resolver: lts-20.6 # 2023-01-09, GHC 9.2.5
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
packages:
|
packages:
|
||||||
@@ -10,23 +10,7 @@ nix:
|
|||||||
pure: false
|
pure: false
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- HTTP-4000.3.16
|
|
||||||
- configurator-pg-0.2.6
|
|
||||||
- hashable-1.4.1.0
|
|
||||||
- hashtables-1.3
|
|
||||||
- hasql-1.6.1.1
|
|
||||||
- hasql-dynamic-statements-0.3.1.2
|
|
||||||
- hasql-implicits-0.1.0.5
|
|
||||||
- hasql-notifications-0.2.0.3
|
|
||||||
- hasql-pool-0.8.0.6
|
|
||||||
- hasql-transaction-1.0.1.2
|
|
||||||
- isomorphism-class-0.1.0.6
|
|
||||||
- lens-aeson-1.1.3
|
|
||||||
- optparse-applicative-0.16.1.0
|
|
||||||
- postgresql-binary-0.12.5
|
|
||||||
- protolude-0.3.2
|
|
||||||
- ptr-0.16.8.2
|
|
||||||
- text-builder-0.6.7
|
|
||||||
- text-builder-dev-0.3.3
|
|
||||||
- git: https://github.com/PostgREST/postgresql-libpq.git
|
- git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
|
- hasql-notifications-0.2.0.4
|
||||||
|
- hasql-pool-0.9
|
||||||
|
|||||||
+24
-136
@@ -5,145 +5,33 @@
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
- completed:
|
- completed:
|
||||||
hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947
|
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
pantry-tree:
|
git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
size: 1428
|
|
||||||
sha256: b73a7f6d21cf20bbf819e19039409c9010efb5000d2b72cdd8fd67a9027c14e8
|
|
||||||
original:
|
|
||||||
hackage: HTTP-4000.3.16
|
|
||||||
- completed:
|
|
||||||
hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849
|
|
||||||
pantry-tree:
|
|
||||||
size: 2463
|
|
||||||
sha256: 97efe7a22afc93033bda5adcffdabc0f1c30dc32b2c3ba02114ce7cd74c942fd
|
|
||||||
original:
|
|
||||||
hackage: configurator-pg-0.2.6
|
|
||||||
- completed:
|
|
||||||
hackage: hashable-1.4.1.0@sha256:50b2f002c68fe67730ee7a3cd8607486197dd99b084255005ad51ecd6970a41b,5019
|
|
||||||
pantry-tree:
|
|
||||||
size: 1248
|
|
||||||
sha256: 9af2f7a42674f7effcabbebc043f97057240783f1709338a77f58216f4a5f18c
|
|
||||||
original:
|
|
||||||
hackage: hashable-1.4.1.0
|
|
||||||
- completed:
|
|
||||||
hackage: hashtables-1.3@sha256:ab21804fdafbbd8ad918b2911dabb729ae0ea891780fe66bf7804cbcd07edadf,10379
|
|
||||||
pantry-tree:
|
|
||||||
size: 2895
|
|
||||||
sha256: e71f113ad989dbc994e0fb52bcc219d62930de9afa8b3441bf7909e864481b33
|
|
||||||
original:
|
|
||||||
hackage: hashtables-1.3
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-1.6.1.1@sha256:948a2137308cc5354e4997bc3666753867124cd25db792424cb9614b1c1b44cf,6626
|
|
||||||
pantry-tree:
|
|
||||||
size: 2622
|
|
||||||
sha256: 28d21bf061522fc513f040e9c383b90532222b7258216cc094e07736add8be10
|
|
||||||
original:
|
|
||||||
hackage: hasql-1.6.1.1
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-dynamic-statements-0.3.1.2@sha256:417aa533c84f074e2fa16bb2c4d4231326aa512097dd1025d915388e56acd1eb,2675
|
|
||||||
pantry-tree:
|
|
||||||
size: 595
|
|
||||||
sha256: 91696d3f3e0ef3254772ae5a8e4e89be68285febb49b302ed83d85ac4037a417
|
|
||||||
original:
|
|
||||||
hackage: hasql-dynamic-statements-0.3.1.2
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-implicits-0.1.0.5@sha256:d16aacad6dc21428d72447d3ae8bcc03839a2f0aa1ec29c797ed9aca4609f9af,1361
|
|
||||||
pantry-tree:
|
|
||||||
size: 264
|
|
||||||
sha256: 0451b99a0a1d02db673d0c40acdf60d4e769e15852eed9e8dc05bffaf43efb70
|
|
||||||
original:
|
|
||||||
hackage: hasql-implicits-0.1.0.5
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-notifications-0.2.0.3@sha256:aca3f7ee847a8f0b7ef6f989dc48f4a094a06c1a34e92aa3c8bb230085966ea6,2027
|
|
||||||
pantry-tree:
|
|
||||||
size: 452
|
|
||||||
sha256: 999f0f2856a00d21f4498a8a58452bbefc4ea972fe2984fd234a68a5fe61d98b
|
|
||||||
original:
|
|
||||||
hackage: hasql-notifications-0.2.0.3
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-pool-0.8.0.6@sha256:b63bb83409bab5bc20ff24f5d62205e9b117701a0fc24531ddeac20ab8c2a42c,1818
|
|
||||||
pantry-tree:
|
|
||||||
size: 346
|
|
||||||
sha256: c4100946b7eae44375511e35a393abe2e1db0e5637c68cea8f53176b796bfd5b
|
|
||||||
original:
|
|
||||||
hackage: hasql-pool-0.8.0.6
|
|
||||||
- completed:
|
|
||||||
hackage: hasql-transaction-1.0.1.2@sha256:297b158cd1f0727f9b0e175bd7d3741c1bcb725a8094956d0ee79b41aafdb30a,2890
|
|
||||||
pantry-tree:
|
|
||||||
size: 983
|
|
||||||
sha256: 3679e6d5c835cc17a8fa0c252b8221e282880044b7219aa1de2531bbd5c40691
|
|
||||||
original:
|
|
||||||
hackage: hasql-transaction-1.0.1.2
|
|
||||||
- completed:
|
|
||||||
hackage: isomorphism-class-0.1.0.6@sha256:d93da31287359c761953b876354de28381f409c5c50e3241c572a443e50c553d,1703
|
|
||||||
pantry-tree:
|
|
||||||
size: 465
|
|
||||||
sha256: c97f922d1ae8f1a0db4c28fac9383d2716934879e95ff0b2b88ebb861d6fba14
|
|
||||||
original:
|
|
||||||
hackage: isomorphism-class-0.1.0.6
|
|
||||||
- completed:
|
|
||||||
hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764
|
|
||||||
pantry-tree:
|
|
||||||
size: 541
|
|
||||||
sha256: b31392b78f2a03111c805f4400007778eb93b49f998ab41dfbebaaf9b5526bad
|
|
||||||
original:
|
|
||||||
hackage: lens-aeson-1.1.3
|
|
||||||
- completed:
|
|
||||||
hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982
|
|
||||||
pantry-tree:
|
|
||||||
size: 2979
|
|
||||||
sha256: dd092d843091c08691485d68a1908517079b1bc6f3d73928f37635a19dc27fc1
|
|
||||||
original:
|
|
||||||
hackage: optparse-applicative-0.16.1.0
|
|
||||||
- completed:
|
|
||||||
hackage: postgresql-binary-0.12.5@sha256:de9da3cba9be541d6c75ae8da2858c33d83dc1b2e0c639b0b9781816b78a91f4,5594
|
|
||||||
pantry-tree:
|
|
||||||
size: 1619
|
|
||||||
sha256: b392337f91031a5b3407393e2f04dfe4e7a28019e88eae6a9370538b90e28c51
|
|
||||||
original:
|
|
||||||
hackage: postgresql-binary-0.12.5
|
|
||||||
- completed:
|
|
||||||
hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240
|
|
||||||
pantry-tree:
|
|
||||||
size: 1594
|
|
||||||
sha256: a36d2912ac552d950ba4476de7d950b56b82dd28e48b9f4d0efee938f10bc525
|
|
||||||
original:
|
|
||||||
hackage: protolude-0.3.2
|
|
||||||
- completed:
|
|
||||||
hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959
|
|
||||||
pantry-tree:
|
|
||||||
size: 1303
|
|
||||||
sha256: 557c438345de19f82bf01d676100da2a191ef06f624e7a4b90b09ac17cbb52a5
|
|
||||||
original:
|
|
||||||
hackage: ptr-0.16.8.2
|
|
||||||
- completed:
|
|
||||||
hackage: text-builder-0.6.7@sha256:efbb3e06107e9c8d1cfe85c963938ca9f375a74379af03da3173be4ef5c37bcf,2364
|
|
||||||
pantry-tree:
|
|
||||||
size: 425
|
|
||||||
sha256: cd0ae197e6f9f3860a8ab71f5b87c4a8452ed1fce2fdfd35e36d68ded6e6648e
|
|
||||||
original:
|
|
||||||
hackage: text-builder-0.6.7
|
|
||||||
- completed:
|
|
||||||
hackage: text-builder-dev-0.3.3@sha256:79ec422defcc2e5b34f94129c72b98d34b2efc1ed8bbd945ccb8f4f535a892c3,2784
|
|
||||||
pantry-tree:
|
|
||||||
size: 724
|
|
||||||
sha256: 8883631a132438e7892fcb13e89d6bbcdc0ac76c56fbea8df8d7aa482ce81f73
|
|
||||||
original:
|
|
||||||
hackage: text-builder-dev-0.3.3
|
|
||||||
- completed:
|
|
||||||
name: postgresql-libpq
|
name: postgresql-libpq
|
||||||
version: 0.9.4.3
|
|
||||||
git: https://github.com/PostgREST/postgresql-libpq.git
|
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
size: 1081
|
sha256: 074668b9669b9c49f3c522c8af5c608799a1965e203c463b188b2632995beac2
|
||||||
sha256: 0df271e48af32eb8292a45301af45e114110d54099ee73dbc609d39770e8175e
|
size: 1414
|
||||||
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
version: 0.9.4.3
|
||||||
original:
|
original:
|
||||||
|
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
git: https://github.com/PostgREST/postgresql-libpq.git
|
git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
- completed:
|
||||||
|
hackage: hasql-notifications-0.2.0.4@sha256:9a09fa9b97feadd9492c8bd8bc6b9cffe0513510102f08374b0c45ecd479ed67,2028
|
||||||
|
pantry-tree:
|
||||||
|
sha256: 56f9e240728e7a65711dde45fa2e2075b914e32cd370424aaa4572392378a60e
|
||||||
|
size: 452
|
||||||
|
original:
|
||||||
|
hackage: hasql-notifications-0.2.0.4
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-pool-0.9@sha256:db7a37f6b3a922c37adc3c7ced47a7c10786d1f171e47a735a6e812a587ba44c,2111
|
||||||
|
pantry-tree:
|
||||||
|
sha256: 49b1181d28c6f5317e794671c2dae155754b834bdcfa30f7e5dbad28e4cf0249
|
||||||
|
size: 346
|
||||||
|
original:
|
||||||
|
hackage: hasql-pool-0.9
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
size: 618951
|
sha256: 4905c93319aa94aa53da8f41d614d7bacdbfe6c63a8c6132d32e6e62f24a9af4
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml
|
size: 649315
|
||||||
sha256: 4c31d4ef975b0211078862566aedf3b82b6cea569fc2cde4c72a51e5a8d236ce
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/6.yaml
|
||||||
original: lts-19.14
|
original: lts-20.6
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ main =
|
|||||||
, "src/PostgREST/Query/SqlFragment.hs"
|
, "src/PostgREST/Query/SqlFragment.hs"
|
||||||
, "src/PostgREST/ApiRequest/Preferences.hs"
|
, "src/PostgREST/ApiRequest/Preferences.hs"
|
||||||
, "src/PostgREST/ApiRequest/QueryParams.hs"
|
, "src/PostgREST/ApiRequest/QueryParams.hs"
|
||||||
|
, "src/PostgREST/Error.hs"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = ""
|
db-pool-acquisition-timeout = 10
|
||||||
|
db-pool-max-lifetime = 1800
|
||||||
db-pre-request = "check_alias"
|
db-pre-request = "check_alias"
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = "open_alias"
|
db-root-spec = "open_alias"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = ""
|
db-pool-acquisition-timeout = 10
|
||||||
|
db-pool-max-lifetime = 1800
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = ""
|
db-pool-acquisition-timeout = 10
|
||||||
|
db-pool-max-lifetime = 1800
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = ""
|
db-pool-acquisition-timeout = 10
|
||||||
|
db-pool-max-lifetime = 1800
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public,extensions,other"
|
|||||||
db-max-rows = 100
|
db-max-rows = 100
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = 30
|
||||||
|
db-pool-max-lifetime = 3600
|
||||||
db-pre-request = "test.other_custom_headers"
|
db-pre-request = "test.other_custom_headers"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "other_root"
|
db-root-spec = "other_root"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public,extensions,private"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = 30
|
||||||
|
db-pool-max-lifetime = 3600
|
||||||
db-pre-request = "test.custom_headers"
|
db-pre-request = "test.custom_headers"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "root"
|
db-root-spec = "root"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public,test"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = 30
|
||||||
|
db-pool-max-lifetime = 3600
|
||||||
db-pre-request = "please_run_fast"
|
db-pre-request = "please_run_fast"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "openapi_v3"
|
db-root-spec = "openapi_v3"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = ""
|
db-pool-acquisition-timeout = 10
|
||||||
|
db-pool-max-lifetime = 1800
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ PGRST_DB_EXTRA_SEARCH_PATH: public, test
|
|||||||
PGRST_DB_MAX_ROWS: 1000
|
PGRST_DB_MAX_ROWS: 1000
|
||||||
PGRST_DB_PLAN_ENABLED: true
|
PGRST_DB_PLAN_ENABLED: true
|
||||||
PGRST_DB_POOL: 1
|
PGRST_DB_POOL: 1
|
||||||
PGRST_DB_POOL_ACQUISITION_TIMEOUT: 10
|
PGRST_DB_POOL_ACQUISITION_TIMEOUT: 30
|
||||||
|
PGRST_DB_POOL_MAX_LIFETIME: 3600
|
||||||
PGRST_DB_PREPARED_STATEMENTS: false
|
PGRST_DB_PREPARED_STATEMENTS: false
|
||||||
PGRST_DB_PRE_REQUEST: please_run_fast
|
PGRST_DB_PRE_REQUEST: please_run_fast
|
||||||
PGRST_DB_ROOT_SPEC: openapi_v3
|
PGRST_DB_ROOT_SPEC: openapi_v3
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ db-extra-search-path = "public, test"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = 30
|
||||||
|
db-pool-max-lifetime = 3600
|
||||||
db-pre-request = "please_run_fast"
|
db-pre-request = "please_run_fast"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "openapi_v3"
|
db-root-spec = "openapi_v3"
|
||||||
|
|||||||
@@ -86,3 +86,15 @@ $$ language sql;
|
|||||||
create or replace function hello() returns text as $$
|
create or replace function hello() returns text as $$
|
||||||
select 'hello';
|
select 'hello';
|
||||||
$$ language sql;
|
$$ 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';
|
||||||
|
$$;
|
||||||
|
|||||||
+11
-1
@@ -50,6 +50,7 @@ def run(
|
|||||||
env=None,
|
env=None,
|
||||||
port=None,
|
port=None,
|
||||||
host=None,
|
host=None,
|
||||||
|
wait_for_readiness=True,
|
||||||
no_pool_connection_available=False,
|
no_pool_connection_available=False,
|
||||||
):
|
):
|
||||||
"Run PostgREST and yield an endpoint that is ready for connections."
|
"Run PostgREST and yield an endpoint that is ready for connections."
|
||||||
@@ -88,7 +89,8 @@ def run(
|
|||||||
process.stdin.write(stdin or b"")
|
process.stdin.write(stdin or b"")
|
||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
|
|
||||||
wait_until_ready(adminurl + "/ready")
|
if wait_for_readiness:
|
||||||
|
wait_until_ready(adminurl + "/ready")
|
||||||
|
|
||||||
process.stdout.read()
|
process.stdout.read()
|
||||||
|
|
||||||
@@ -137,6 +139,14 @@ def freeport():
|
|||||||
return s.getsockname()[1]
|
return s.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
|
def wait_until_exit(postgrest):
|
||||||
|
"Wait for PostgREST to exit, or times out"
|
||||||
|
try:
|
||||||
|
return postgrest.process.wait(timeout=1)
|
||||||
|
except (subprocess.TimeoutExpired):
|
||||||
|
raise PostgrestTimedOut()
|
||||||
|
|
||||||
|
|
||||||
def wait_until_ready(url):
|
def wait_until_ready(url):
|
||||||
"Wait for the given HTTP endpoint to return a status of 200."
|
"Wait for the given HTTP endpoint to return a status of 200."
|
||||||
session = requests_unixsocket.Session()
|
session = requests_unixsocket.Session()
|
||||||
|
|||||||
+41
-1
@@ -66,6 +66,15 @@ def test_read_secret_from_stdin_dbconfig(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
exitCode = wait_until_exit(postgrest)
|
||||||
|
assert exitCode == 1
|
||||||
|
|
||||||
|
|
||||||
def test_connect_with_dburi(dburi, defaultenv):
|
def test_connect_with_dburi(dburi, defaultenv):
|
||||||
"Connecting with db-uri instead of LIPQ* environment variables should work."
|
"Connecting with db-uri instead of LIPQ* environment variables should work."
|
||||||
defaultenv_without_libpq = {
|
defaultenv_without_libpq = {
|
||||||
@@ -325,7 +334,7 @@ def test_db_schema_notify_reload(defaultenv):
|
|||||||
"/rpc/change_db_schema_and_full_reload", data={"schemas": "v1"}
|
"/rpc/change_db_schema_and_full_reload", data={"schemas": "v1"}
|
||||||
)
|
)
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.2)
|
||||||
|
|
||||||
response = postgrest.session.get("/rpc/get_guc_value?name=search_path")
|
response = postgrest.session.get("/rpc/get_guc_value?name=search_path")
|
||||||
assert response.text == '"\\"v1\\", \\"public\\""'
|
assert response.text == '"\\"v1\\", \\"public\\""'
|
||||||
@@ -563,6 +572,16 @@ def test_pool_acquisition_timeout(defaultenv, metapostgrest):
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["message"] == "Timed out acquiring connection from connection pool."
|
assert data["message"] == "Timed out acquiring connection from connection pool."
|
||||||
|
|
||||||
|
# ensure the message appears on the logs as well
|
||||||
|
output = None
|
||||||
|
for _ in range(10):
|
||||||
|
output = postgrest.process.stdout.readline()
|
||||||
|
if output:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
assert "Timed out acquiring connection from connection pool." in output.decode()
|
||||||
|
|
||||||
|
|
||||||
def test_change_statement_timeout_held_connection(defaultenv, metapostgrest):
|
def test_change_statement_timeout_held_connection(defaultenv, metapostgrest):
|
||||||
"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration"
|
"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration"
|
||||||
@@ -818,6 +837,27 @@ def test_no_pool_connection_required_on_bad_embedding(defaultenv):
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_notify_reloading_catalog_cache(defaultenv):
|
||||||
|
"notify should reload the connection catalog cache"
|
||||||
|
|
||||||
|
with run(env=defaultenv) as postgrest:
|
||||||
|
|
||||||
|
# first the id col is an uuid
|
||||||
|
response = postgrest.session.get(
|
||||||
|
"/cats?id=eq.dea27321-f988-4a57-93e4-8eeb38f3cf1e"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# change it to a bigint
|
||||||
|
response = postgrest.session.post("/rpc/drop_change_cats")
|
||||||
|
assert response.status_code == 204
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# next request should succeed with a bigint value
|
||||||
|
response = postgrest.session.get("/cats?id=eq.1")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
|
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
|
||||||
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
|
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
|
||||||
# A stack size of 200K seems to be enough for succeess
|
# A stack size of 200K seems to be enough for succeess
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
[
|
[
|
||||||
{ "$ref": "#/parameters/body.child_entities" },
|
{ "$ref": "#/parameters/body.child_entities" },
|
||||||
{ "$ref": "#/parameters/select" },
|
{ "$ref": "#/parameters/select" },
|
||||||
{ "$ref": "#/parameters/preferReturn" }
|
{ "$ref": "#/parameters/preferPost" }
|
||||||
]
|
]
|
||||||
|]
|
|]
|
||||||
|
|
||||||
@@ -310,6 +310,23 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
describe "VIEW created for a TABLE with a O2M relationship" $ do
|
||||||
|
|
||||||
|
it "fk points to destination TABLE instead of the VIEW" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let referralLink = r ^? key "definitions" . key "projects" . key "properties" . key "client_id"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
referralLink `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "integer",
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Note:\nThis is a Foreign Key to `clients.id`.<fk table='clients' column='id'/>"
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
describe "PostgreSQL to Swagger Type Mapping" $ do
|
describe "PostgreSQL to Swagger Type Mapping" $ do
|
||||||
|
|
||||||
it "character varying to string" $ do
|
it "character varying to string" $ do
|
||||||
@@ -490,6 +507,117 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
it "array types to array" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let text_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text_arr"
|
||||||
|
let int_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_int_arr"
|
||||||
|
let bool_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bool_arr"
|
||||||
|
let char_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_char_arr"
|
||||||
|
let varchar_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_varchar_arr"
|
||||||
|
let bigint_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint_arr"
|
||||||
|
let numeric_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric_arr"
|
||||||
|
let json_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_json_arr"
|
||||||
|
let jsonb_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_jsonb_arr"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
|
||||||
|
text_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "text[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
int_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "integer[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
bool_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "boolean[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
char_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "character[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
varchar_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "character varying[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
bigint_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "bigint[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
numeric_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "numeric[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
json_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "json[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
jsonb_arr_types `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"format": "jsonb[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
|
||||||
describe "Detects default values" $ do
|
describe "Detects default values" $ do
|
||||||
|
|
||||||
it "text" $ do
|
it "text" $ do
|
||||||
@@ -569,7 +697,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
it "includes function summary/description and body schema for arguments" $ do
|
it "includes function summary/description and body schema for arguments" $ do
|
||||||
r <- simpleBody <$> get "/"
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
let method s = key "paths" . key "/rpc/varied_arguments" . key s
|
let method s = key "paths" . key "/rpc/varied_arguments_openapi" . key s
|
||||||
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
|
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
|
||||||
summary = r ^? method "post" . key "summary"
|
summary = r ^? method "post" . key "summary"
|
||||||
description = r ^? method "post" . key "description"
|
description = r ^? method "post" . key "description"
|
||||||
@@ -590,7 +718,15 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
"date",
|
"date",
|
||||||
"money",
|
"money",
|
||||||
"enum",
|
"enum",
|
||||||
"arr"
|
"text_arr",
|
||||||
|
"int_arr",
|
||||||
|
"bool_arr",
|
||||||
|
"char_arr",
|
||||||
|
"varchar_arr",
|
||||||
|
"bigint_arr",
|
||||||
|
"numeric_arr",
|
||||||
|
"json_arr",
|
||||||
|
"jsonb_arr"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"double": {
|
"double": {
|
||||||
@@ -617,9 +753,64 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
"format": "enum_menagerie_type",
|
"format": "enum_menagerie_type",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"arr": {
|
"text_arr": {
|
||||||
"format": "text[]",
|
"format": "text[]",
|
||||||
"type": "string"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"int_arr": {
|
||||||
|
"format": "integer[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bool_arr": {
|
||||||
|
"format": "boolean[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"char_arr": {
|
||||||
|
"format": "character[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"varchar_arr": {
|
||||||
|
"format": "character varying[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bigint_arr": {
|
||||||
|
"format": "bigint[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"numeric_arr": {
|
||||||
|
"format": "numeric[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json_arr": {
|
||||||
|
"format": "json[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {}
|
||||||
|
},
|
||||||
|
"jsonb_arr": {
|
||||||
|
"format": "jsonb[]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {}
|
||||||
},
|
},
|
||||||
"integer": {
|
"integer": {
|
||||||
"format": "integer",
|
"format": "integer",
|
||||||
|
|||||||
@@ -202,10 +202,10 @@ spec =
|
|||||||
it "fails if the fk is not known" $
|
it "fails if the fk is not known" $
|
||||||
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint":"Verify that 'message' and 'person' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
"hint":null,
|
||||||
"message":"Could not find a relationship between 'message' and 'person' in the schema cache",
|
"message":"Could not find a relationship between 'message' and 'person' in the schema cache",
|
||||||
"code": "PGRST200",
|
"code": "PGRST200",
|
||||||
"details": null}|]
|
"details":"Searched for a foreign key relationship between 'message' and 'person' using the hint 'space' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
@@ -492,10 +492,10 @@ spec =
|
|||||||
it "doesn't work if the junction is only internal" $
|
it "doesn't work if the junction is only internal" $
|
||||||
get "/end_1?select=end_2(*)" `shouldRespondWith`
|
get "/end_1?select=end_2(*)" `shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint":"Verify that 'end_1' and 'end_2' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
"hint": null,
|
||||||
"message":"Could not find a relationship between 'end_1' and 'end_2' in the schema cache",
|
"message":"Could not find a relationship between 'end_1' and 'end_2' in the schema cache",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"details": null}|]
|
"details": "Searched for a foreign key relationship between 'end_1' and 'end_2' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
it "shouldn't try to embed if the private junction has an exposed homonym" $
|
it "shouldn't try to embed if the private junction has an exposed homonym" $
|
||||||
@@ -503,10 +503,10 @@ spec =
|
|||||||
-- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669
|
-- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669
|
||||||
get "/schauspieler?select=filme(*)" `shouldRespondWith`
|
get "/schauspieler?select=filme(*)" `shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint":"Verify that 'schauspieler' and 'filme' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
"hint":null,
|
||||||
"message":"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache",
|
"message":"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"details": null}|]
|
"details":"Searched for a foreign key relationship between 'schauspieler' and 'filme' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import Network.HTTP.Types
|
|||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion120,
|
||||||
pgVersion120, pgVersion130)
|
pgVersion130)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ spec actualPgVersion = do
|
|||||||
r <- request methodGet "/projects?id=in.(1,2,3)"
|
r <- request methodGet "/projects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -35,14 +36,14 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then Just [aesonQQ|15.63|]
|
then 15.63
|
||||||
else Just [aesonQQ|15.69|]
|
else 15.69
|
||||||
|
|
||||||
it "outputs the total cost for a single filter on a view" $ do
|
it "outputs the total cost for a single filter on a view" $ do
|
||||||
r <- request methodGet "/projects_view?id=gt.2"
|
r <- request methodGet "/projects_view?id=gt.2"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -51,8 +52,8 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then Just [aesonQQ|24.28|]
|
then 24.28
|
||||||
else Just [aesonQQ|32.28|]
|
else 32.28
|
||||||
|
|
||||||
it "outputs blocks info when using the buffers option" $
|
it "outputs blocks info when using the buffers option" $
|
||||||
if actualPgVersion >= pgVersion130
|
if actualPgVersion >= pgVersion130
|
||||||
@@ -158,7 +159,7 @@ spec actualPgVersion = do
|
|||||||
r <- request methodPost "/projects"
|
r <- request methodPost "/projects"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"id":100, "name": "Project 100"}|]
|
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"id":100, "name": "Project 100"}|]
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -167,14 +168,14 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then Just [aesonQQ|3.28|]
|
then 3.28
|
||||||
else Just [aesonQQ|3.33|]
|
else 3.33
|
||||||
|
|
||||||
it "outputs the total cost for an update" $ do
|
it "outputs the total cost for an update" $ do
|
||||||
r <- request methodPatch "/projects?id=eq.3"
|
r <- request methodPatch "/projects?id=eq.3"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"name": "Patched Project"}|]
|
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"name": "Patched Project"}|]
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -183,28 +184,28 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then Just [aesonQQ|12.45|]
|
then 12.45
|
||||||
else Just [aesonQQ|12.5|]
|
else 12.5
|
||||||
|
|
||||||
it "outputs the total cost for a delete" $ do
|
it "outputs the total cost for a delete" $ do
|
||||||
r <- request methodDelete "/projects?id=in.(1,2,3)"
|
r <- request methodDelete "/projects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` Just [aesonQQ|15.68|]
|
totalCost `shouldBe` 15.68
|
||||||
|
|
||||||
it "outputs the total cost for a single upsert" $ do
|
it "outputs the total cost for a single upsert" $ do
|
||||||
r <- request methodPut "/tiobe_pls?name=eq.Go"
|
r <- request methodPut "/tiobe_pls?name=eq.Go"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json")
|
(acceptHdrs "application/vnd.pgrst.plan+json")
|
||||||
[json| [ { "name": "Go", "rank": 19 } ]|]
|
[json| [ { "name": "Go", "rank": 19 } ]|]
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -213,8 +214,8 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion >= pgVersion120
|
if actualPgVersion >= pgVersion120
|
||||||
then Just [aesonQQ|1.3|]
|
then 1.3
|
||||||
else Just [aesonQQ|1.35|]
|
else 1.35
|
||||||
|
|
||||||
it "outputs the plan for application/vnd.pgrst.object" $ do
|
it "outputs the plan for application/vnd.pgrst.object" $ do
|
||||||
r <- request methodDelete "/projects?id=eq.6"
|
r <- request methodDelete "/projects?id=eq.6"
|
||||||
@@ -232,14 +233,14 @@ spec actualPgVersion = do
|
|||||||
r <- request methodGet "/rpc/getallprojects?id=in.(1,2,3)"
|
r <- request methodGet "/rpc/getallprojects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
let totalCost = planCost r
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` Just [aesonQQ|68.57|]
|
totalCost `shouldBe` 68.57
|
||||||
|
|
||||||
it "outputs the plan for text/xml" $ do
|
it "outputs the plan for text/xml" $ do
|
||||||
r <- request methodGet "/rpc/return_scalar_xml"
|
r <- request methodGet "/rpc/return_scalar_xml"
|
||||||
@@ -282,33 +283,56 @@ spec actualPgVersion = do
|
|||||||
describe "resource embedding costs" $ do
|
describe "resource embedding costs" $ do
|
||||||
it "a one to many doesn't surpass a threshold" $ do
|
it "a one to many doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/clients?select=*,projects(*)&id=eq.1"
|
r <- request methodGet "/clients?select=*,projects(*)&id=eq.1"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
[planHdr] ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
liftIO $ planCost r `shouldSatisfy` (< 33.3)
|
||||||
liftIO $ totalCost `shouldBe`
|
|
||||||
if actualPgVersion > pgVersion120
|
|
||||||
then Just [aesonQQ|33.25|]
|
|
||||||
else Just [aesonQQ|33.27|]
|
|
||||||
|
|
||||||
it "a many to one doesn't surpass a threshold" $ do
|
it "a many to one doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/projects?select=*,clients(*)&id=eq.1"
|
r <- request methodGet "/projects?select=*,clients(*)&id=eq.1"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
[planHdr] ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
liftIO $ planCost r `shouldSatisfy` (< 16.5)
|
||||||
liftIO $ totalCost `shouldBe`
|
|
||||||
if actualPgVersion > pgVersion120
|
|
||||||
then Just [aesonQQ|16.39|]
|
|
||||||
else Just [aesonQQ|16.41|]
|
|
||||||
|
|
||||||
it "a many to many doesn't surpass a threshold" $ do
|
it "a many to many doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/users?select=*,tasks(*)&id=eq.1"
|
r <- request methodGet "/users?select=*,tasks(*)&id=eq.1"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
liftIO $ planCost r `shouldSatisfy` (< 70.9)
|
||||||
liftIO $ totalCost `shouldBe`
|
|
||||||
if | actualPgVersion > pgVersion120 -> Just [aesonQQ|69.34|]
|
|
||||||
| actualPgVersion > pgVersion100 -> Just [aesonQQ|69.36|]
|
describe "function call costs" $ do
|
||||||
| otherwise -> Just [aesonQQ|70.81|]
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
|
r <- request methodGet "/rpc/get_projects_below?id=3"
|
||||||
|
[planHdr] ""
|
||||||
|
|
||||||
|
liftIO $ planCost r `shouldSatisfy` (< 36.4)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
||||||
|
r <- request methodGet "/rpc/getallprojects"
|
||||||
|
[planHdr] ""
|
||||||
|
|
||||||
|
liftIO $ planCost r `shouldSatisfy` (< 71.0)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
|
r <- request methodGet "/rpc/add_them?a=3&b=4"
|
||||||
|
[planHdr] ""
|
||||||
|
|
||||||
|
liftIO $ planCost r `shouldSatisfy` (< 1.18)
|
||||||
|
|
||||||
|
context "params=multiple-objects" $ do
|
||||||
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
|
r <- request methodPost "/rpc/get_projects_below"
|
||||||
|
[planHdr, ("Prefer", "params=multiple-objects")]
|
||||||
|
[str| [{"id": 1}, {"id": 4}] |]
|
||||||
|
|
||||||
|
liftIO $ planCost r `shouldSatisfy` (< 4503.4)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
|
r <- request methodPost "/rpc/add_them"
|
||||||
|
[planHdr, ("Prefer", "params=multiple-objects")]
|
||||||
|
[str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]
|
||||||
|
|
||||||
|
liftIO $ planCost r `shouldSatisfy` (< 5.85)
|
||||||
|
|
||||||
disabledSpec :: SpecWith ((), Application)
|
disabledSpec :: SpecWith ((), Application)
|
||||||
disabledSpec =
|
disabledSpec =
|
||||||
|
|||||||
@@ -590,8 +590,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request partitions as children from a partitioned table" $
|
it "cannot request partitions as children from a partitioned table" $
|
||||||
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
|
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"hint":"Verify that 'car_models' and 'car_model_sales_202101' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_models' and 'car_model_sales_202101' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -601,8 +601,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request a partitioned table as parent from a partition" $
|
it "cannot request a partitioned table as parent from a partition" $
|
||||||
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"hint":"Verify that 'car_model_sales_202101' and 'car_models' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_model_sales_202101' and 'car_models' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -612,8 +612,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request a partition as parent from a partitioned table" $
|
it "cannot request a partition as parent from a partitioned table" $
|
||||||
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"hint":"Verify that 'car_model_sales' and 'car_models_default' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
{"hint":"Perhaps you meant 'car_models' instead of 'car_models_default'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_model_sales' and 'car_models_default' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -623,8 +623,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request partitioned tables as children from a partition" $
|
it "cannot request partitioned tables as children from a partition" $
|
||||||
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"hint":"Verify that 'car_models_default' and 'car_model_sales' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_models_default'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_models_default' and 'car_model_sales' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
|
|||||||
@@ -120,17 +120,39 @@ spec actualPgVersion =
|
|||||||
it "should fail with 404 on unknown proc name" $
|
it "should fail with 404 on unknown proc name" $
|
||||||
get "/rpc/fake" `shouldRespondWith` 404
|
get "/rpc/fake" `shouldRespondWith` 404
|
||||||
|
|
||||||
|
it "should fail with 404 and hint the closest proc on unknown proc name" $
|
||||||
|
get "/rpc/sayhell" `shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"hint":"Perhaps you meant to call the function test.sayhello",
|
||||||
|
"message":"Could not find the function test.sayhell without parameters in the schema cache",
|
||||||
|
"code":"PGRST202",
|
||||||
|
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "should fail with 404 on unknown proc args" $ do
|
it "should fail with 404 on unknown proc args" $ do
|
||||||
get "/rpc/sayhello" `shouldRespondWith` 404
|
get "/rpc/sayhello" `shouldRespondWith` 404
|
||||||
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
|
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
|
||||||
|
|
||||||
|
it "should fail with 404 and hint the closest args on unknown proc args" $
|
||||||
|
get "/rpc/sayhello?nam=Peter" `shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"hint":"Perhaps you meant to call the function test.sayhello(name)",
|
||||||
|
"message":"Could not find the function test.sayhello(nam) in the schema cache",
|
||||||
|
"code":"PGRST202",
|
||||||
|
"details":"Searched for the function test.sayhello with parameter nam, but no matches were found in the schema cache."} |]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "should not ignore unknown args and fail with 404" $
|
it "should not ignore unknown args and fail with 404" $
|
||||||
get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`
|
get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint":"Perhaps you meant to call the function test.add_them(a, b)",
|
||||||
"message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache",
|
"message":"Could not find the function test.add_them(a, b, smthelse) in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null} |]
|
"details":"Searched for the function test.add_them with parameters a, b, smthelse, but no matches were found in the schema cache."} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -141,10 +163,10 @@ spec actualPgVersion =
|
|||||||
[json|{}|]
|
[json|{}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint":null,
|
||||||
"message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache",
|
"message":"Could not find the function test.sayhello in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null} |]
|
"details":"Searched for the function test.sayhello with a single json/jsonb parameter, but no matches were found in the schema cache."} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -152,19 +174,19 @@ spec actualPgVersion =
|
|||||||
it "should fail with 404 for overloaded functions with unknown args" $ do
|
it "should fail with 404 for overloaded functions with unknown args" $ do
|
||||||
get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`
|
get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint":null,
|
||||||
"message":"Could not find the test.overloaded(wrong_arg) function in the schema cache",
|
"message":"Could not find the function test.overloaded(wrong_arg) in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null} |]
|
"details":"Searched for the function test.overloaded with parameter wrong_arg, but no matches were found in the schema cache."} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`
|
get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint":"Perhaps you meant to call the function test.overloaded(a, b, c)",
|
||||||
"message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache",
|
"message":"Could not find the function test.overloaded(a, b, wrong_arg) in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null} |]
|
"details":"Searched for the function test.overloaded with parameters a, b, wrong_arg, but no matches were found in the schema cache."} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -246,13 +268,17 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"id": 2, "articleStars": [{"userId": 3}]}|]
|
[json|{"id": 2, "articleStars": [{"userId": 3}]}|]
|
||||||
|
|
||||||
it "can embed an M2M relationship table" $
|
it "can embed an M2M relationship table" $ do
|
||||||
get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"
|
get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"
|
||||||
`shouldRespondWith` [json|[
|
`shouldRespondWith` [json|[
|
||||||
{"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},
|
{"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},
|
||||||
{"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}
|
{"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}
|
||||||
]|]
|
]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/2565
|
||||||
|
get "/rpc/get_yards?select=groups(*)"
|
||||||
|
`shouldRespondWith` [json|[]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can embed an M2M relationship table that has a parent relationship table" $
|
it "can embed an M2M relationship table that has a parent relationship table" $
|
||||||
get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"
|
get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"
|
||||||
@@ -1247,10 +1273,10 @@ spec actualPgVersion =
|
|||||||
[json|{"x": 1, "y": 2}|]
|
[json|{"x": 1, "y": 2}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint": "Perhaps you meant to call the function test.unnamed_text_param",
|
||||||
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache",
|
"message": "Could not find the function test.unnamed_int_param(x, y) in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null
|
"details":"Searched for the function test.unnamed_int_param with parameters x, y or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache."
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
@@ -1262,10 +1288,10 @@ spec actualPgVersion =
|
|||||||
[str|a simple text|]
|
[str|a simple text|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint": null,
|
||||||
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache",
|
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null
|
"details":"Searched for the function test.unnamed_int_param with a single unnamed text parameter, but no matches were found in the schema cache."
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
@@ -1277,10 +1303,10 @@ spec actualPgVersion =
|
|||||||
[str|a simple text|]
|
[str|a simple text|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint": null,
|
||||||
"message": "Could not find the test.unnamed_int_param function with a single unnamed xml parameter in the schema cache",
|
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null
|
"details":"Searched for the function test.unnamed_int_param with a single unnamed xml parameter, but no matches were found in the schema cache."
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
@@ -1293,10 +1319,10 @@ spec actualPgVersion =
|
|||||||
file
|
file
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint": null,
|
||||||
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache",
|
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null
|
"details":"Searched for the function test.unnamed_int_param with a single unnamed bytea parameter, but no matches were found in the schema cache."
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
@@ -1353,10 +1379,10 @@ spec actualPgVersion =
|
|||||||
"a,b\n1,2\n4,6\n100,200"
|
"a,b\n1,2\n4,6\n100,200"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
"hint":"Perhaps you meant to call the function test.overloaded_unnamed_param(x, y)",
|
||||||
"message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache",
|
"message":"Could not find the function test.overloaded_unnamed_param(a, b) in the schema cache",
|
||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":null
|
"details":"Searched for the function test.overloaded_unnamed_param with parameters a, b, but no matches were found in the schema cache."
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ import qualified Feature.RpcPreRequestGucsSpec
|
|||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
pool <- P.acquire 3 Nothing $ toUtf8 $ configDbUri testCfg
|
pool <- P.acquire 3 10 60 $ toUtf8 $ configDbUri testCfg
|
||||||
|
|
||||||
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
|
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
-- TODO Can be replaced now by obtaining the EXPLAIN plan and adding the cost tests on PlanSpec.hs
|
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Control.Lens ((^?))
|
|
||||||
import qualified Data.Aeson.Lens as L
|
|
||||||
import qualified Hasql.Decoders as HD
|
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
|
||||||
import qualified Hasql.DynamicStatements.Statement as H
|
|
||||||
import qualified Hasql.Pool as P
|
|
||||||
import qualified Hasql.Statement as H
|
|
||||||
import qualified Hasql.Transaction as HT
|
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
|
||||||
import Text.Heredoc
|
|
||||||
|
|
||||||
import Protolude hiding (get, toS)
|
|
||||||
|
|
||||||
import PostgREST.Plan.CallPlan
|
|
||||||
import PostgREST.Query.QueryBuilder (callPlanToQuery)
|
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers
|
|
||||||
import PostgREST.SchemaCache.Proc
|
|
||||||
|
|
||||||
import Test.Hspec
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
pool <- P.acquire 3 Nothing "postgresql://"
|
|
||||||
|
|
||||||
hspec $ describe "QueryCost" $
|
|
||||||
context "call proc query" $ do
|
|
||||||
it "should not exceed cost when calling setof composite proc" $ do
|
|
||||||
cost <- exec pool $
|
|
||||||
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
|
||||||
(KeyParams [ProcParam "id" "int" True False])
|
|
||||||
(Just [str| {"id": 3} |]) False False [])
|
|
||||||
liftIO $
|
|
||||||
cost `shouldSatisfy` (< Just 40)
|
|
||||||
|
|
||||||
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
|
||||||
cost <- exec pool $
|
|
||||||
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
|
|
||||||
liftIO $
|
|
||||||
cost `shouldSatisfy` (< Just 30)
|
|
||||||
|
|
||||||
it "should not exceed cost when calling scalar proc" $ do
|
|
||||||
cost <- exec pool $
|
|
||||||
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
|
||||||
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
|
||||||
(Just [str| {"a": 3, "b": 4} |]) True False [])
|
|
||||||
liftIO $
|
|
||||||
cost `shouldSatisfy` (< Just 10)
|
|
||||||
|
|
||||||
context "params=multiple-objects" $ do
|
|
||||||
it "should not exceed cost when calling setof composite proc" $ do
|
|
||||||
cost <- exec pool $
|
|
||||||
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
|
||||||
(KeyParams [ProcParam "id" "int" True False])
|
|
||||||
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
|
|
||||||
liftIO $ do
|
|
||||||
-- lower bound needed for now to make sure that cost is not Nothing
|
|
||||||
cost `shouldSatisfy` (> Just 2000)
|
|
||||||
cost `shouldSatisfy` (< Just 2100)
|
|
||||||
|
|
||||||
it "should not exceed cost when calling scalar proc" $ do
|
|
||||||
cost <- exec pool $
|
|
||||||
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
|
||||||
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
|
||||||
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])
|
|
||||||
liftIO $
|
|
||||||
cost `shouldSatisfy` (< Just 10)
|
|
||||||
|
|
||||||
|
|
||||||
exec :: P.Pool -> H.Snippet -> IO (Maybe Int64)
|
|
||||||
exec pool query =
|
|
||||||
join . rightToMaybe <$>
|
|
||||||
P.use pool (HT.transaction HT.ReadCommitted HT.Read $ HT.statement mempty $ explainCost query)
|
|
||||||
|
|
||||||
explainCost :: H.Snippet -> H.Statement () (Maybe Int64)
|
|
||||||
explainCost query =
|
|
||||||
H.dynamicallyParameterized snippet decodeExplain False
|
|
||||||
where
|
|
||||||
snippet = "EXPLAIN (FORMAT JSON) " <> query
|
|
||||||
decodeExplain :: HD.Result (Maybe Int64)
|
|
||||||
decodeExplain =
|
|
||||||
let row = HD.singleRow $ HD.column $ HD.nonNullable HD.bytea in
|
|
||||||
(^? L.nth 0 . L.key "Plan" . L.key "Total Cost" . L._Integral) <$> row
|
|
||||||
+18
-1
@@ -1,9 +1,12 @@
|
|||||||
module SpecHelper where
|
module SpecHelper where
|
||||||
|
|
||||||
|
import Control.Lens ((^?))
|
||||||
|
import Data.Aeson.Lens
|
||||||
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
|
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
|
import Data.Scientific (toRealFloat)
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
|
|
||||||
import Data.Aeson (Value (..), decode, encode)
|
import Data.Aeson (Value (..), decode, encode)
|
||||||
@@ -79,7 +82,8 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
|||||||
, configDbMaxRows = Nothing
|
, configDbMaxRows = Nothing
|
||||||
, configDbPlanEnabled = False
|
, configDbPlanEnabled = False
|
||||||
, configDbPoolSize = 10
|
, configDbPoolSize = 10
|
||||||
, configDbPoolAcquisitionTimeout = Nothing
|
, configDbPoolAcquisitionTimeout = 10
|
||||||
|
, configDbPoolMaxLifetime = 1800
|
||||||
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
|
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
|
||||||
, configDbPreparedStatements = True
|
, configDbPreparedStatements = True
|
||||||
, configDbRootSpec = Nothing
|
, configDbRootSpec = Nothing
|
||||||
@@ -213,6 +217,9 @@ rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r
|
|||||||
acceptHdrs :: BS.ByteString -> [Header]
|
acceptHdrs :: BS.ByteString -> [Header]
|
||||||
acceptHdrs mime = [(hAccept, mime)]
|
acceptHdrs mime = [(hAccept, mime)]
|
||||||
|
|
||||||
|
planHdr :: Header
|
||||||
|
planHdr = (hAccept, "application/vnd.pgrst.plan+json")
|
||||||
|
|
||||||
rangeUnit :: Header
|
rangeUnit :: Header
|
||||||
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
||||||
|
|
||||||
@@ -276,3 +283,13 @@ requestMutation method path body =
|
|||||||
|
|
||||||
data BaseTable = BaseTable ByteString ByteString Value
|
data BaseTable = BaseTable ByteString ByteString Value
|
||||||
data MutationCheck = MutationCheck BaseTable (WaiExpectation ())
|
data MutationCheck = MutationCheck BaseTable (WaiExpectation ())
|
||||||
|
|
||||||
|
planCost :: SResponse -> Float
|
||||||
|
planCost resp =
|
||||||
|
let res = simpleBody resp ^? nth 0 . key "Plan" . key "Total Cost" in
|
||||||
|
-- big value in case parsing fails
|
||||||
|
fromMaybe 1000000000.0 $ unbox =<< res
|
||||||
|
where
|
||||||
|
unbox :: Value -> Maybe Float
|
||||||
|
unbox (Number n) = Just $ toRealFloat n
|
||||||
|
unbox _ = Nothing
|
||||||
|
|||||||
Vendored
+91
-1
@@ -272,6 +272,56 @@ $_$An RPC function
|
|||||||
|
|
||||||
Just a test for RPC function arguments$_$;
|
Just a test for RPC function arguments$_$;
|
||||||
|
|
||||||
|
CREATE FUNCTION varied_arguments_openapi(
|
||||||
|
double double precision,
|
||||||
|
"varchar" character varying,
|
||||||
|
"boolean" boolean,
|
||||||
|
date date,
|
||||||
|
money money,
|
||||||
|
enum enum_menagerie_type,
|
||||||
|
text_arr text[],
|
||||||
|
int_arr int[],
|
||||||
|
bool_arr boolean[],
|
||||||
|
char_arr char[],
|
||||||
|
varchar_arr varchar[],
|
||||||
|
bigint_arr bigint[],
|
||||||
|
numeric_arr numeric[],
|
||||||
|
json_arr json[],
|
||||||
|
jsonb_arr jsonb[],
|
||||||
|
"integer" integer default 42,
|
||||||
|
json json default '{}',
|
||||||
|
jsonb jsonb default '{}'
|
||||||
|
) RETURNS json
|
||||||
|
LANGUAGE sql
|
||||||
|
IMMUTABLE
|
||||||
|
AS $_$
|
||||||
|
SELECT json_build_object(
|
||||||
|
'double', double,
|
||||||
|
'varchar', "varchar",
|
||||||
|
'boolean', "boolean",
|
||||||
|
'date', date,
|
||||||
|
'money', money,
|
||||||
|
'enum', enum,
|
||||||
|
'text_arr', text_arr,
|
||||||
|
'int_arr', int_arr,
|
||||||
|
'bool_arr', bool_arr,
|
||||||
|
'char_arr', char_arr,
|
||||||
|
'varchar_arr', varchar_arr,
|
||||||
|
'bigint_arr', bigint_arr,
|
||||||
|
'numeric_arr', numeric_arr,
|
||||||
|
'json_arr', json_arr,
|
||||||
|
'jsonb_arr', jsonb_arr,
|
||||||
|
'integer', "integer",
|
||||||
|
'json', json,
|
||||||
|
'jsonb', jsonb
|
||||||
|
);
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION varied_arguments_openapi(double precision, character varying, boolean, date, money, enum_menagerie_type, text[], int[], boolean[], char[], varchar[], bigint[], numeric[], json[], jsonb[], integer, json, jsonb) IS
|
||||||
|
$_$An RPC function
|
||||||
|
|
||||||
|
Just a test for RPC function arguments$_$;
|
||||||
|
|
||||||
|
|
||||||
CREATE FUNCTION json_argument(arg json) RETURNS text
|
CREATE FUNCTION json_argument(arg json) RETURNS text
|
||||||
LANGUAGE sql
|
LANGUAGE sql
|
||||||
@@ -1814,7 +1864,16 @@ CREATE TABLE test.openapi_types(
|
|||||||
"a_real" real,
|
"a_real" real,
|
||||||
"a_double_precision" double precision,
|
"a_double_precision" double precision,
|
||||||
"a_json" json,
|
"a_json" json,
|
||||||
"a_jsonb" jsonb
|
"a_jsonb" jsonb,
|
||||||
|
"a_text_arr" text[],
|
||||||
|
"a_int_arr" int[],
|
||||||
|
"a_bool_arr" boolean[],
|
||||||
|
"a_char_arr" char[],
|
||||||
|
"a_varchar_arr" varchar[],
|
||||||
|
"a_bigint_arr" bigint[],
|
||||||
|
"a_numeric_arr" numeric[],
|
||||||
|
"a_json_arr" json[],
|
||||||
|
"a_jsonb_arr" jsonb[]
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE test.openapi_defaults(
|
CREATE TABLE test.openapi_defaults(
|
||||||
@@ -2996,3 +3055,34 @@ CREATE TABLE public.tb (
|
|||||||
|
|
||||||
CREATE VIEW test.va AS SELECT a1 FROM public.ta;
|
CREATE VIEW test.va AS SELECT a1 FROM public.ta;
|
||||||
CREATE VIEW test.vb AS SELECT b1 FROM public.tb;
|
CREATE VIEW test.vb AS SELECT b1 FROM public.tb;
|
||||||
|
|
||||||
|
CREATE TABLE test.groups (
|
||||||
|
name text PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE test.yards (
|
||||||
|
id bigint PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE test.group_yard (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
group_id text NOT NULL REFERENCES test.groups(name),
|
||||||
|
yard_id bigint NOT NULL REFERENCES test.yards(id),
|
||||||
|
PRIMARY KEY (id, group_id, yard_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE FUNCTION test.get_yards() RETURNS SETOF test.yards
|
||||||
|
LANGUAGE sql
|
||||||
|
AS $$
|
||||||
|
select * from test.yards;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- view's name is alphabetically before projects
|
||||||
|
create view test.alpha_projects as
|
||||||
|
select c.id, p.name as pro_name, c.name as cli_name
|
||||||
|
from projects p join clients c on p.client_id = c.id;
|
||||||
|
|
||||||
|
-- view's name is alphabetically after projects
|
||||||
|
create view test.zeta_projects as
|
||||||
|
select c.id, p.name as pro_name, c.name as cli_name
|
||||||
|
from projects p join clients c on p.client_id = c.id;
|
||||||
Reference in New Issue
Block a user