Compare commits

...
19 Commits
Author SHA1 Message Date
Laurence Islaandsteve-chavez e07807deab bump version to 10.2.0 2023-04-12 12:49:39 -05:00
steve-chavez bd50b1e4d3 ci: pin Nix version to avoid error
To 2.13.3
2023-04-12 12:49:39 -05:00
Robert Vollmertandsteve-chavez f26cdd5151 feat: use hasql-pool-0.9, add db-pool-max-lifetime (fixes #2638)
- db-pool-acquisition-timeout is no longer optional, defaults to 10s
- new option db-pool-max-lifetime limits the maximal lifetime of a
  postgresql connection, defaults to 30m
2023-04-12 12:49:36 -05:00
Steve Chavez b869dd7be9 fix: log to stderr on AcquisitionTimeoutUsageError (#2667)
* refactor: remove uneeded type on checkIsFatal
* dry with a logPgrstError function
2023-04-12 12:49:04 -05:00
RobertandLaurence Isla 97a4402911 Update nixpkgs, dependencies (#2612)
* relax upper bounds on HTTP, hspec, lens-aeson, optparse-applicative (fixes #2580)
* upgrade stackage snapshot to latest LTS, with GHC 9.2.5
* bump nixpkgs to 2023-01-12
* fix complaints due to updated linters
2023-04-12 09:52:08 -05:00
RobertandLaurence Isla a101d27c9c bump postgresql-libpq (#2599)
For https://github.com/PostgREST/postgresql-libpq/pull/2.
2023-04-12 09:52:08 -05:00
steve-chavezandLaurence Isla 519dbc75f3 refactor: delete QueryCost, instead use PlanSpec 2023-04-12 09:52:08 -05:00
steve-chavezandLaurence Isla ae3c784921 refactor: add planCost and planHdr for tests 2023-04-12 09:52:08 -05:00
steve-chavez f56bed2a75 bump version to 10.1.2 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez af8e436732 Add missing fixes to the changelog 2023-02-02 03:30:15 -05:00
steve-chavez 98a29bee04 fix: NOTIFY pgrst not reoading the catalog cache 2023-02-02 03:30:15 -05:00
Tuan LeandSteve Chavez 557285b659 fix: consider authentication failure as a fatal error 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez 81501aefa0 fix: FK pointing to VIEW instead of TABLE in OpenAPI output 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez 12c1d4a8e4 Add upsert headers for POST requests to the OpenAPI output 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez 8aa7368786 fix: Add required OpenAPI items object when the paramater is an array 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez 9d4ff812c9 Add suggestions with fuzzy text search when no relationship is found (#2583) 2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez 171dd313d9 fix: clarify error messages for functions
Move explanation on single unnamed parameters to the error details
2023-02-02 03:30:15 -05:00
Laurence IslaandSteve Chavez fd24a7374b feat: hint function names/parameters on error 2023-02-02 03:30:15 -05:00
steve-chavez a525790c4c fix: bad M2M embed on RPC 2023-02-02 03:30:15 -05:00
51 changed files with 895 additions and 562 deletions
+2
View File
@@ -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
+3 -7
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
-6
View File
@@ -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
+3 -3
View File
@@ -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";
} }
+20 -20
View File
@@ -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
{ {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+7 -1
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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 ([],[])
+1 -1
View File
@@ -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"
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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"
| |
+8 -4
View File
@@ -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
View File
@@ -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" _ _ _ _
+9 -8
View File
@@ -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} _ ->
-7
View File
@@ -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
+31 -9
View File
@@ -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"])
+3 -5
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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"
] ]
+2 -1
View File
@@ -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 = ""
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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 = ""
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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"
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
+196 -5
View File
@@ -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] }
+62 -38
View File
@@ -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 =
+8 -8
View File
@@ -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
+54 -28
View File
@@ -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
View File
@@ -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
-86
View File
@@ -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
View File
@@ -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
+91 -1
View File
@@ -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;