Compare commits
21
Commits
@@ -11,7 +11,7 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: cachix/install-nix-action@v22
|
- uses: cachix/install-nix-action@v23
|
||||||
with:
|
with:
|
||||||
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: cachix/cachix-action@v12
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
name: Seed ${{ matrix.os }}
|
name: Seed ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
|||||||
+16
-16
@@ -17,7 +17,7 @@ jobs:
|
|||||||
name: Lint & check code style
|
name: Lint & check code style
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pgVersion: [9.6, 10, 11, 12, 13, 14, 15]
|
pgVersion: [9.6, 10, 11, 12, 13, 14, 15, 16]
|
||||||
name: Test PG ${{ matrix.pgVersion }} (Nix)
|
name: Test PG ${{ matrix.pgVersion }} (Nix)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
name: Test memory (Nix)
|
name: Test memory (Nix)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
name: Build Linux static (Nix)
|
name: Build Linux static (Nix)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
name: Build MacOS (Nix)
|
name: Build MacOS (Nix)
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
name: Build ${{ matrix.name }} (Stack)
|
name: Build ${{ matrix.name }} (Stack)
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Stack working files cache
|
- name: Stack working files cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
name: Get FreeBSD build from CirrusCI
|
name: Get FreeBSD build from CirrusCI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Get FreeBSD executable from CirrusCI
|
- name: Get FreeBSD executable from CirrusCI
|
||||||
env:
|
env:
|
||||||
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
||||||
@@ -221,7 +221,7 @@ jobs:
|
|||||||
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Workaround runner image issue
|
- name: Workaround runner image issue
|
||||||
# https://github.com/actions/runner-images/issues/7061
|
# https://github.com/actions/runner-images/issues/7061
|
||||||
run: sudo chown -R "$USER" /usr/local/.ghcup
|
run: sudo chown -R "$USER" /usr/local/.ghcup
|
||||||
@@ -260,7 +260,7 @@ jobs:
|
|||||||
GITHUB_COMMIT: ${{ github.sha }}
|
GITHUB_COMMIT: ${{ github.sha }}
|
||||||
GHC_VERSION: ${{ matrix.ghc }}
|
GHC_VERSION: ${{ matrix.ghc }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- id: Remote-Dir
|
- id: Remote-Dir
|
||||||
name: Unique directory name for the remote build
|
name: Unique directory name for the remote build
|
||||||
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
||||||
@@ -324,7 +324,7 @@ jobs:
|
|||||||
version: ${{ steps.Identify-Version.outputs.version }}
|
version: ${{ steps.Identify-Version.outputs.version }}
|
||||||
isprerelease: ${{ steps.Identify-Version.outputs.isprerelease }}
|
isprerelease: ${{ steps.Identify-Version.outputs.isprerelease }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- id: Identify-Version
|
- id: Identify-Version
|
||||||
name: Identify the version to be released
|
name: Identify the version to be released
|
||||||
run: |
|
run: |
|
||||||
@@ -377,7 +377,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -443,7 +443,7 @@ jobs:
|
|||||||
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -496,7 +496,7 @@ jobs:
|
|||||||
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Publish images for ARM builds on Docker Hub
|
- name: Publish images for ARM builds on Docker Hub
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
env:
|
env:
|
||||||
@@ -523,7 +523,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Remove uploaded files from server
|
- name: Remove uploaded files from server
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
name: Loadtest (Nix)
|
name: Loadtest (Nix)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
|
|||||||
@@ -5,6 +5,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [11.2.2] - 2023-10-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2824, Fix regression by reverting fix that returned 206 when first position = length in a `Range` header - @laurenceisla, @strengthless
|
||||||
|
|
||||||
|
## [11.2.1] - 2023-10-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2899, Fix `application/vnd.pgrst.array` not accepted as a valid mediatype - @taimoorzaeem
|
||||||
|
- #2524, Fix schema cache and configuration reloading with `NOTIFY` not working on Windows - @diogob, @laurenceisla
|
||||||
|
- #2915, Fix duplicate headers in response - @taimoorzaeem
|
||||||
|
- #2824, Fix range request with first position same as length return status 206 - @taimoorzaeem
|
||||||
|
- #2939, Fix wrong `Preference-Applied` with `Prefer: tx=commit` when transaction is rollbacked - @steve-chavez
|
||||||
|
- #2939, Fix `count=exact` not being included in `Preference-Applied` - @steve-chavez
|
||||||
|
- #2800, Fix not including to-one embed resources that had a `NULL` value in any of the selected fields when doing null filtering on them - @laurenceisla
|
||||||
|
- #2846, Fix error when requesting `Prefer: count=<type>` and doing null filtering on embedded resources - @laurenceisla
|
||||||
|
- #2959, Fix setting `default_transaction_isolation` unnecessarily - @steve-chavez
|
||||||
|
- #2929, Fix arrow filtering on RPC returning dynamic TABLE with composite type - @steve-chavez
|
||||||
|
- #2963, Fix RPCs not embedding correctly when using overloaded functions for computed relationships - @laurenceisla
|
||||||
|
- #2970, Fix regression that rejects URI connection strings with certain unescaped characters in the password - @laurenceisla, @steve-chavez
|
||||||
|
|
||||||
## [11.2.0] - 2023-08-10
|
## [11.2.0] - 2023-08-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+13
@@ -50,6 +50,19 @@ let
|
|||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
name = "postgresql-16";
|
||||||
|
postgresql = pkgs.postgresql_16.withPackages (p: [
|
||||||
|
p.postgis
|
||||||
|
(p.pg_safeupdate.overrideAttrs (old: {
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp safeupdate.dylib safeupdate.so || true
|
||||||
|
install -D safeupdate.so -t $out/lib
|
||||||
|
'';
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ let
|
|||||||
(prev.callHackageDirect
|
(prev.callHackageDirect
|
||||||
{
|
{
|
||||||
pkg = "hasql-notifications";
|
pkg = "hasql-notifications";
|
||||||
ver = "0.2.0.5";
|
ver = "0.2.0.6";
|
||||||
sha256 = "sha256-KV36zs/RTgJh/oBBPZaNiQshusmakWnjYTcdoNa4JLA=";
|
sha256 = "sha256-7PyFlB2B70njudOjaX6tk1m77ol9vnF5fI0LF86kVAI=";
|
||||||
}
|
}
|
||||||
{ });
|
{ });
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ self: super:
|
|||||||
{
|
{
|
||||||
## Example for including a postgresql version from a specific nixpks commit:
|
## Example for including a postgresql version from a specific nixpks commit:
|
||||||
##
|
##
|
||||||
# postgresql_14 =
|
postgresql_16 =
|
||||||
# let
|
let
|
||||||
# rev = "76b1e16c6659ccef7187ca69b287525fea133244";
|
rev = "5148520bfab61f99fd25fb9ff7bfbb50dad3c9db";
|
||||||
# tarballHash = "1vsahpcx80k2bgslspb0sa6j4bmhdx77sw6la455drqcrqhdqj6a";
|
tarballHash = "1dfjmz65h8z4lk845724vypzmf3dbgsdndjpj8ydlhx6c7rpcq3p";
|
||||||
#
|
|
||||||
# pinnedPkgs =
|
pinnedPkgs =
|
||||||
# builtins.fetchTarball {
|
builtins.fetchTarball {
|
||||||
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
# sha256 = tarballHash;
|
sha256 = tarballHash;
|
||||||
# };
|
};
|
||||||
# in
|
in
|
||||||
# (import pinnedPkgs { }).pkgs.postgresql_14;
|
(import pinnedPkgs { }).pkgs.postgresql_16;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 11.2.0
|
version: 11.2.2
|
||||||
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
|
||||||
@@ -92,7 +92,7 @@ library
|
|||||||
, 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.2.0.6 && < 0.3
|
||||||
, hasql-pool >= 0.10 && < 0.11
|
, hasql-pool >= 0.10 && < 0.11
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ userApiRequest conf req reqBody = do
|
|||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iPayload = payload
|
, iPayload = payload
|
||||||
, iPreferences = Preferences.fromHeaders hdrs
|
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs
|
||||||
, iQueryParams = qPrms
|
, iQueryParams = qPrms
|
||||||
, iColumns = columns
|
, iColumns = columns
|
||||||
, iHeaders = iHdrs
|
, iHeaders = iHdrs
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
--
|
--
|
||||||
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
||||||
--
|
--
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
module PostgREST.ApiRequest.Preferences
|
module PostgREST.ApiRequest.Preferences
|
||||||
( Preferences(..)
|
( Preferences(..)
|
||||||
, PreferCount(..)
|
, PreferCount(..)
|
||||||
@@ -15,8 +16,8 @@ module PostgREST.ApiRequest.Preferences
|
|||||||
, PreferResolution(..)
|
, PreferResolution(..)
|
||||||
, PreferTransaction(..)
|
, PreferTransaction(..)
|
||||||
, fromHeaders
|
, fromHeaders
|
||||||
, ToAppliedHeader(..)
|
|
||||||
, shouldCount
|
, shouldCount
|
||||||
|
, prefAppliedHeader
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
@@ -53,7 +54,7 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- One header with comma-separated values can be used to set multiple preferences:
|
-- One header with comma-separated values can be used to set multiple preferences:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = Nothing
|
-- , preferRepresentation = Nothing
|
||||||
@@ -65,7 +66,7 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- Multiple headers can also be used:
|
-- Multiple headers can also be used:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = Nothing
|
-- , preferRepresentation = Nothing
|
||||||
@@ -77,13 +78,13 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- If a preference is set more than once, only the first is used:
|
-- If a preference is set more than once, only the first is used:
|
||||||
--
|
--
|
||||||
-- >>> preferTransaction $ fromHeaders [("Prefer", "tx=commit, tx=rollback")]
|
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
|
||||||
-- Just Commit
|
-- Just Commit
|
||||||
--
|
--
|
||||||
-- This is also the case across multiple headers:
|
-- This is also the case across multiple headers:
|
||||||
--
|
--
|
||||||
-- >>> :{
|
-- >>> :{
|
||||||
-- preferResolution . fromHeaders $
|
-- preferResolution . fromHeaders True $
|
||||||
-- [ ("Prefer", "resolution=ignore-duplicates")
|
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||||
-- , ("Prefer", "resolution=merge-duplicates")
|
-- , ("Prefer", "resolution=merge-duplicates")
|
||||||
-- ]
|
-- ]
|
||||||
@@ -92,12 +93,12 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- Preferences not recognized by the application are ignored:
|
-- Preferences not recognized by the application are ignored:
|
||||||
--
|
--
|
||||||
-- >>> preferResolution $ fromHeaders [("Prefer", "resolution=foo")]
|
-- >>> preferResolution $ fromHeaders True [("Prefer", "resolution=foo")]
|
||||||
-- Nothing
|
-- Nothing
|
||||||
--
|
--
|
||||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Nothing
|
-- { preferResolution = Nothing
|
||||||
-- , preferRepresentation = Just Full
|
-- , preferRepresentation = Just Full
|
||||||
@@ -107,14 +108,14 @@ data Preferences
|
|||||||
-- , preferMissing = Just ApplyDefaults
|
-- , preferMissing = Just ApplyDefaults
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
fromHeaders :: [HTTP.Header] -> Preferences
|
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
|
||||||
fromHeaders headers =
|
fromHeaders allowTxEndOverride headers =
|
||||||
Preferences
|
Preferences
|
||||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||||
, preferParameters = parsePrefs [SingleObject]
|
, preferParameters = parsePrefs [SingleObject]
|
||||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
, preferTransaction = parsePrefs [Commit, Rollback]
|
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
@@ -128,6 +129,22 @@ fromHeaders headers =
|
|||||||
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
|
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
|
||||||
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||||
|
|
||||||
|
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
||||||
|
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing } =
|
||||||
|
if null prefsVals
|
||||||
|
then Nothing
|
||||||
|
else Just (HTTP.hPreferenceApplied, combined)
|
||||||
|
where
|
||||||
|
combined = BS.intercalate ", " prefsVals
|
||||||
|
prefsVals = catMaybes [
|
||||||
|
toHeaderValue <$> preferResolution
|
||||||
|
, toHeaderValue <$> preferMissing
|
||||||
|
, toHeaderValue <$> preferRepresentation
|
||||||
|
, toHeaderValue <$> preferParameters
|
||||||
|
, toHeaderValue <$> preferCount
|
||||||
|
, toHeaderValue <$> preferTransaction
|
||||||
|
]
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Convert a preference into the value that we look for in the 'Prefer' headers.
|
-- Convert a preference into the value that we look for in the 'Prefer' headers.
|
||||||
--
|
--
|
||||||
@@ -137,16 +154,6 @@ fromHeaders headers =
|
|||||||
class ToHeaderValue a where
|
class ToHeaderValue a where
|
||||||
toHeaderValue :: a -> ByteString
|
toHeaderValue :: a -> ByteString
|
||||||
|
|
||||||
-- |
|
|
||||||
-- Header to indicate that a preference has been applied.
|
|
||||||
--
|
|
||||||
-- >>> toAppliedHeader MergeDuplicates
|
|
||||||
-- ("Preference-Applied","resolution=merge-duplicates")
|
|
||||||
--
|
|
||||||
class ToHeaderValue a => ToAppliedHeader a where
|
|
||||||
toAppliedHeader :: a -> HTTP.Header
|
|
||||||
toAppliedHeader x = (HTTP.hPreferenceApplied, toHeaderValue x)
|
|
||||||
|
|
||||||
-- | How to handle duplicate values.
|
-- | How to handle duplicate values.
|
||||||
data PreferResolution
|
data PreferResolution
|
||||||
= MergeDuplicates
|
= MergeDuplicates
|
||||||
@@ -156,8 +163,6 @@ instance ToHeaderValue PreferResolution where
|
|||||||
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
||||||
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||||
|
|
||||||
instance ToAppliedHeader PreferResolution
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- How to return the mutated data.
|
-- How to return the mutated data.
|
||||||
--
|
--
|
||||||
@@ -168,8 +173,6 @@ data PreferRepresentation
|
|||||||
| None -- ^ Return nothing from the mutated data.
|
| None -- ^ Return nothing from the mutated data.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance ToAppliedHeader PreferRepresentation
|
|
||||||
|
|
||||||
instance ToHeaderValue PreferRepresentation where
|
instance ToHeaderValue PreferRepresentation where
|
||||||
toHeaderValue Full = "return=representation"
|
toHeaderValue Full = "return=representation"
|
||||||
toHeaderValue None = "return=minimal"
|
toHeaderValue None = "return=minimal"
|
||||||
@@ -209,8 +212,6 @@ instance ToHeaderValue PreferTransaction where
|
|||||||
toHeaderValue Commit = "tx=commit"
|
toHeaderValue Commit = "tx=commit"
|
||||||
toHeaderValue Rollback = "tx=rollback"
|
toHeaderValue Rollback = "tx=rollback"
|
||||||
|
|
||||||
instance ToAppliedHeader PreferTransaction
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
||||||
-- in the json body.
|
-- in the json body.
|
||||||
@@ -222,5 +223,3 @@ data PreferMissing
|
|||||||
instance ToHeaderValue PreferMissing where
|
instance ToHeaderValue PreferMissing where
|
||||||
toHeaderValue ApplyDefaults = "missing=default"
|
toHeaderValue ApplyDefaults = "missing=default"
|
||||||
toHeaderValue ApplyNulls = "missing=null"
|
toHeaderValue ApplyNulls = "missing=null"
|
||||||
|
|
||||||
instance ToAppliedHeader PreferMissing
|
|
||||||
|
|||||||
@@ -150,8 +150,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
|
|||||||
liftEither . mapLeft Error.ApiRequestError $
|
liftEither . mapLeft Error.ApiRequestError $
|
||||||
ApiRequest.userApiRequest conf req body
|
ApiRequest.userApiRequest conf req body
|
||||||
|
|
||||||
Response.optionalRollback conf apiRequest $
|
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
|
||||||
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
|
|
||||||
|
|
||||||
runDbHandler :: AppState.AppState -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
runDbHandler :: AppState.AppState -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||||
runDbHandler appState isoLvl mode authenticated prepared handler = do
|
runDbHandler appState isoLvl mode authenticated prepared handler = do
|
||||||
|
|||||||
@@ -479,26 +479,26 @@ readPGRSTEnvironment =
|
|||||||
-- >>> addFallbackAppName ver "postgres:///postgres?host=server&port=5432"
|
-- >>> addFallbackAppName ver "postgres:///postgres?host=server&port=5432"
|
||||||
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
--
|
--
|
||||||
-- >>> addFallbackAppName ver "host=localhost port=5432 dbname=postgres"
|
|
||||||
-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11.1.0 (5a04ec7)'"
|
|
||||||
--
|
|
||||||
-- >>> addFallbackAppName ver "postgresql://"
|
-- >>> addFallbackAppName ver "postgresql://"
|
||||||
-- "postgresql://?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
-- "postgresql://?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
--
|
--
|
||||||
-- >>> addFallbackAppName strangeVer "host=localhost port=5432 dbname=postgres"
|
|
||||||
-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11\\'1&0@#$%,.:\"[]{}?+^()=asdfqwer'"
|
|
||||||
--
|
|
||||||
-- >>> addFallbackAppName strangeVer "postgres:///postgres?host=server&port=5432"
|
-- >>> addFallbackAppName strangeVer "postgres:///postgres?host=server&port=5432"
|
||||||
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011%271%260%40%23%24%25%2C.%3A%22%5B%5D%7B%7D%3F%2B%5E%28%29%3Dasdfqwer"
|
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011%271%260%40%23%24%25%2C.%3A%22%5B%5D%7B%7D%3F%2B%5E%28%29%3Dasdfqwer"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "postgres://user:invalid_chars[]#@host:5432/postgres"
|
||||||
|
-- "postgres://user:invalid_chars[]#@host:5432/postgres"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "invalid_uri1=val1 invalid_uri2=val2"
|
||||||
|
-- "invalid_uri1=val1 invalid_uri2=val2"
|
||||||
addFallbackAppName :: ByteString -> Text -> Text
|
addFallbackAppName :: ByteString -> Text -> Text
|
||||||
addFallbackAppName version dbUri = dbUri <>
|
addFallbackAppName version dbUri = dbUri <>
|
||||||
case uriQuery <$> parseURI (toS dbUri) of
|
case uriQuery <$> parseURI (toS dbUri) of
|
||||||
Nothing -> " " <> keyValFmt -- Assume key/value connection string if the uri is not valid
|
-- Does not add the application name to key=val connection strings or invalid URIs
|
||||||
|
Nothing -> mempty
|
||||||
Just "" -> "?" <> uriFmt
|
Just "" -> "?" <> uriFmt
|
||||||
Just "?" -> uriFmt
|
Just "?" -> uriFmt
|
||||||
_ -> "&" <> uriFmt
|
_ -> "&" <> uriFmt
|
||||||
where
|
where
|
||||||
uriFmt = pKeyWord <> toS (escapeURIString isUnescapedInURIComponent $ toS pgrstVer)
|
uriFmt = pKeyWord <> toS (escapeURIString isUnescapedInURIComponent $ toS pgrstVer)
|
||||||
keyValFmt = pKeyWord <> "'" <> T.replace "'" "\\'" pgrstVer <> "'"
|
|
||||||
pKeyWord = "fallback_application_name="
|
pKeyWord = "fallback_application_name="
|
||||||
pgrstVer = "PostgREST " <> T.decodeUtf8 version
|
pgrstVer = "PostgREST " <> T.decodeUtf8 version
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ queryRoleSettings prepared =
|
|||||||
select
|
select
|
||||||
kv.rolname,
|
kv.rolname,
|
||||||
i.value as iso_lvl,
|
i.value as iso_lvl,
|
||||||
array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transation_isolation') as role_settings
|
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
||||||
from kv_settings kv
|
from kv_settings kv
|
||||||
join pg_settings ps on ps.name = kv.key and ps.context = 'user'
|
join pg_settings ps on ps.name = kv.key and ps.context = 'user'
|
||||||
left join iso_setting i on i.rolname = kv.rolname
|
left join iso_setting i on i.rolname = kv.rolname
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ decodeMediaType mt =
|
|||||||
"application/vnd.pgrst.object+json":rest -> checkSingularNullStrip rest
|
"application/vnd.pgrst.object+json":rest -> checkSingularNullStrip rest
|
||||||
"application/vnd.pgrst.object":rest -> checkSingularNullStrip rest
|
"application/vnd.pgrst.object":rest -> checkSingularNullStrip rest
|
||||||
"application/vnd.pgrst.array+json":rest -> checkArrayNullStrip rest
|
"application/vnd.pgrst.array+json":rest -> checkArrayNullStrip rest
|
||||||
|
"application/vnd.pgrst.array":rest -> checkArrayNullStrip rest
|
||||||
"*/*":_ -> MTAny
|
"*/*":_ -> MTAny
|
||||||
other:_ -> MTOther other
|
other:_ -> MTOther other
|
||||||
_ -> MTAny
|
_ -> MTAny
|
||||||
|
|||||||
+12
-15
@@ -226,22 +226,19 @@ resolveTableFieldName table fieldName =
|
|||||||
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
||||||
Just . resolveColumnField
|
Just . resolveColumnField
|
||||||
|
|
||||||
resolveTableField :: Table -> Field -> CoercibleField
|
|
||||||
resolveTableField table (fieldName, []) = resolveTableFieldName table fieldName
|
|
||||||
resolveTableField table (fieldName, jp) =
|
|
||||||
case resolveTableFieldName table fieldName of
|
|
||||||
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
|
||||||
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
|
||||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp}
|
|
||||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp}
|
|
||||||
-- other types will get converted `to_jsonb(col)->attr`
|
|
||||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
|
||||||
|
|
||||||
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
||||||
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
||||||
resolveTypeOrUnknown ResolverContext{..} field@(fn, jp) =
|
resolveTypeOrUnknown ResolverContext{..} (fn, jp) =
|
||||||
fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
case res of
|
||||||
Just . flip resolveTableField field
|
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
||||||
|
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
||||||
|
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
|
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
|
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
||||||
|
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||||
|
where
|
||||||
|
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
||||||
|
Just . flip resolveTableFieldName fn
|
||||||
|
|
||||||
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
||||||
--
|
--
|
||||||
@@ -383,7 +380,7 @@ addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,re
|
|||||||
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
|
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r}
|
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r}
|
||||||
ComputedRelationship{} ->
|
ComputedRelationship{} ->
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r{relTable=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias}
|
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias}
|
||||||
_ ->
|
_ ->
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r}
|
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r}
|
||||||
) <$> rel
|
) <$> rel
|
||||||
|
|||||||
@@ -226,9 +226,9 @@ optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
|
|||||||
SQL.condemn
|
SQL.condemn
|
||||||
where
|
where
|
||||||
shouldCommit =
|
shouldCommit =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Commit
|
preferTransaction == Just Commit
|
||||||
shouldRollback =
|
shouldRollback =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Rollback
|
preferTransaction == Just Rollback
|
||||||
|
|
||||||
-- | Runs local (transaction scoped) GUCs for every request.
|
-- | Runs local (transaction scoped) GUCs for every request.
|
||||||
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
|
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ readPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logi
|
|||||||
then mempty
|
then mempty
|
||||||
else " WHERE " ) <>
|
else " WHERE " ) <>
|
||||||
intercalateSnippet " AND " (
|
intercalateSnippet " AND " (
|
||||||
map (pgFmtLogicTree qi) logicForest ++
|
map (pgFmtLogicTreeCount qi) logicForest ++
|
||||||
map pgFmtJoinCondition relJoinConds ++
|
map pgFmtJoinCondition relJoinConds ++
|
||||||
subQueries
|
subQueries
|
||||||
)
|
)
|
||||||
@@ -216,6 +216,18 @@ readPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logi
|
|||||||
if joinType == Just JTInner
|
if joinType == Just JTInner
|
||||||
then ("EXISTS (" <> readPlanToCountQuery readReq <> " )"):rest
|
then ("EXISTS (" <> readPlanToCountQuery readReq <> " )"):rest
|
||||||
else rest
|
else rest
|
||||||
|
findNullEmbedRel fld = find (\(Node ReadPlan{relAggAlias} _) -> fld == relAggAlias) forest
|
||||||
|
|
||||||
|
-- https://github.com/PostgREST/postgrest/pull/2930#discussion_r1325293698
|
||||||
|
pgFmtLogicTreeCount :: QualifiedIdentifier -> CoercibleLogicTree -> SQL.Snippet
|
||||||
|
pgFmtLogicTreeCount qiCount (CoercibleExpr hasNot op frst) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTreeCount qiCount <$> frst) <> ")"
|
||||||
|
where
|
||||||
|
notOp = if hasNot then "NOT" else mempty
|
||||||
|
opSql And = " AND "
|
||||||
|
opSql Or = " OR "
|
||||||
|
pgFmtLogicTreeCount _ (CoercibleStmnt (CoercibleFilterNullEmbed hasNot fld)) =
|
||||||
|
maybe mempty (\x -> (if not hasNot then "NOT " else mempty) <> "EXISTS (" <> readPlanToCountQuery x <> ")") (findNullEmbedRel fld)
|
||||||
|
pgFmtLogicTreeCount qiCount (CoercibleStmnt flt) = pgFmtFilter qiCount flt
|
||||||
|
|
||||||
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
||||||
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||||
@@ -230,8 +242,10 @@ getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
|||||||
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
|
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
|
||||||
fromF rel mainQi tblAlias = "FROM " <>
|
fromF rel mainQi tblAlias = "FROM " <>
|
||||||
(case rel of
|
(case rel of
|
||||||
Just ComputedRelationship{relFunction,relTable} -> fromQi relFunction <> "(" <> pgFmtIdent (qiName relTable) <> ")"
|
-- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.
|
||||||
_ -> fromQi mainQi) <>
|
-- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386
|
||||||
|
Just ComputedRelationship{relFunction,relTableAlias,relTable} -> fromQi relFunction <> "(" <> pgFmtIdent (qiName relTableAlias) <> "::" <> fromQi relTable <> ")"
|
||||||
|
_ -> fromQi mainQi) <>
|
||||||
maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias <>
|
maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias <>
|
||||||
(case rel of
|
(case rel of
|
||||||
Just Relationship{relCardinality=M2M Junction{junTable=jt}} -> ", " <> fromQi jt
|
Just Relationship{relCardinality=M2M Junction{junTable=jt}} -> ", " <> fromQi jt
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module PostgREST.Query.SqlFragment
|
|||||||
, mutRangeF
|
, mutRangeF
|
||||||
, orderF
|
, orderF
|
||||||
, pgFmtColumn
|
, pgFmtColumn
|
||||||
|
, pgFmtFilter
|
||||||
, pgFmtIdent
|
, pgFmtIdent
|
||||||
, pgFmtJoinCondition
|
, pgFmtJoinCondition
|
||||||
, pgFmtLogicTree
|
, pgFmtLogicTree
|
||||||
@@ -336,7 +337,7 @@ pgFmtArrayLiteralForField values _ = unknownLiteral (pgBuildArrayLiteral values)
|
|||||||
|
|
||||||
|
|
||||||
pgFmtFilter :: QualifiedIdentifier -> CoercibleFilter -> SQL.Snippet
|
pgFmtFilter :: QualifiedIdentifier -> CoercibleFilter -> SQL.Snippet
|
||||||
pgFmtFilter _ (CoercibleFilterNullEmbed hasNot fld) = pgFmtIdent fld <> " IS " <> (if hasNot then "NOT" else mempty) <> " NULL"
|
pgFmtFilter _ (CoercibleFilterNullEmbed hasNot fld) = pgFmtIdent fld <> " IS " <> (if not hasNot then "NOT " else mempty) <> "DISTINCT FROM NULL"
|
||||||
pgFmtFilter _ (CoercibleFilter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams
|
pgFmtFilter _ (CoercibleFilter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams
|
||||||
pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField table fld <> case oper of
|
pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField table fld <> case oper of
|
||||||
Op op val -> " " <> simpleOperator op <> " " <> pgFmtUnknownLiteralForField (unknownLiteral val) fld
|
Op op val -> " " <> simpleOperator op <> " " <> pgFmtUnknownLiteralForField (unknownLiteral val) fld
|
||||||
|
|||||||
+28
-80
@@ -17,9 +17,6 @@ module PostgREST.Response
|
|||||||
, updateResponse
|
, updateResponse
|
||||||
, addRetryHint
|
, addRetryHint
|
||||||
, isServiceUnavailable
|
, isServiceUnavailable
|
||||||
, optionalRollback
|
|
||||||
, concatPrefAppsHeaders
|
|
||||||
, addPrefToHeaders
|
|
||||||
, traceHeaderMiddleware
|
, traceHeaderMiddleware
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -42,10 +39,9 @@ import qualified PostgREST.Response.OpenAPI as OpenAPI
|
|||||||
import PostgREST.ApiRequest (ApiRequest (..),
|
import PostgREST.ApiRequest (ApiRequest (..),
|
||||||
InvokeMethod (..))
|
InvokeMethod (..))
|
||||||
import PostgREST.ApiRequest.Preferences (PreferRepresentation (..),
|
import PostgREST.ApiRequest.Preferences (PreferRepresentation (..),
|
||||||
PreferTransaction (..),
|
|
||||||
Preferences (..),
|
Preferences (..),
|
||||||
shouldCount,
|
prefAppliedHeader,
|
||||||
toAppliedHeader)
|
shouldCount)
|
||||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
@@ -68,11 +64,12 @@ import Protolude.Conv (toS)
|
|||||||
|
|
||||||
|
|
||||||
readResponse :: Bool -> QualifiedIdentifier -> ApiRequest -> ResultSet -> Wai.Response
|
readResponse :: Bool -> QualifiedIdentifier -> ApiRequest -> ResultSet -> Wai.Response
|
||||||
readResponse headersOnly identifier ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
readResponse headersOnly identifier ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} resultSet = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
let
|
let
|
||||||
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing
|
||||||
headers =
|
headers =
|
||||||
[ contentRange
|
[ contentRange
|
||||||
, ( "Content-Location"
|
, ( "Content-Location"
|
||||||
@@ -82,6 +79,7 @@ readResponse headersOnly identifier ctxApiRequest@ApiRequest{..} resultSet = cas
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
++ contentTypeHeaders ctxApiRequest
|
++ contentTypeHeaders ctxApiRequest
|
||||||
|
++ prefHeader
|
||||||
rsOrErrBody = if status == HTTP.status416
|
rsOrErrBody = if status == HTTP.status416
|
||||||
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
|
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
|
||||||
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
@@ -98,6 +96,9 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiReques
|
|||||||
let
|
let
|
||||||
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
|
prefHeader = prefAppliedHeader $
|
||||||
|
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
|
||||||
|
preferRepresentation Nothing preferCount preferTransaction preferMissing
|
||||||
headers =
|
headers =
|
||||||
catMaybes
|
catMaybes
|
||||||
[ if null rsLocation then
|
[ if null rsLocation then
|
||||||
@@ -111,20 +112,15 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiReques
|
|||||||
)
|
)
|
||||||
, Just . RangeQuery.contentRangeH 1 0 $
|
, Just . RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
, if null pkCols && isNothing (qsOnConflict iQueryParams) then
|
, prefHeader
|
||||||
Nothing
|
|
||||||
else
|
|
||||||
toAppliedHeader <$> preferResolution
|
|
||||||
, toAppliedHeader <$> preferMissing
|
|
||||||
]
|
]
|
||||||
|
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> response HTTP.status201 (addPrefToHeaders headers Full ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
Just Full -> response HTTP.status201 (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
||||||
Just None -> response HTTP.status201 (addPrefToHeaders headers None) mempty
|
Just None -> response HTTP.status201 headers mempty
|
||||||
Just HeadersOnly -> response HTTP.status201 (addPrefToHeaders headers HeadersOnly) mempty
|
Just HeadersOnly -> response HTTP.status201 headers mempty
|
||||||
Nothing -> response HTTP.status201 headers mempty
|
Nothing -> response HTTP.status201 headers mempty
|
||||||
|
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
@@ -137,12 +133,12 @@ updateResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet
|
|||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
headers = catMaybes [contentRangeHeader, toAppliedHeader <$> preferMissing]
|
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing
|
||||||
|
headers = catMaybes [contentRangeHeader, prefHeader]
|
||||||
|
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> response HTTP.status200 (addPrefToHeaders headers Full ++ contentTypeHeaders ctxApiRequest)
|
Just Full -> response HTTP.status200 (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
||||||
(LBS.fromStrict rsBody)
|
Just None -> response HTTP.status204 headers mempty
|
||||||
Just None -> response HTTP.status204 (addPrefToHeaders headers None) mempty
|
|
||||||
_ -> response HTTP.status204 headers mempty
|
_ -> response HTTP.status204 headers mempty
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
@@ -153,11 +149,12 @@ singleUpsertResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resu
|
|||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing
|
||||||
|
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> response HTTP.status200 (contentTypeHeaders ctxApiRequest ++ [toAppliedHeader Full]) (LBS.fromStrict rsBody)
|
Just Full -> response HTTP.status200 (contentTypeHeaders ctxApiRequest ++ prefHeader) (LBS.fromStrict rsBody)
|
||||||
Just None -> response HTTP.status204 [toAppliedHeader None] mempty
|
Just None -> response HTTP.status204 prefHeader mempty
|
||||||
_ -> response HTTP.status204 [] mempty
|
_ -> response HTTP.status204 prefHeader mempty
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
@@ -170,12 +167,12 @@ deleteResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet
|
|||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
RangeQuery.contentRangeH 1 0 $
|
RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
headers = [contentRangeHeader]
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing
|
||||||
|
headers = contentRangeHeader : prefHeader
|
||||||
|
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> response HTTP.status200 (addPrefToHeaders headers Full ++ contentTypeHeaders ctxApiRequest)
|
Just Full -> response HTTP.status200 (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
||||||
(LBS.fromStrict rsBody)
|
Just None -> response HTTP.status204 headers mempty
|
||||||
Just None -> response HTTP.status204 (addPrefToHeaders headers None) mempty
|
|
||||||
_ -> response HTTP.status204 headers mempty
|
_ -> response HTTP.status204 headers mempty
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
@@ -209,7 +206,7 @@ respondInfo allowHeader =
|
|||||||
Wai.responseLBS HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
Wai.responseLBS HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||||
|
|
||||||
invokeResponse :: InvokeMethod -> Routine -> ApiRequest -> ResultSet -> Wai.Response
|
invokeResponse :: InvokeMethod -> Routine -> ApiRequest -> ResultSet -> Wai.Response
|
||||||
invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
invokeResponse invMethod proc ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} resultSet = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
@@ -219,7 +216,8 @@ invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resu
|
|||||||
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
|
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
|
||||||
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
else LBS.fromStrict rsBody
|
else LBS.fromStrict rsBody
|
||||||
headers = [contentRange]
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing
|
||||||
|
headers = contentRange : prefHeader
|
||||||
|
|
||||||
if Routine.funcReturnsVoid proc then
|
if Routine.funcReturnsVoid proc then
|
||||||
response HTTP.status204 headers mempty
|
response HTTP.status204 headers mempty
|
||||||
@@ -278,62 +276,12 @@ addRetryHint delay response = do
|
|||||||
isServiceUnavailable :: Wai.Response -> Bool
|
isServiceUnavailable :: Wai.Response -> Bool
|
||||||
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
||||||
|
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> ExceptT Error.Error IO Wai.Response -> ExceptT Error.Error IO Wai.Response
|
|
||||||
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} resp = do
|
|
||||||
newRes <- catchError resp $ return . Error.errorResponseFor
|
|
||||||
return $ Wai.mapResponseHeaders preferenceApplied newRes
|
|
||||||
where
|
|
||||||
shouldCommit =
|
|
||||||
configDbTxAllowOverride && preferTransaction == Just Commit
|
|
||||||
shouldRollback =
|
|
||||||
configDbTxAllowOverride && preferTransaction == Just Rollback
|
|
||||||
preferenceApplied
|
|
||||||
| shouldCommit =
|
|
||||||
addHeadersIfNotIncluded
|
|
||||||
[toAppliedHeader Commit]
|
|
||||||
| shouldRollback =
|
|
||||||
addHeadersIfNotIncluded
|
|
||||||
[toAppliedHeader Rollback]
|
|
||||||
| otherwise =
|
|
||||||
identity
|
|
||||||
|
|
||||||
-- | Add headers not already included to allow the user to override them instead of duplicating them
|
-- | Add headers not already included to allow the user to override them instead of duplicating them
|
||||||
addHeadersIfNotIncluded :: [HTTP.Header] -> [HTTP.Header] -> [HTTP.Header]
|
addHeadersIfNotIncluded :: [HTTP.Header] -> [HTTP.Header] -> [HTTP.Header]
|
||||||
addHeadersIfNotIncluded newHeaders initialHeaders =
|
addHeadersIfNotIncluded newHeaders initialHeaders =
|
||||||
filter (\(nk, nv) -> isNothing $ find (\(ik, iv) -> ik == nk && nv == iv) initialHeaders) newHeaders ++
|
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
||||||
initialHeaders
|
initialHeaders
|
||||||
|
|
||||||
-- | Filters out multiple Preference-Applied Headers from the list and concatenate them into a single Preference-Applied header:
|
|
||||||
--
|
|
||||||
-- >>> :{
|
|
||||||
-- concatPrefAppsHeaders
|
|
||||||
-- [("Content-Type","application/json")
|
|
||||||
-- , ("Preference-Applied","tx=commit")
|
|
||||||
-- , ("Preference-Applied","return=minimal")]
|
|
||||||
-- :}
|
|
||||||
-- [("Content-Type","application/json"),("Preference-Applied","tx=commit, return=minimal")]
|
|
||||||
|
|
||||||
concatPrefAppsHeaders :: [HTTP.Header] -> [HTTP.Header]
|
|
||||||
concatPrefAppsHeaders headers = otherHeaders ++ [(HTTP.hPreferenceApplied, combinedPrefApps)]
|
|
||||||
where
|
|
||||||
(prefApps, otherHeaders) = L.partition (\(k, _) -> k == HTTP.hPreferenceApplied) headers
|
|
||||||
prefAppsValues = [ v | (_,v) <- prefApps]
|
|
||||||
combinedPrefApps = BS.intercalate ", " prefAppsValues
|
|
||||||
|
|
||||||
-- | Given response headers and a preferRepresentation value, add
|
|
||||||
-- preferRepresentation to Preference-Applied
|
|
||||||
--
|
|
||||||
-- >>> :{
|
|
||||||
-- addPrefToHeaders
|
|
||||||
-- [("Content-Type", "application/json")
|
|
||||||
-- , ("Preference-Applied", "tx=commit")]
|
|
||||||
-- None
|
|
||||||
-- :}
|
|
||||||
-- [("Content-Type","application/json"),("Preference-Applied","tx=commit, return=minimal")]
|
|
||||||
|
|
||||||
addPrefToHeaders :: [HTTP.Header] -> PreferRepresentation -> [HTTP.Header]
|
|
||||||
addPrefToHeaders headers pref = concatPrefAppsHeaders (headers ++ [toAppliedHeader pref])
|
|
||||||
|
|
||||||
traceHeaderMiddleware :: AppConfig -> Wai.Middleware
|
traceHeaderMiddleware :: AppConfig -> Wai.Middleware
|
||||||
traceHeaderMiddleware AppConfig{configServerTraceHeader} app req respond =
|
traceHeaderMiddleware AppConfig{configServerTraceHeader} app req respond =
|
||||||
case configServerTraceHeader of
|
case configServerTraceHeader of
|
||||||
|
|||||||
@@ -882,6 +882,7 @@ allComputedRels =
|
|||||||
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
||||||
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
||||||
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
(QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>
|
||||||
|
pure (QualifiedIdentifier mempty mempty) <*>
|
||||||
column HD.bool <*>
|
column HD.bool <*>
|
||||||
column HD.bool
|
column HD.bool
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ data Relationship = Relationship
|
|||||||
{ relFunction :: QualifiedIdentifier
|
{ relFunction :: QualifiedIdentifier
|
||||||
, relTable :: QualifiedIdentifier
|
, relTable :: QualifiedIdentifier
|
||||||
, relForeignTable :: QualifiedIdentifier
|
, relForeignTable :: QualifiedIdentifier
|
||||||
|
, relTableAlias :: QualifiedIdentifier
|
||||||
, relToOne :: Bool
|
, relToOne :: Bool
|
||||||
, relIsSelf :: Bool
|
, relIsSelf :: Bool
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@ nix:
|
|||||||
extra-deps:
|
extra-deps:
|
||||||
- git: https://github.com/PostgREST/postgresql-libpq.git
|
- git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
- hasql-notifications-0.2.0.5
|
- hasql-notifications-0.2.0.6
|
||||||
- hasql-pool-0.10
|
- hasql-pool-0.10
|
||||||
|
|||||||
+3
-3
@@ -16,12 +16,12 @@ packages:
|
|||||||
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
git: https://github.com/PostgREST/postgresql-libpq.git
|
git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
- completed:
|
- completed:
|
||||||
hackage: hasql-notifications-0.2.0.5@sha256:6c67d2ee42e948162e89176a2f52a2c6bb562678b33688e96e048be9b3d74122,2028
|
hackage: hasql-notifications-0.2.0.6@sha256:16d783f5cd1660fad924fd3769380889de5804e057f09b304dcdc3a3ff11eb3c,2028
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
sha256: 8493755b0817a36b910a37d568769ca49ab715ef5dd935a0d09f0716aa39da51
|
sha256: 2319743501bb3c0bef801014ce61308b8666cef86ae5a97a0a283c0c1ec12d4f
|
||||||
size: 452
|
size: 452
|
||||||
original:
|
original:
|
||||||
hackage: hasql-notifications-0.2.0.5
|
hackage: hasql-notifications-0.2.0.6
|
||||||
- completed:
|
- completed:
|
||||||
hackage: hasql-pool-0.10@sha256:912197a328acb85505f98bb9700d61f366b87659ca45126c5c2d636687b801c3,2112
|
hackage: hasql-pool-0.10@sha256:912197a328acb85505f98bb9700d61f366b87659ca45126c5c2d636687b801c3,2112
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ postJsonArrayTest(){
|
|||||||
|
|
||||||
echo "Running memory usage tests.."
|
echo "Running memory usage tests.."
|
||||||
|
|
||||||
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "16M"
|
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "23M"
|
||||||
jsonKeyTest "1M" "POST" "/leak?columns=blob" "16M"
|
jsonKeyTest "1M" "POST" "/leak?columns=blob" "16M"
|
||||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "16M"
|
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "16M"
|
||||||
|
|
||||||
|
|||||||
@@ -192,3 +192,29 @@ spec = describe "computed relationships" $ do
|
|||||||
{"name":"Windows 10","computed_clients":{"name":"Microsoft"}}
|
{"name":"Windows 10","computed_clients":{"name":"Microsoft"}}
|
||||||
]}
|
]}
|
||||||
]|] { matchHeaders = [matchContentTypeJson] }
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/2963
|
||||||
|
context "can be defined using overloaded functions" $ do
|
||||||
|
it "tables" $ do
|
||||||
|
get "/items?select=*,computed_rel_overload(*)&limit=1"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"computed_rel_overload":[{"id":1}]}]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/items2?select=*,computed_rel_overload(*)&limit=1"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "rpc" $ do
|
||||||
|
get "/rpc/search?id=1&select=*,computed_rel_overload(*)"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"computed_rel_overload":[{"id":1}]}]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/search2?id=1&select=*,computed_rel_overload(*)"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ spec =
|
|||||||
`shouldRespondWith` [json|[{"id":2}]|]
|
`shouldRespondWith` [json|[{"id":2}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/1"
|
, matchHeaders = ["Content-Range" <:> "*/1"
|
||||||
, "Preference-Applied" <:> "return=representation"]
|
, "Preference-Applied" <:> "return=representation, count=exact"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "ignores ?select= when return not set or return=minimal" $ do
|
it "ignores ?select= when return not set or return=minimal" $ do
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ spec actualPgVersion = do
|
|||||||
, matchHeaders = [ matchContentTypeJson
|
, matchHeaders = [ matchContentTypeJson
|
||||||
, matchHeaderAbsent hLocation
|
, matchHeaderAbsent hLocation
|
||||||
, "Content-Range" <:> "*/1"
|
, "Content-Range" <:> "*/1"
|
||||||
, "Preference-Applied" <:> "return=representation"]
|
, "Preference-Applied" <:> "return=representation, count=exact"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can rename and cast the selected columns" $
|
it "can rename and cast the selected columns" $
|
||||||
|
|||||||
@@ -300,3 +300,11 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
|||||||
"code": "PGRST100",
|
"code": "PGRST100",
|
||||||
"hint": null} |]
|
"hint": null} |]
|
||||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works when an RPC returns a dynamic TABLE with a composite type" $
|
||||||
|
get "/rpc/returns_complex?select=val->r&val->i=gt.0.5&order=val->>i.desc" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"r":0.3},
|
||||||
|
{"r":0.2}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ spec =
|
|||||||
, matchHeaders = [matchCTArrayStrip]
|
, matchHeaders = [matchCTArrayStrip]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "strips nulls when Accept: application/vnd.pgrst.array;nulls=stripped" $
|
||||||
|
request methodGet "/organizations?select=*"
|
||||||
|
[("Accept","application/vnd.pgrst.array;nulls=stripped")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1,"name":"Referee Org","manager_id":1},{"id":2,"name":"Auditor Org","manager_id":2},{"id":3,"name":"Acme","referee":1,"auditor":2,"manager_id":3},{"id":4,"name":"Umbrella","referee":1,"auditor":2,"manager_id":4},{"id":5,"name":"Cyberdyne","referee":3,"auditor":4,"manager_id":5},{"id":6,"name":"Oscorp","referee":3,"auditor":4,"manager_id":6}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchCTArrayStrip]
|
||||||
|
}
|
||||||
|
|
||||||
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
|
it "strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped" $
|
||||||
request methodGet "/organizations?limit=1"
|
request methodGet "/organizations?limit=1"
|
||||||
[singularStrip]
|
[singularStrip]
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ spec = do
|
|||||||
it "returns whole range with status 200" $
|
it "returns whole range with status 200" $
|
||||||
get "/items" `shouldRespondWith` 200
|
get "/items" `shouldRespondWith` 200
|
||||||
|
|
||||||
|
context "count with an empty body" $ do
|
||||||
|
it "returns empty body with Content-Range */0" $
|
||||||
|
request methodGet "/items?id=eq.0"
|
||||||
|
[("Prefer", "count=exact")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
{ matchHeaders = ["Content-Range" <:> "*/0"] }
|
||||||
|
|
||||||
context "when I don't want the count" $ do
|
context "when I don't want the count" $ do
|
||||||
it "returns range Content-Range with /*" $
|
it "returns range Content-Range with /*" $
|
||||||
request methodGet "/menagerie"
|
request methodGet "/menagerie"
|
||||||
@@ -211,11 +219,24 @@ spec = do
|
|||||||
, "Content-Range" <:> "2-4/*" ]
|
, "Content-Range" <:> "2-4/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "succeeds if offset equals 0 as a no-op" $
|
context "succeeds if offset equals 0 as a no-op" $ do
|
||||||
get "/items?select=id&offset=0&order=id"
|
it "no items" $ do
|
||||||
`shouldRespondWith`
|
get "/items?offset=0&id=eq.0"
|
||||||
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]
|
`shouldRespondWith`
|
||||||
{ matchHeaders = ["Content-Range" <:> "0-14/*"] }
|
[json|[]|]
|
||||||
|
{ matchHeaders = ["Content-Range" <:> "*/*"] }
|
||||||
|
|
||||||
|
request methodGet "/items?offset=0&id=eq.0"
|
||||||
|
[("Prefer", "count=exact")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
{ matchHeaders = ["Content-Range" <:> "*/0"] }
|
||||||
|
|
||||||
|
it "one or more items" $
|
||||||
|
get "/items?select=id&offset=0&order=id"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]
|
||||||
|
{ matchHeaders = ["Content-Range" <:> "0-14/*"] }
|
||||||
|
|
||||||
it "succeeds if offset is negative as a no-op" $
|
it "succeeds if offset is negative as a no-op" $
|
||||||
get "/items?select=id&offset=-4&order=id"
|
get "/items?select=id&offset=-4&order=id"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ module Feature.Query.RelatedQueriesSpec where
|
|||||||
|
|
||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Network.HTTP.Types
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -256,3 +257,124 @@ spec = describe "related queries" $ do
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- "?table=not.is.null" does a "table IS DISTINCT FROM NULL" instead of a "table IS NOT NULL"
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/2800#issuecomment-1720315818
|
||||||
|
it "embeds verifying that the entire target table row is not null" $ do
|
||||||
|
get "/table_b?select=name,table_a(name)&table_a=not.is.null" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"name":"Test 1","table_a":{"name":"Not null 1"}},
|
||||||
|
{"name":"Test 2","table_a":{"name":null}}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/table_b?select=name,table_a()&table_a=is.null" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"name":"Test 3"}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with count=exact" $ do
|
||||||
|
request methodGet "/projects?select=name,clients(name)&clients=not.is.null"
|
||||||
|
[("Prefer", "count=exact")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"name":"Windows 7", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"Windows 10", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"IOS", "clients":{"name":"Apple"}},
|
||||||
|
{"name":"OSX", "clients":{"name":"Apple"}}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/4" ]
|
||||||
|
}
|
||||||
|
request methodGet "/projects?select=name,clients()&clients=is.null"
|
||||||
|
[("Prefer", "count=exact")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"name":"Orphan"}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
request methodGet "/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)"
|
||||||
|
[("Prefer", "count=exact")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Walmart"},
|
||||||
|
{"id":2,"name":"Target"}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with count=planned" $ do
|
||||||
|
request methodGet "/projects?select=name,clients(name)&clients=not.is.null"
|
||||||
|
[("Prefer", "count=planned")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"name":"Windows 7", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"Windows 10", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"IOS", "clients":{"name":"Apple"}},
|
||||||
|
{"name":"OSX", "clients":{"name":"Apple"}}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 206
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/1200" ]
|
||||||
|
}
|
||||||
|
request methodGet "/projects?select=name,clients()&clients=is.null"
|
||||||
|
[("Prefer", "count=planned")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"name":"Orphan"}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
request methodGet "/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)"
|
||||||
|
[("Prefer", "count=planned")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Walmart"},
|
||||||
|
{"id":2,"name":"Target"}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 206
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/952" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with count=estimated" $ do
|
||||||
|
request methodGet "/projects?select=name,clients(name)&clients=not.is.null"
|
||||||
|
[("Prefer", "count=estimated")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"name":"Windows 7", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"Windows 10", "clients":{"name":"Microsoft"}},
|
||||||
|
{"name":"IOS", "clients":{"name":"Apple"}},
|
||||||
|
{"name":"OSX", "clients":{"name":"Apple"}}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 206
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/1200" ]
|
||||||
|
}
|
||||||
|
request methodGet "/projects?select=name,clients()&clients=is.null"
|
||||||
|
[("Prefer", "count=estimated")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"name":"Orphan"}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
request methodGet "/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)"
|
||||||
|
[("Prefer", "count=estimated")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Walmart"},
|
||||||
|
{"id":2,"name":"Target"}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 206
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/952" ]
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should not be updated, either
|
-- the rows should not be updated, either
|
||||||
@@ -88,8 +87,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 4 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should not be updated, either
|
-- the rows should not be updated, either
|
||||||
@@ -145,8 +143,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should not exist, either
|
-- the rows should not exist, either
|
||||||
@@ -161,8 +158,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should not exist, either
|
-- the rows should not exist, either
|
||||||
@@ -177,8 +173,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should not exist, either
|
-- the rows should not exist, either
|
||||||
@@ -226,8 +221,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should still exist
|
-- the rows should still exist
|
||||||
@@ -244,8 +238,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 5 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular ]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- the rows should still exist
|
-- the rows should still exist
|
||||||
@@ -318,8 +311,7 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
[json|{"details":"The result contains 2 rows","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST116","hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [ matchContentTypeSingular
|
, matchHeaders = [ matchContentTypeSingular]
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- should rollback function
|
-- should rollback function
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
|
|||||||
preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]
|
preferRollback = [("Prefer", "return=representation"), ("Prefer", "tx=rollback")]
|
||||||
|
|
||||||
withoutPreferenceApplied = []
|
withoutPreferenceApplied = []
|
||||||
withPreferenceCommitApplied = [ "Preference-Applied" <:> "tx=commit" ]
|
withPreferenceCommitApplied = [ matchHeaderValuePresent "Preference-Applied" "tx=commit" ]
|
||||||
withPreferenceRollbackApplied = [ "Preference-Applied" <:> "tx=rollback" ]
|
withPreferenceRollbackApplied = [ matchHeaderValuePresent "Preference-Applied" "tx=rollback" ]
|
||||||
|
|
||||||
shouldRespondToReads reqHeaders respHeaders = do
|
shouldRespondToReads reqHeaders respHeaders = do
|
||||||
it "responds to GET" $ do
|
it "responds to GET" $ do
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ matchCTArrayStrip = "Content-Type" <:> "application/vnd.pgrst.array+json;nulls=s
|
|||||||
matchCTSingularStrip :: MatchHeader
|
matchCTSingularStrip :: MatchHeader
|
||||||
matchCTSingularStrip = "Content-Type" <:> "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8"
|
matchCTSingularStrip = "Content-Type" <:> "application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8"
|
||||||
|
|
||||||
|
matchHeaderValuePresent :: HeaderName -> BS.ByteString -> MatchHeader
|
||||||
|
matchHeaderValuePresent name val = MatchHeader $ \headers _ ->
|
||||||
|
case lookup name headers of
|
||||||
|
Just hdr -> if val `BS.isInfixOf` hdr then Nothing else Just $ "missing header value: " <> toS val <> "\n"
|
||||||
|
Nothing -> Just $ "missing header: " <> toS (original name) <> "\n"
|
||||||
|
|
||||||
matchHeaderAbsent :: HeaderName -> MatchHeader
|
matchHeaderAbsent :: HeaderName -> MatchHeader
|
||||||
matchHeaderAbsent name = MatchHeader $ \headers _body ->
|
matchHeaderAbsent name = MatchHeader $ \headers _body ->
|
||||||
case lookup name headers of
|
case lookup name headers of
|
||||||
|
|||||||
Vendored
+5
@@ -853,3 +853,8 @@ INSERT INTO datarep_next_two_todos VALUES (2, 1, 3, 'do these first');
|
|||||||
TRUNCATE TABLE bitchar_with_length CASCADE;
|
TRUNCATE TABLE bitchar_with_length CASCADE;
|
||||||
INSERT INTO bitchar_with_length(bit, char) VALUES ('00000', 'aaaaa');
|
INSERT INTO bitchar_with_length(bit, char) VALUES ('00000', 'aaaaa');
|
||||||
INSERT INTO bitchar_with_length(bit, char) VALUES ('11111', 'bbbbb');
|
INSERT INTO bitchar_with_length(bit, char) VALUES ('11111', 'bbbbb');
|
||||||
|
|
||||||
|
TRUNCATE TABLE table_a CASCADE;
|
||||||
|
INSERT INTO table_a(id, name) VALUES (1, 'Not null 1'), (2, null), (3, 'Not null 2');
|
||||||
|
TRUNCATE TABLE table_b CASCADE;
|
||||||
|
INSERT INTO table_b(table_a_id, name) VALUES (1, 'Test 1'), (2, 'Test 2'), (null, 'Test 3');
|
||||||
|
|||||||
Vendored
+103
@@ -3368,3 +3368,106 @@ $$ language sql;
|
|||||||
create function returns_setof_record_params(id int, name text) returns setof record as $$
|
create function returns_setof_record_params(id int, name text) returns setof record as $$
|
||||||
select * from projects p where p.id >= $1 and p.name like $2;
|
select * from projects p where p.id >= $1 and p.name like $2;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
|
create function raise_sqlstate_test1() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC","details":"DEF","hint":"XYZ"}',
|
||||||
|
detail = '{"status":332,"status_text":"My Custom Status","headers":{"X-Header":"str"}}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_test2() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC"}',
|
||||||
|
detail = '{"status":332,"headers":{"X-Header":"str"}}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_test3() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC"}',
|
||||||
|
detail = '{"status":404,"headers":{"X-Header":"str"}}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_test4() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC"}',
|
||||||
|
detail = '{"status":404,"status_text":"My Not Found","headers":{"X-Header":"str"}}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_invalid_json_message() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = 'INVALID',
|
||||||
|
detail = '{"status":332,"headers":{"X-Header":"str"}}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_invalid_json_details() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC","details":"DEF"}',
|
||||||
|
detail = 'INVALID';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create function raise_sqlstate_missing_details() returns void
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise sqlstate 'PGRST' USING
|
||||||
|
message = '{"code":"123","message":"ABC","details":"DEF"}';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create table table_a (
|
||||||
|
id int primary key,
|
||||||
|
name text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table table_b (
|
||||||
|
table_a_id int references table_a(id),
|
||||||
|
name text
|
||||||
|
);
|
||||||
|
|
||||||
|
create or replace function test.returns_complex()
|
||||||
|
returns table(id int, val complex) as $$
|
||||||
|
select 1, row(0.1, 0.5)::complex as val
|
||||||
|
union
|
||||||
|
select 2, row(0.2, 0.6)::complex as val
|
||||||
|
union
|
||||||
|
select 3, row(0.3, 0.7)::complex as val;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function computed_rel_overload(items) returns setof items2 as $$
|
||||||
|
select * from items2 limit 1
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function computed_rel_overload(items2) returns setof items2 as $$
|
||||||
|
select * from items2 limit 2
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function search2(id bigint) returns setof items2
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
as $$ begin
|
||||||
|
return query select items2.id from items2 where items2.id=search2.id;
|
||||||
|
end$$;
|
||||||
|
|||||||
Reference in New Issue
Block a user