[
  {
    "path": ".cirrus.yml",
    "content": "freebsd_instance:\n  image_family: freebsd-14-3\n\nbuild_task:\n  # Don't change this name without adjusting .github/workflows/build.yaml\n  name: Build FreeBSD (Stack)\n  install_script: pkg install -y postgresql16-client hs-stack git\n\n  only_if: |\n    $CIRRUS_TAG != '' || $CIRRUS_BRANCH == 'main' || $CIRRUS_BRANCH =~ 'v*' ||\n    changesInclude(\n      '.github/workflows/build.yaml',\n      '.github/actions/artifact-from-cirrus/**',\n      '.cirrus.yml',\n      'postgrest.cabal',\n      'stack.yaml*',\n      '**.hs'\n    )\n\n  stack_cache:\n    folders: /.stack\n    fingerprint_script:\n      - echo $CIRRUS_OS\n      - stack --version\n      - md5sum postgrest.cabal\n      - md5sum stack.yaml.lock\n\n  stack_work_cache:\n    folders: .stack-work\n    fingerprint_script:\n      - echo $CIRRUS_OS\n      - stack --version\n      - md5sum postgrest.cabal\n      - md5sum stack.yaml.lock\n      - find main src -type f -iname '*.hs' -exec md5sum \"{}\" +\n\n  build_script: |\n    stack build -j 1 --local-bin-path . --copy-bins\n    strip postgrest\n\n  bin_artifacts:\n    path: postgrest\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "/CHANGELOG.md merge=union\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\npatreon: postgrest\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report to help us improve\ntype: Bug\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\nBefore reporting a bug:\nIf your database schema has changed while the PostgREST server is running,\nsend the server a SIGUSR1 signal or restart it (http://postgrest.org/en/stable/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.\n-->\n### Environment\n\n* PostgreSQL version: (if using docker, specify the image)\n* PostgREST version: (if using docker, specify the image)\n* Operating system:\n\n### Description of issue\n\nDescribe the behavior you expected vs the actual behavior. Include:\n\n- A minimal SQL definition.\n- How you make the request to PostgREST (curl command preferred).\n- The PostgREST response.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an enhancement for this project\ntype: Feature\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Problem\n\nA clear and concise description of what the problem is.\n\n## Solution\n\nA clear and concise description of what you want to happen.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nWhen submitting a new feature or fix:\n\n- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased\n- If relevant, update the docs\n- Use a prefix for the PR title or commits, e.g. \"fix: description of the fix\".\n  + `add`, Add a new feature\n  + `amend`, To amend an unrealease commit\n  + `change`, Breaking changes\n  + `chore`, Maintenance, update sponsors, changelog, readme etc\n  + `ci`, CI configuration files and scripts\n  + `docs`, Documentation\n  + `fix`, Bug fix\n  + `nix`, Related to Nix\n  + `perf`, Performance improvements\n  + `refactor`, Refactoring code\n  + `remove`, Remove a feature or fix\n  + `test`, Adding tests\n  + Other prefixes may be used if necessary\n- If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message\n-->\n"
  },
  {
    "path": ".github/actionlint.yml",
    "content": "# TODO: Remove this once a new actionlint release has been cut\n# and made its way to us through nixpkgs.\nself-hosted-runner:\n  labels:\n    - ubuntu-24.04-arm\n"
  },
  {
    "path": ".github/actions/artifact-from-cirrus/action.yaml",
    "content": "name: Artifact from Cirrus\n\ndescription: Waits for a specific Cirrus CI run to complete, then downloads the artifact and uploads it to the current workflow. This will silently succeed if Cirrus CI did not schedule a task within 2 minutes.\n\ninputs:\n  download:\n    description: Name of Artifact to download from Cirrus CI\n    required: true\n  task:\n    description: Name of Cirrus Task\n    required: true\n  token:\n    description: GitHub Token\n    required: true\n  upload:\n    description: Name of Artifact to upload on GitHub Actions\n    required: true\n\nruns:\n  using: composite\n  steps:\n    - shell: bash\n      run: echo \"GH_TOKEN=${{ inputs.token }}\" >> \"$GITHUB_ENV\"\n    - name: Wait for Check Suite to be created\n      id: check-suite\n      env:\n        # GITHUB_SHA does weird things for pull request, so we roll our own:\n        COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}\n      shell: bash\n      run: |\n        get_check_runs_url() {\n          gh api \"repos/{owner}/{repo}/commits/${COMMIT}/check-suites\" \\\n            | jq -r '.check_suites[] | select(.app.slug == \"cirrus-ci\") | .check_runs_url'\n        }\n        for _ in $(seq 1 12); do\n          check_runs_url=\"$(get_check_runs_url)\"\n          if [ -z \"$check_runs_url\" ]; then\n            echo \"Cirrus CI task has not started, yet. Waiting...\"\n            sleep 10\n          else\n            echo \"check_runs_url=$check_runs_url\" >> \"$GITHUB_OUTPUT\"\n            exit 0\n          fi\n        done\n        >&2 echo \"Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?\"\n    - name: Find task by name\n      id: find-task\n      if: steps.check-suite.outputs.check_runs_url\n      shell: bash\n      run: |\n        get_number_of_tasks() {\n          gh api \"${{ steps.check-suite.outputs.check_runs_url }}\" \\\n            | jq -r '.check_runs | map(select(.name == \"${{ inputs.task }}\")) | length'\n        }\n        tasks=\"$(get_number_of_tasks)\"\n        case \"$tasks\" in\n          0)\n            echo \"Task not found, assuming it's skipped intentionally...\"\n            exit 0\n            ;;\n          1)\n            echo \"task_found=1\" >> \"$GITHUB_OUTPUT\"\n            exit 0\n            ;;\n          *)\n            >&2 echo \"More than 1 task with the same name found. Don't know what to do...\"\n            exit 1\n            ;;\n        esac\n    - name: Wait for Cirrus CI to complete task\n      if: steps.find-task.outputs.task_found\n      shell: bash\n      run: |\n        get_conclusion() {\n          gh api \"${{ steps.check-suite.outputs.check_runs_url }}\" \\\n            | jq -r '.check_runs[] | select(.name == \"${{ inputs.task }}\" and .status == \"completed\") | .conclusion'\n        }\n        while true; do\n          conclusion=\"$(get_conclusion)\"\n          if [ -z \"$conclusion\" ]; then\n            echo \"Cirrus CI task has not completed, yet. Waiting...\"\n            sleep 30\n          else\n            if [ \"$conclusion\" == \"success\" ]; then\n              break\n            else\n              exit 1\n            fi\n          fi\n        done\n    - name: Download artifact from Cirrus CI\n      if: steps.find-task.outputs.task_found\n      id: download\n      shell: bash\n      run: |\n        get_external_id() {\n          gh api \"${{ steps.check-suite.outputs.check_runs_url }}\" \\\n            | jq -er '.check_runs[] | select(.name == \"${{ inputs.task }}\") | .external_id'\n        }\n        archive=\"$(mktemp)\"\n        artifacts=\"$(mktemp -d)\"\n        until curl --no-progress-meter --fail -o \"${archive}\" \\\n                \"https://api.cirrus-ci.com/v1/artifact/task/$(get_external_id)/${{ inputs.download }}.zip\"\n        do\n          # This happens when a tag is pushed on the same commit. In this case the\n          # job is immediately marked as \"completed\" for us, so we end up here after a few\n          # seconds - but the actual Cirrus CI task is still running and didn't produce its artifact, yet.\n          echo \"Artifact not found on Cirrus CI, yet. Waiting...\"\n          sleep 30\n        done\n        unzip \"${archive}\" -d \"${artifacts}\"\n        echo \"artifacts=${artifacts}\" >> \"$GITHUB_OUTPUT\"\n    - name: Save artifact to GitHub Actions\n      if: steps.find-task.outputs.task_found\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: ${{ inputs.upload }}\n        path: ${{ steps.download.outputs.artifacts }}\n        if-no-files-found: error\n"
  },
  {
    "path": ".github/actions/cache-on-main/action.yaml",
    "content": "name: Cache on main\n\ndescription: Stores caches on main and release branches only, but restores them on all branches.\n\ninputs:\n  path:\n    description: Path(s) to cache\n    required: true\n  save-prs:\n    description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.\n    type: boolean\n  prefix:\n    description: Cache key prefix to be used in both primary key and restore-keys.\n    required: true\n  suffix:\n    description: Cache key suffix to be used only in primary key.\n    required: true\n\nruns:\n  using: composite\n  steps:\n    - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n      if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}\n      with:\n        path: ${{ inputs.path }}\n        key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}\n        restore-keys: |\n          ${{ runner.os }}-${{ inputs.prefix }}-\n    - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n      if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/'))  }}\n      with:\n        path: ${{ inputs.path }}\n        key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}\n        restore-keys: |\n          ${{ runner.os }}-${{ inputs.prefix }}-\n"
  },
  {
    "path": ".github/actions/setup-nix/action.yaml",
    "content": "name: Setup Nix\n\ndescription: Installs nix, sets up cachix and installs a subset of tooling.\n\ninputs:\n  authToken:\n    description: Token to pass to cachix\n  tools:\n    description: Tools to install with nix-env -iA <tools>\n\nruns:\n  using: composite\n  steps:\n    - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34\n      with:\n        nix_conf: |-\n          always-allow-substitutes = true\n          max-jobs = auto\n    - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17\n      with:\n        name: postgrest\n        authToken: ${{ inputs.authToken }}\n        skipPush: ${{ inputs.authToken == '' }}\n    - if: ${{ inputs.tools }}\n      run: nix-env -f default.nix -iA ${{ inputs.tools }}\n      shell: bash\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "codecov:\n  branch: main\n  require_ci_to_pass: false\n\ncomment: false\n\ncoverage:\n  status:\n    project:\n      default:\n        target: auto\n        threshold: 1%\n        only_pulls: false\n    patch:\n      default:\n        target: auto\n        threshold: 1%\n        only_pulls: true\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:best-practices\"\n  ],\n  \"baseBranchPatterns\": [\n    \"main\",\n    \"/^v[0-9]+/\"\n  ],\n  \"rebaseWhen\": \"conflicted\",\n  \"pip_requirements\": {\n    \"enabled\": false\n  },\n  \"packageRules\": [\n    {\n      \"matchBaseBranches\": [\n        \"/^v[0-9]+/\"\n      ],\n      \"matchManagers\": [\n        \"haskell-cabal\"\n      ],\n      \"enabled\": false\n    },\n    {\n      \"matchBaseBranches\": [\n        \"/^v[0-9]+/\"\n      ],\n      \"groupName\": \"all dependencies\"\n    },\n    {\n      \"matchManagers\": [\n        \"haskell-cabal\"\n      ],\n      \"matchPackageNames\": [\n        \"base\",\n        \"bytestring\",\n        \"containers\",\n        \"directory\",\n        \"mtl\",\n        \"parsec\",\n        \"process\",\n        \"text\"\n      ],\n      \"groupName\": \"GHC dependencies\"\n    },\n    {\n      \"matchManagers\": [\n        \"haskell-cabal\"\n      ],\n      \"matchPackageNames\": [\n        \"hasql\",\n        \"hasql-dynamic-statements\",\n        \"hasql-notifications\",\n        \"hasql-transaction\",\n        \"hasql-pool\"\n      ],\n      \"groupName\": \"hasql\"\n    },\n    {\n      \"matchManagers\": [\n        \"haskell-cabal\"\n      ],\n      \"matchPackageNames\": [\n        \"fuzzyset\"\n      ],\n      \"allowedVersions\": \"<0.3\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/backport.yaml",
    "content": "name: Backport\n\non:\n  pull_request_target:\n    types:\n      - closed\n      - labeled\n\njobs:\n  backport:\n    name: Backport\n    runs-on: ubuntu-24.04\n    # It triggers only when PR is already merged on either:\n    #\n    # - The merge event itself (action != labeled) or\n    # - A label event with the right label (backport ...).\n    #\n    # The result will be that we can add the label before or after merge,\n    # but the workflow will only run once the PR had been merged.\n    if: >\n      github.event.pull_request.merged &&\n      (\n        github.event.action != 'labeled' ||\n        startsWith(github.event.label.name, 'backport')\n      )\n    steps:\n\n      # This actions creates the github token using the postgrest app secrets\n      - name: Create Github App Token\n        id: app-token\n        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0\n        with:\n          app-id: ${{ vars.POSTGREST_CI_APP_ID }}\n          private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}\n          permission-contents: write\n          permission-pull-requests: write\n          permission-workflows: write # required when backporting CI changes\n\n      # This is required for backport action to cherry-pick the PR\n      - name: Fetch PR ref\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          token: ${{ steps.app-token.outputs.token }}\n\n      # Backport action that creates the PR with given settings\n      - name: Create backport PR\n        uses: korthout/backport-action@4aaf0e03a94ff0a619c9a511b61aeb42adea5b02 # v4.2.0\n        with:\n          github_token: ${{ steps.app-token.outputs.token }}\n          pull_description: 'Backport for #${pull_number}.'\n          pull_title: '${target_branch}: ${pull_title}'\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Build\n\non:\n  workflow_call:\n    secrets:\n      CACHIX_AUTH_TOKEN:\n        required: false\n  pull_request:\n    branches:\n      - main\n      - v[0-9]+\n    paths:\n      - .github/workflows/build.yaml\n      - .github/actions/**\n      - .github/scripts/**\n      - .github/*\n      - '*.nix'\n      - nix/**\n      - .cirrus.yml\n      - cabal.project*\n      - postgrest.cabal\n      - stack.yaml*\n      - '**.hs'\n      - '!**.md'\n\nconcurrency:\n  # Terminate all previous runs of the same workflow for pull requests\n  group: build-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  static:\n    name: Nix - Linux x86-64 static\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n\n      - name: Build static executable\n        run: nix-build -A postgrestStatic -A postgrestStatic.tests\n      - name: Save built executable as artifact\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: postgrest-linux-static-x86-64\n          path: result/bin/postgrest\n          if-no-files-found: error\n\n      - name: Build Docker image\n        run: nix-build -A docker.image --out-link postgrest-docker.tar.gz\n      - name: Save built Docker image as artifact\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: postgrest-docker-x86-64\n          path: postgrest-docker.tar.gz\n          if-no-files-found: error\n\n\n  macos:\n    name: Nix - MacOS\n    runs-on: macos-15\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n      - name: Install gnu sed\n        run: brew install gnu-sed\n\n      - name: Build everything\n        run: |\n          # The --dry-run will give us a list of derivations to download from cachix and\n          # derivations to build. We only take those that would have to be built and then build\n          # those explicitly. This has the advantage that pure verification will not include\n          # a download anymore, making it much faster. If something needs to be built, only\n          # the dependencies required to do so will be downloaded, but not everything.\n          nix-build --dry-run 2>&1 \\\n            | gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \\\n            | xargs nix-build\n\n\n  stack:\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - name: Linux aarch64\n            runs-on: ubuntu-24.04-arm\n            cache: |\n              ~/.stack/pantry\n              ~/.stack/snapshots\n              ~/.stack/stack.sqlite3\n            artifact: postgrest-ubuntu-aarch64\n            deps: sudo apt-get update && sudo apt-get install libpq-dev\n\n          - name: MacOS aarch64\n            runs-on: macos-14\n            cache: |\n              ~/.stack/pantry\n              ~/.stack/snapshots\n              ~/.stack/stack.sqlite3\n            artifact: postgrest-macos-aarch64\n            deps: brew link --force libpq\n\n          - name: Windows\n            runs-on: windows-2022\n            cache: |\n              C:\\sr\\pantry\n              C:\\sr\\snapshots\n              C:\\sr\\stack.sqlite3\n            deps: Add-Content $env:GITHUB_PATH $env:PGBIN\n            artifact: postgrest-windows-x86-64\n\n    name: Stack - ${{ matrix.name }}\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: haskell-actions/setup@f9150cb1d140e9a9271700670baa38991e6fa25c # v2.10.3\n        with:\n          # This must match the version in stack.yaml's resolver\n          ghc-version: 9.6.7\n          enable-stack: true\n          stack-no-global: true\n          stack-setup-ghc: true\n      - name: Cache ~/.stack\n        uses: ./.github/actions/cache-on-main\n        with:\n          path: ${{ matrix.cache }}\n          prefix: stack\n          suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}\n      - name: Cache .stack-work\n        uses: ./.github/actions/cache-on-main\n        with:\n          path: .stack-work\n          save-prs: true\n          prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}\n          suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}\n      - name: Install dependencies\n        if: matrix.deps\n        run: ${{ matrix.deps }}\n      - name: Build with Stack\n        run: stack build --lock-file error-on-write --local-bin-path result --copy-bins\n      - name: Strip Executable\n        run: strip result/postgrest*\n      - name: Save built executable as artifact\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: ${{ matrix.artifact }}\n          path: |\n            result/postgrest\n            result/postgrest.exe\n          if-no-files-found: error\n\n\n  freebsd:\n    name: Stack - FreeBSD from CirrusCI\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: ./.github/actions/artifact-from-cirrus\n        with:\n          token: ${{ github.token }}\n          task: Build FreeBSD (Stack)\n          download: bin\n          upload: postgrest-freebsd-x86-64\n\n\n  cabal:\n    strategy:\n      matrix:\n        ghc: ['9.6.7', '9.8.4']\n      fail-fast: false\n    name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: haskell-actions/setup@f9150cb1d140e9a9271700670baa38991e6fa25c # v2.10.3\n        with:\n          ghc-version: ${{ matrix.ghc }}\n      - name: Cache .cabal\n        uses: ./.github/actions/cache-on-main\n        with:\n          path: |\n            ~/.cabal/packages\n            ~/.cabal/store\n          prefix: cabal-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}\n          suffix: ${{ hashFiles('postgrest.cabal', 'cabal.project') }}\n      - name: Cache dist-newstyle\n        uses: ./.github/actions/cache-on-main\n        with:\n          path: dist-newstyle\n          save-prs: true\n          prefix: cabal-${{ matrix.ghc }}-dist-newstyle-${{ hashFiles('postgrest.cabal', 'cabal.project', 'cabal.project.freeze') }}\n          suffix: ${{ hashFiles('**/*.hs') }}\n      - name: Install dependencies\n        run: cabal build --only-dependencies --enable-tests --enable-benchmarks\n      - name: Build\n        run: cabal build --enable-tests --enable-benchmarks all\n"
  },
  {
    "path": ".github/workflows/check.yaml",
    "content": "name: Check\n\non:\n  workflow_call:\n    secrets:\n      CACHIX_AUTH_TOKEN:\n        required: false\n  pull_request:\n    branches:\n      - main\n      - v[0-9]+\n\nconcurrency:\n  # Terminate all previous runs of the same workflow for pull requests\n  group: style-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  lint-style:\n    name: Lint & Style\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: style.lint.bin style.styleCheck.bin\n      - name: Run linter (check locally with `nix-shell --run postgrest-lint`)\n        run: postgrest-lint\n      - name: Run style check (auto-format with `nix-shell --run postgrest-style`)\n        run: postgrest-style-check\n\n  commit:\n    if: github.event_name != 'push'  # we don't run this on a push, a failure on push disrupts the release workflow\n    name: Commit\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 100  # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: gitTools.commitCheck.bin\n      - name: Run commitlint (check locally with `nix-shell --run postgrest-commitlint`)\n        run: |\n          # Fetch target branch explicitly\n          git fetch origin ${{ github.base_ref }}\n\n          # Run commitlint\n          postgrest-commitlint --from origin/${{ github.base_ref }} --to HEAD\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n      - v[0-9]+\n\njobs:\n  check:\n    name: Check\n    uses: ./.github/workflows/check.yaml\n    secrets:\n      CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}\n\n\n  docs:\n    name: Docs\n    uses: ./.github/workflows/docs.yaml\n    secrets:\n      CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}\n\n\n  test:\n    name: Test\n    uses: ./.github/workflows/test.yaml\n    secrets:\n      CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}\n      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n\n  build:\n    name: Build\n    uses: ./.github/workflows/build.yaml\n    secrets:\n      CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}\n\n\n  tag:\n    name: Tag\n    concurrency:\n      # Never tag outdated commits on the main branch by skipping superseded commits\n      group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}\n      # TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved\n      cancel-in-progress: false\n    if: vars.RELEASE_ENABLED\n    runs-on: ubuntu-24.04\n    needs:\n      - docs\n      - test\n      - build\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}\n      - name: Tag latest commit\n        run: |\n          cabal_version=\"$(grep -oP '^version:\\s*\\K.*' postgrest.cabal)\"\n\n          if [[ \"$cabal_version\" == *.* ]]; then\n            git fetch --tags\n\n            if [ -z \"$(git tag --list \"v$cabal_version\")\" ]; then\n              git tag \"v$cabal_version\"\n              git push origin \"v$cabal_version\"\n            fi\n          else\n            git tag -f \"devel\"\n            git push -f origin \"devel\"\n          fi\n"
  },
  {
    "path": ".github/workflows/docs.yaml",
    "content": "name: Docs\n\non:\n  workflow_call:\n    secrets:\n      CACHIX_AUTH_TOKEN:\n        required: false\n  pull_request:\n    branches:\n      - main\n      - v[0-9]+\n    paths:\n      - .github/workflows/docs.yaml\n      - .github/actions/setup-nix/**\n      - default.nix\n      - nix/**\n      - docs/**\n      - '!**.md'\n\nconcurrency:\n  # Terminate all previous runs of the same workflow for pull requests\n  group: docs-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - name: Setup Nix Environment\n      uses: ./.github/actions/setup-nix\n      with:\n        authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n        tools: docs.build.bin\n    - run: postgrest-docs-build\n    - run: git diff --exit-code HEAD locales || echo \"Please commit changes to the locales/ folder after running postgrest-docs-build.\"\n\n\n  spellcheck:\n    name: Spellcheck\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - name: Setup Nix Environment\n      uses: ./.github/actions/setup-nix\n      with:\n        authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n        tools: docs.spellcheck.bin docs.dictcheck.bin\n    - name: Run spellcheck\n      run: postgrest-docs-spellcheck\n    - name: Run dictcheck\n      run: postgrest-docs-dictcheck\n"
  },
  {
    "path": ".github/workflows/linkcheck.yaml",
    "content": "name: Linkcheck\n\non:\n  schedule:\n    - cron: '1 2 * * 3'\n  workflow_dispatch:\n\njobs:\n  linkcheck:\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - name: Setup Nix Environment\n      uses: ./.github/actions/setup-nix\n      with:\n        authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n        tools: docs.linkcheck.bin\n    - run: postgrest-docs-linkcheck\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - devel\n      - v*\n\nconcurrency:\n  # Terminate all previous runs of the same workflow for the same tag.\n  group: release-${{ github.ref }}\n  # TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved\n  cancel-in-progress: false\n\njobs:\n  build:\n    name: Build\n    uses: ./.github/workflows/build.yaml\n    secrets:\n      CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}\n\n\n  prepare:\n    name: Prepare\n    runs-on: ubuntu-24.04\n    needs:\n      - build\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Check the version to be released\n        run: |\n          cabal_version=\"$(grep -oP '^version:\\s*\\K.*' postgrest.cabal)\"\n\n          if [ \"${GITHUB_REF_NAME}\" != \"devel\" ] && [ \"${GITHUB_REF_NAME}\" != \"v$cabal_version\" ]; then\n            echo \"Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release...\"\n            exit 1\n          fi\n      - name: Identify changes from CHANGELOG.md\n        run: |\n          if [ \"${GITHUB_REF_NAME}\" == \"devel\" ]; then\n            echo \"Getting unreleased changes...\"\n            sed -n \"1,/## Unreleased/d;/## \\[/q;p\" CHANGELOG.md > CHANGES.md\n          else\n            version=\"$(grep -oP '^version:\\s*\\K.*' postgrest.cabal)\"\n            echo \"Propper release, getting changes for version $version ...\"\n            sed -n \"1,/## \\[$version\\]/d;/## \\[/q;p\" CHANGELOG.md > CHANGES.md\n          fi\n\n          echo \"Relevant extract from CHANGELOG.md:\"\n          cat CHANGES.md\n      - name: Save CHANGES.md as artifact\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: release-changes\n          path: CHANGES.md\n          if-no-files-found: error\n\n\n  github:\n    name: GitHub\n    permissions:\n      contents: write\n    runs-on: ubuntu-24.04\n    needs:\n      - prepare\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Download all artifacts\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          path: artifacts\n      - name: Create release bundle with archives for all builds\n        run: |\n          find artifacts -type f -iname postgrest -exec chmod +x {} \\;\n\n          mkdir -p release-bundle\n\n          tar cJvf \"release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz\" \\\n            -C artifacts/postgrest-linux-static-x86-64 postgrest\n\n          tar cJvf \"release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz\" \\\n            -C artifacts/postgrest-macos-aarch64 postgrest\n\n          tar cJvf \"release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz\" \\\n            -C artifacts/postgrest-freebsd-x86-64 postgrest\n\n          tar cJvf \"release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz\" \\\n            -C artifacts/postgrest-ubuntu-aarch64 postgrest\n\n          zip --junk-paths \"release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip\" \\\n            artifacts/postgrest-windows-x86-64/postgrest.exe\n\n      - name: Save release bundle\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: release-bundle\n          path: release-bundle\n          if-no-files-found: error\n\n      - name: Publish release on GitHub\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          echo \"Releasing version ${GITHUB_REF_NAME} on GitHub...\"\n\n          if [ \"${GITHUB_REF_NAME}\" == \"devel\" ]; then\n            # To replace the existing release, we must first delete the old assets,\n            # then modify the release, then add the new assets.\n            gh release view devel --json assets \\\n              | jq -r '.assets[] | .name' \\\n              | xargs -rn1 \\\n              gh release delete-asset -y devel\n            gh release edit devel \\\n              -t devel \\\n              --verify-tag \\\n              -F artifacts/release-changes/CHANGES.md \\\n              --prerelease\n            gh release upload --clobber devel release-bundle/*\n          else\n            gh release create \"${GITHUB_REF_NAME}\" \\\n              -t \"${GITHUB_REF_NAME}\" \\\n              --verify-tag \\\n              -F artifacts/release-changes/CHANGES.md \\\n              release-bundle/*\n          fi\n\n\n  docker:\n    name: Docker Hub\n    runs-on: ubuntu-24.04-arm\n    needs:\n      - prepare\n    if: |\n      vars.DOCKER_REPO && vars.DOCKER_USER\n    env:\n      DOCKER_REPO: ${{ vars.DOCKER_REPO }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Download x86-64 Docker image\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: postgrest-docker-x86-64\n      - name: Download aarch64 binary\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: postgrest-ubuntu-aarch64\n      - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n      - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          username: ${{ vars.DOCKER_USER }}\n          password: ${{ secrets.DOCKER_PASS }}\n      - name: Build aarch64 Docker image\n        run: |\n          # This only pushes the image via digest, not a tag. This will not appear\n          # in the image list on Docker Hub, yet. It will be later added to the main\n          # tag's manifest.\n          docker buildx build \\\n            -t \"$DOCKER_REPO/postgrest\" \\\n            --platform linux/arm64 \\\n            --output push-by-digest=true,type=image,push=true \\\n            --metadata-file metadata.json \\\n            .\n          echo \"SHA256_ARM=$(jq -r '.\"containerimage.digest\"' metadata.json)\" >> \"$GITHUB_ENV\"\n      - name: Publish images on Docker Hub\n        run: |\n          docker load -i postgrest-docker.tar.gz\n\n          docker tag postgrest:latest \"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}\"\n          docker push \"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}\"\n          docker buildx imagetools create --append \\\n            -t \"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}\" \\\n            \"$DOCKER_REPO/postgrest@$SHA256_ARM\"\n\n          # Only tag 'latest' for full releases\n          if [ \"${GITHUB_REF_NAME}\" != \"devel\" ]; then\n            echo \"Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ...\"\n            docker tag postgrest:latest \"$DOCKER_REPO\"/postgrest:latest\n            docker push \"$DOCKER_REPO\"/postgrest:latest\n            docker buildx imagetools create --append \\\n              -t \"$DOCKER_REPO/postgrest:latest\" \\\n              \"$DOCKER_REPO/postgrest@$SHA256_ARM\"\n          else\n            echo \"Skipping push to 'latest' tag for pre-release...\"\n          fi\n\n\n  docker-description:\n    name: Docker Hub Description\n    runs-on: ubuntu-24.04\n    if: |\n      vars.DOCKER_REPO && vars.DOCKER_USER &&\n      github.ref == 'refs/tags/devel'\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0\n        with:\n          username: ${{ vars.DOCKER_USER }}\n          password: ${{ secrets.DOCKER_PASS }}\n          repository: ${{ vars.DOCKER_REPO }}/postgrest\n          short-description: ${{ github.event.repository.description }}\n          readme-filepath: ./docker-hub-readme.md\n\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Test\n\non:\n  workflow_call:\n    secrets:\n      CACHIX_AUTH_TOKEN:\n        required: false\n      CODECOV_TOKEN:\n        required: false\n  pull_request:\n    branches:\n      - main\n      - v[0-9]+\n    paths:\n      - .github/workflows/test.yaml\n      - .github/workflows/report.yaml\n      - .github/actions/setup-nix/**\n      - default.nix\n      - nix/**\n      - .stylish-haskell.yaml\n      - cabal.project\n      - postgrest.cabal\n      - '**.hs'\n      - test/**\n      - '!**.md'\n\nconcurrency:\n  # Terminate all previous runs of the same workflow for pull requests\n  group: test-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  coverage:\n    name: Coverage\n    runs-on: ubuntu-24.04\n    defaults:\n      run:\n        # Hack for enabling color output, see:\n        # https://github.com/actions/runner/issues/241#issuecomment-842566950\n        shell: script -qec \"bash --noprofile --norc -eo pipefail {0}\"\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin cabalTools.update.bin\n\n      - run: postgrest-cabal-update\n\n      - name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)\n        run: postgrest-coverage\n      - name: Upload coverage to codecov\n        uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3\n        with:\n          files: ./coverage/codecov.json\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n      - name: Run doctests\n        if: always()\n        run: postgrest-test-doctests\n\n      - name: Check the spec tests for idempotence\n        if: always()\n        run: postgrest-test-spec-idempotence\n\n\n  postgres:\n    strategy:\n      fail-fast: false\n      matrix:\n        pgVersion: [13, 14, 15, 16, 17]\n    name: PG ${{ matrix.pgVersion }}\n    runs-on: ubuntu-24.04\n    defaults:\n      run:\n        # Hack for enabling color output, see:\n        # https://github.com/actions/runner/issues/241#issuecomment-842566950\n        shell: script -qec \"bash --noprofile --norc -eo pipefail {0}\"\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin\n\n      - run: postgrest-cabal-update\n\n      - name: Run spec tests\n        if: always()\n        run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec\n\n      - name: Run observability tests\n        if: always()\n        run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability\n\n      - name: Run IO tests\n        if: always()\n        run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv\n\n      - name: Run IO tests on a big schema\n        if: always()\n        run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-big-schema -vv\n\n\n  memory:\n    name: Memory\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: tests.testMemory.bin cabalTools.update.bin\n\n      - run: postgrest-cabal-update\n\n      - name: Run memory tests\n        run: postgrest-test-memory\n\n\n  loadtest:\n    strategy:\n      matrix:\n        kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']\n    name: Loadtest\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n          tools: loadtest.loadtestAgainst.bin loadtest.report.bin cabalTools.update.bin\n\n      - run: postgrest-cabal-update\n\n      - name: Run loadtest\n        env:\n          TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}\n        run: |\n          if [ \"$TARGET_BRANCH\" = \"main\" ]; then\n            latest_tag=$(git tag --sort=-creatordate --list \"v*\" | head -n1)\n          else\n            latest_tag=$(git tag --merged HEAD --sort=-creatordate \"v*\" | head -n1)\n          fi\n          postgrest-loadtest-against -k ${{ matrix.kind }} \"$TARGET_BRANCH\" \"$latest_tag\"\n          postgrest-loadtest-report -g ${{ matrix.kind }} >> \"$GITHUB_STEP_SUMMARY\"\n\n  flake:\n    strategy:\n      fail-fast: false\n      matrix:\n        runs-on:\n          - macos-14 # aarch64-darwin\n          - ubuntu-24.04 # x86_64-linux\n          - ubuntu-24.04-arm # aarch64-linux\n    name: Flake Check\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n      - name: Setup Nix Environment\n        uses: ./.github/actions/setup-nix\n        with:\n          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'\n      - name: Run flake check\n        run: |\n          nix flake check\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\ndb\ndist\n.cabal-sandbox\ncabal.sandbox.config\nhscope.out\ncodex.tags\n.anvil\n.stack-work*\ntags\nsite\n*~\n*#*\n.#*\n*.swp\nresult*\ndist-*\npostgrest.hp\npostgrest.prof\n__pycache__\n*.tix\ncoverage\n.hpc\nloadtest\n.history\n.docs-build\ngen_targets.http\ngen_jwk.json\ngen_private.json\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\nsphinx:\n  configuration: docs/conf.py\npython:\n   install:\n   - requirements: docs/requirements.txt\nbuild:\n  os: ubuntu-24.04\n  tools:\n    python: \"3.11\"\n"
  },
  {
    "path": ".stylish-haskell.yaml",
    "content": "# stylish-haskell configuration file\n# ==================================\n\n# The stylish-haskell tool is mainly configured by specifying steps. These steps\n# are a list, so they have an order, and one specific step may appear more than\n# once (if needed). Each file is processed by these steps in the given order.\nsteps:\n  # Convert some ASCII sequences to their Unicode equivalents. This is disabled\n  # by default.\n  # - unicode_syntax:\n  #     # In order to make this work, we also need to insert the UnicodeSyntax\n  #     # language pragma. If this flag is set to true, we insert it when it's\n  #     # not already present. You may want to disable it if you configure\n  #     # language extensions using some other method than pragmas. Default:\n  #     # true.\n  #     add_language_pragma: true\n\n  # Align the right hand side of some elements.  This is quite conservative\n  # and only applies to statements where each element occupies a single\n  # line.\n  - simple_align:\n      cases: true\n      top_level_patterns: true\n      records: true\n\n  # Import cleanup\n  - imports:\n      # There are different ways we can align names and lists.\n      #\n      # - global: Align the import names and import list throughout the entire\n      #   file.\n      #\n      # - file: Like global, but don't add padding when there are no qualified\n      #   imports in the file.\n      #\n      # - group: Only align the imports per group (a group is formed by adjacent\n      #   import lines).\n      #\n      # - none: Do not perform any alignment.\n      #\n      # Default: global.\n      align: group\n\n      # The following options affect only import list alignment.\n      #\n      # List align has following options:\n      #\n      # - after_alias: Import list is aligned with end of import including\n      #   'as' and 'hiding' keywords.\n      #\n      #   > import qualified Data.List      as List (concat, foldl, foldr, head,\n      #   >                                          init, last, length)\n      #\n      # - with_alias: Import list is aligned with start of alias or hiding.\n      #\n      #   > import qualified Data.List      as List (concat, foldl, foldr, head,\n      #   >                                 init, last, length)\n      #\n      # - new_line: Import list starts always on new line.\n      #\n      #   > import qualified Data.List      as List\n      #   >     (concat, foldl, foldr, head, init, last, length)\n      #\n      # Default: after_alias\n      list_align: after_alias\n\n      # Right-pad the module names to align imports in a group:\n      #\n      # - true: a little more readable\n      #\n      #   > import qualified Data.List       as List (concat, foldl, foldr,\n      #   >                                           init, last, length)\n      #   > import qualified Data.List.Extra as List (concat, foldl, foldr,\n      #   >                                           init, last, length)\n      #\n      # - false: diff-safe\n      #\n      #   > import qualified Data.List as List (concat, foldl, foldr, init,\n      #   >                                     last, length)\n      #   > import qualified Data.List.Extra as List (concat, foldl, foldr,\n      #   >                                           init, last, length)\n      #\n      # Default: true\n      pad_module_names: true\n\n      # Long list align style takes effect when import is too long. This is\n      # determined by 'columns' setting.\n      #\n      # - inline: This option will put as much specs on same line as possible.\n      #\n      # - new_line: Import list will start on new line.\n      #\n      # - new_line_multiline: Import list will start on new line when it's\n      #   short enough to fit to single line. Otherwise it'll be multiline.\n      #\n      # - multiline: One line per import list entry.\n      #   Type with constructor list acts like single import.\n      #\n      #   > import qualified Data.Map as M\n      #   >     ( empty\n      #   >     , singleton\n      #   >     , ...\n      #   >     , delete\n      #   >     )\n      #\n      # Default: inline\n      long_list_align: inline\n\n      # Align empty list (importing instances)\n      #\n      # Empty list align has following options\n      #\n      # - inherit: inherit list_align setting\n      #\n      # - right_after: () is right after the module name:\n      #\n      #   > import Vector.Instances ()\n      #\n      # Default: inherit\n      empty_list_align: inherit\n\n      # List padding determines indentation of import list on lines after import.\n      # This option affects 'long_list_align'.\n      #\n      # - <integer>: constant value\n      #\n      # - module_name: align under start of module name.\n      #   Useful for 'file' and 'group' align settings.\n      list_padding: 4\n\n      # Separate lists option affects formatting of import list for type\n      # or class. The only difference is single space between type and list\n      # of constructors, selectors and class functions.\n      #\n      # - true: There is single space between Foldable type and list of it's\n      #   functions.\n      #\n      #   > import Data.Foldable (Foldable (fold, foldl, foldMap))\n      #\n      # - false: There is no space between Foldable type and list of it's\n      #   functions.\n      #\n      #   > import Data.Foldable (Foldable(fold, foldl, foldMap))\n      #\n      # Default: true\n      separate_lists: true\n\n      # Space surround option affects formatting of import lists on a single\n      # line. The only difference is single space after the initial\n      # parenthesis and a single space before the terminal parenthesis.\n      #\n      # - true: There is single space associated with the enclosing\n      #   parenthesis.\n      #\n      #   > import Data.Foo ( foo )\n      #\n      # - false: There is no space associated with the enclosing parenthesis\n      #\n      #   > import Data.Foo (foo)\n      #\n      # Default: false\n      space_surround: false\n\n  # Language pragmas\n  - language_pragmas:\n      # We can generate different styles of language pragma lists.\n      #\n      # - vertical: Vertical-spaced language pragmas, one per line.\n      #\n      # - compact: A more compact style.\n      #\n      # - compact_line: Similar to compact, but wrap each line with\n      #   `{-#LANGUAGE #-}'.\n      #\n      # Default: vertical.\n      style: vertical\n\n      # Align affects alignment of closing pragma brackets.\n      #\n      # - true: Brackets are aligned in same column.\n      #\n      # - false: Brackets are not aligned together. There is only one space\n      #   between actual import and closing bracket.\n      #\n      # Default: true\n      align: true\n\n      # stylish-haskell can detect redundancy of some language pragmas. If this\n      # is set to true, it will remove those redundant pragmas. Default: true.\n      remove_redundant: true\n\n  # Replace tabs by spaces. This is disabled by default.\n  # - tabs:\n  #     # Number of spaces to use for each tab. Default: 8, as specified by the\n  #     # Haskell report.\n  #     spaces: 8\n\n  # Remove trailing whitespace\n  - trailing_whitespace: {}\n\n# A common setting is the number of columns (parts of) code will be wrapped\n# to. Different steps take this into account. Default: 80.\ncolumns: 70\n\n# By default, line endings are converted according to the OS. You can override\n# preferred format here.\n#\n# - native: Native newline format. CRLF on Windows, LF on other OSes.\n#\n# - lf: Convert to LF (\"\\n\").\n#\n# - crlf: Convert to CRLF (\"\\r\\n\").\n#\n# Default: native.\nnewline: native\n\n# Sometimes, language extensions are specified in a cabal file or from the\n# command line instead of using language pragmas in the file. stylish-haskell\n# needs to be aware of these, so it can parse the file correctly.\n#\n# No language extensions are enabled by default.\nlanguage_extensions:\n  - TemplateHaskell\n  - QuasiQuotes\n  - CPP\n"
  },
  {
    "path": "BACKERS.md",
    "content": "# Sponsors & Backers\n\nPostgREST ongoing development is only possible thanks to our Sponsors and Backers, listed below. If you'd like to join them, you can do so by supporting the PostgREST organization on [Patreon](https://www.patreon.com/postgrest).\n\n## Sponsors\n\n<table align=\"center\">\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/cybertec.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/supabase.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.euronodes.com/postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/euronodes.svg\">\n        </a>\n      </td>\n    </tr>\n    <tr></tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/neon.jpg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/bytebase.svg\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\n## Lead Backers\n\n- [Roboflow](https://github.com/roboflow)\n- Evans Fernandes\n- [Jan Sommer](https://github.com/nerfpops)\n- [Franz Gusenbauer](https://www.igutech.at/)\n\n## Backers\n\n- Zac Miller\n- Tsingson Qin\n- Michel Pelletier\n- Jay Hannah\n- Robert Stolarz\n- Nicholas DiBiase\n- Christopher Reid\n- Nathan Bouscal\n- Daniel Rafaj\n- David Fenko\n- Remo Rechkemmer\n- Severin Ibarluzea\n- Tom Saleeba\n- Pawel Tyll\n\n## Former Backers\n\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.timescale.com?utm_campaign=postgrest&utm_source=sponsor&utm_medium=referral&utm_content=github\" target=\"_blank\">\n          <img width=\"222px\" src=\"static/timescaledb.png\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img max-width=\"222px\" height=\"88\" src=\"static/retool.png\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo\" target=\"_blank\">\n          <img width=\"222px\" src=\"static/2ndquadrant.png\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"222px\" src=\"static/oblivious.jpg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://code.build/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"222px\" src=\"static/code-build.png\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/tembo.png\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\n- [Christiaan Westerbeek](https://devotis.nl)\n- [Daniel Babiak](https://github.com/dbabiak)\n- Kofi Gumbs\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file. From version `14.0` onwards PostgREST follows a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.\n\n## Unreleased\n\n### Added\n\n- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359\n- Add a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581\n- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599\n- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618\n- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957\n  + Removed unnecessary double count when building the `Content-Range`.\n- Add config `client_error_verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824\n- Add `Vary` header to responses by @develop7 in #4609\n\n### Changed\n\n- All responses now include a `Vary` header by @develop7 in #4609\n\n- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359\n  + Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.\n\n## [14.6] - 2026-03-06\n\n### Fixed\n\n- Fix leaking table and function names when calculating error hint by @taimoorzaeem in #4675\n\n## [14.5] - 2026-02-12\n\n### Fixed\n\n- Don't hide async exceptions in logs by @stevechavez in #4646\n\n## [14.4] - 2026-01-29\n\n### Fixed\n\n- Ensure Listener connections are released by @mkleczek in #4614\n- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707\n- Fix listener running with exception masked after first failure by @mkleczek in #4615\n\n## [14.3] - 2026-01-03\n\n### Fixed\n\n- Fix performance and high memory usage of relation hint calculation by @mkleczek in #4462, #4463\n\n## [14.2] - 2025-12-18\n\n### Fixed\n\n- Fix `hasSingleUnnamedParam` incorrectly matching functions with named parameters by @joelonsql in #4553\n  + Functions with a single named parameter (e.g., `foo(data json)`) no longer incorrectly match the single-param fallback, returning a clean `PGRST202` error instead of a confusing PostgreSQL `42883` error.\n- Fix misleading logs on unsupported PostgreSQL versions by @taimoorzaeem in #4519\n- Fix regression where the `PGRST103` error response was truncated by @laurenceisla in #4455\n  + Happened when an `offset` was greater than the rows requested and `Prefer: count=exact` was sent.\n- Fix not returning `Content-Length` on empty HTTP `201` responses by @laurenceisla in #4518\n- Fix inaccurate Server-Timing header durations by @steve-chavez in #4522\n- Fix inaccurate \"Schema cache queried\" logs by @steve-chavez in #4522\n\n## [14.1] - 2025-11-05\n\n## Fixed\n\n- Fix `db-pre-config` function failing when function names are pg reserved words by @taimoorzaeem in #4380\n- Fix `server-host=!6` incorrectly binds to IPv4 address by @taimoorzaeem in #3202\n\n## [14.0] - 2025-10-24\n\n### Added\n\n- Bounded JWT cache using the SIEVE algorithm by @mkleczek in #4084\n  + It now uses a fixed size cache instead of arbitrary sized cache.\n- Add `--ready` flag for postgrest healthcheck by @taimoorzaeem in #4239\n\n### Fixed\n\n- Fix not logging OpenAPI queries when `log-query=main-query` is enabled by @steve-chavez in #4226\n- Fix not logging explain query when `log-query=main-query` is enabled by @steve-chavez in #4319\n- Fix not logging transaction variables and db-pre-request function when `log-query=main-query` is enabled by @steve-chavez in #3934\n- Fix not logging the JSON message to stderr on a `PGRST002` error by @laurenceisla in #4129\n- Fix reloading the Schema Cache unnecessarily on a `PGRST002` error by @laurenceisla in #4367\n- Fix schema cache loading taking a long time for large schemas by @mkleczek in #4360, #3704\n\n### Changed\n\n- Drop support for PostgreSQL EOL version 12 by @wolfgangwalther in #3865\n- From now on PostgREST will follow a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.\n- Replaced `jwt-cache-max-lifetime` config with `jwt-cache-max-entries` by @mkleczek in #4084\n- `log-query` config now takes a boolean instead of a string value by @steve-chavez in #3934\n\n## [13.0.8] - 2025-10-24\n\n### Fixed\n\n- Fix loading utf-8 config files with `ASCII` locale set by @taimoorzaeem in #4386\n\n## [13.0.7] - 2025-09-14\n\n### Added\n\n- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089\n  + It now shows the invalid schema in the `message` field.\n  + The exposed schemas are now listed in the `hint` instead of the `message` field.\n- Improve error details of `PGRST301` error by @taimoorzaeem in #4051\n\n## [13.0.6] - 2025-08-30\n\n### Fixed\n\n- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588\n- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291\n- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292\n\n## [13.0.5] - 2025-08-24\n\n### Fixed\n\n- Fix OpenAPI broken docs link by @taimoorzaeem in #4080\n- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174\n- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887\n- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271\n\n## [13.0.4] - 2025-06-17\n\n### Fixed\n\n- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135\n- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132\n\n## [13.0.3] - 2025-06-16\n\n- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100\n- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136\n\n### Fixed\n\n## [13.0.2] - 2025-06-02\n\n### Fixed\n\n- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109\n\n## [13.0.1] - 2025-06-01\n\n### Fixed\n\n- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601\n- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074\n  + It can now be disabled by setting it to empty string.\n  + Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.\n\n## [13.0.0] - 2025-05-08\n\n### Added\n\n - #3558, Add the `admin-server-host` config to set the host for the admin server - @develop7\n - #3607, Log to stderr when the JWT secret is less than 32 characters long - @laurenceisla\n - #2858, Performance improvements when calling RPCs via GET using indexes in more cases - @wolfgangwalther\n - #3560, Log resolved host in \"Listening on ...\" messages - @develop7\n - #3727, Log maximum pool size - @steve-chavez\n - #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem\n - #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem\n - #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla\n - #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla\n - #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem\n - #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla\n   + The selected columns in the embedded resources are aggregated into arrays\n   + Aggregates are not supported\n - #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem\n - #4016, Add `Content-Length` response header - @laurenceisla\n\n### Fixed\n\n - #3693, Prevent spread embedding to allow aggregates when they are disabled - @laurenceisla\n - #3693, A nested spread embedding now correctly groups by the fields of its top parent relationship - @laurenceisla\n - #3693, Fix spread embedding errors when using the `count()` aggregate without a field - @laurenceisla\n   + Fixed `\"column reference <col> is ambiguous\"` error when selecting `?select=...table(col,count())`\n   + Fixed `\"column <json_aggregate>.<alias> does not exist\"` error when selecting `?select=...table(aias:count())`\n - #3727, Clarify \"listening\" logs - @steve-chavez\n - #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez\n - #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem\n - #3600, #3926, Improve JWT errors - @taimoorzaeem\n - #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem\n - #3965, Fix filter on unselected columns in a table-valued function - @taimoorzaeem\n - #4052, Fix schema cache load duplicate objects with different object type but same oid - @taimoorzaeem\n\n### Changed\n\n - #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther\n - #2052, Dropped support for PostgreSQL 10 - @wolfgangwalther\n - #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther\n - #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7\n - #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla\n - #3644, Fail schema cache lookup with invalid `db-schemas` or `db-extra-search-path` config - @wolfgangwalther\n   - Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.\n   - Note: This also applies when deleting the `public` schema - both config options default to that.\n - #3757, Remove support for `Prefer: params=single-object` - @joelonsql\n   + This preference was deprecated in favor of Functions with an array of JSON objects\n - #3013, Drop support for Limited updates/deletes\n   + The feature was complicated and largely unused.\n - #3956, Drop `/config` endpoint of admin server - @steve-chavez\n   + The endpoint was at risk of being left unprotected when exposing it.\n   + The accompanying `admin-server-config-enabled` config was also dropped.\n - #3598, PostgREST now validates the `kid` parameter of the JWT - @wolfgangwalther\n   + If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the `jwt-secret`.\n   + If the JWT doesn't contain a `kid`, the behavior should be backwards compatible. PostgREST  will try each key in the `jwt-secret` one by one until it finds one that works.\n - #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem\n - #3600, #3926, Improve JWT errors - @taimoorzaeem\n   + Return `PGRST301` error when `Bearer` in auth header is sent empty\n   + Diagnostic error messages instead of exposed internals\n   + Return new `PGRST303` error when jwt claims decoding fails\n - #3906, Return `PGRST125` and `PGRST126` errors instead of empty json - @taimoorzaeem\n\n## [12.2.12] - 2025-05-01\n\n### Fixed\n\n- #3956, Fix exposing admin server `/config` by default - @steve-chavez\n  + The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`\n\n## [12.2.11] - 2025-04-22\n\n### Fixed\n\n - #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem\n\n## [12.2.10] - 2025-04-18\n\n### Fixed\n\n- #3889, Fix: JWT cache purging on every request decreases performance - @mkleczek\n\n## [12.2.9] - 2025-04-16\n\n### Fixed\n\n - #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem\n - #4014, Fix JWT cache allows old tokens after the jwt-secret is changed in a config reload - @taimoorzaeem\n\n## [12.2.8] - 2025-02-10\n\n### Fixed\n\n - #3841, Log `503` client error to stderr - @taimoorzaeem\n\n## [12.2.7] - 2025-02-03\n\n### Fixed\n\n - #2524, Fix schema reloading notice on windows - @diogob\n\n## [12.2.6] - 2025-01-29\n\n### Fixed\n\n - #3788, Fix jwt cache does not remove expired entries - @taimoorzaeem\n\n## [12.2.5] - 2025-01-20\n\n### Fixed\n\n - #3867, Fix startup for arm64 docker image - @wolfgangwalther\n\n## [12.2.4] - 2025-01-18\n\n### Fixed\n\n - #3779, Always log the schema cache load time - @steve-chavez\n - #3706, Fix insert with `missing=default` uses default value of domain instead of column - @taimoorzaeem\n\n## [12.2.3] - 2024-08-01\n\n### Fixed\n\n - #3091, Broken link in OpenAPI description `externalDocs` - @salim-b\n - #3659, Embed One-to-One relationship with different column order properly - @wolfgangwalther\n - #3504, Remove `format` from `rowFilter` parameters in OpenAPI - @dantheman2865\n - #3660, Fix regression that loaded the schema cache before the in-database configuration - @steve-chavez, @laurenceisla\n\n## [12.2.2] - 2024-07-10\n\n### Fixed\n\n - #3093, Nested empty embeds no longer show empty values and are correctly omitted - @laurenceisla\n - #3644, Make --dump-schema work with in-database pgrst.db_schemas setting - @wolfgangwalther\n - #3644, Show number of timezones in schema cache load report - @wolfgangwalther\n - #3644, List correct enum options in OpenApi output when multiple types with same name are present - @wolfgangwalther\n - #3523, Fix schema cache loading retry without backoff - @steve-chavez\n\n## [12.2.1] - 2024-06-27\n\n### Fixed\n\n - #3147, Don't reload schema cache on every listener failure - @steve-chavez\n\n### Documentation\n\n - #3592, Architecture diagram now supports dark mode and has links - @laurenceisla\n - #3616, The schema isolation diagram now supports dark mode and uses well-known schemas - @laurenceisla\n\n## [12.2.0] - 2024-06-11\n\n### Added\n\n - #2887, Add Preference `max-affected` to limit affected resources - @taimoorzaeem\n - #3171, Add an ability to dump config via admin API - @skywriter\n - #3171, #3046, Log schema cache stats to stderr - @steve-chavez\n - #3210, Dump schema cache through admin API - @taimoorzaeem\n - #2676, Performance improvement on bulk json inserts, around 10% increase on requests per second by removing `json_typeof` from write queries - @steve-chavez\n - #3435, Add log-level=debug, for development purposes - @steve-chavez\n - #1526, Add `/metrics` endpoint on admin server - @steve-chavez\n   - Exposes connection pool metrics, schema cache metrics\n - #3404, Show the failed MESSAGE or DETAIL in the `details` field of the `PGRST121` (could not parse RAISE 'PGRST') error - @laurenceisla\n - #3404, Show extra information in the `PGRST121` (could not parse RAISE 'PGRST') error - @laurenceisla\n   + Shows the failed MESSAGE or DETAIL in the `details` field\n   + Shows the correct JSON format in the `hints` field\n - #3340, Log when the LISTEN channel gets a notification - @steve-chavez\n - #3184, Log full pg version to stderr on connection - @steve-chavez\n - #3242. Add config `db-hoisted-tx-settings` to apply only hoisted function settings - @taimoorzaeem\n - #3214, #3229 Log connection pool events on log-level=debug - @steve-chavez, @laurenceisla\n\n### Fixed\n\n - #3237, Dump media handlers and timezones with --dump-schema - @wolfgangwalther\n - #3323, #3324, Don't hide error on LISTEN channel failure - @steve-chavez\n - #3330, Incorrect admin server `/ready` response on slow schema cache loads - @steve-chavez\n - #3345, Fix in-database configuration values not loading for `pgrst.server_trace_header` and `pgrst.server_cors_allowed_origins` - @laurenceisla\n - #3404, Clarify the `PGRST121` (could not parse RAISE 'PGRST') error message - @laurenceisla\n - #3267, Fix wrong `503 Service Unavailable` on pg error `53400` - @taimoorzaeem\n - #2985, Fix not adding `application_name` on all connection strings - @steve-chavez\n - #3424, Admin `/live` and `/ready` now differentiates a failure as 500 status - @steve-chavez\n    + 503 status is still given when postgREST is in a recovering state\n - #3478, Media Types are parsed case insensitively - @develop7\n - #3533, #3536, Fix listener silently failing on read replica - @steve-chavez\n    + If the LISTEN connection fails, it's retried with exponential backoff\n - #3414, Force listener to connect to read-write instances using `target_session_attrs` - @steve-chavez\n - #3255, Fix incorrect `413 Request Entity Too Large` on pg errors `54*` - @taimoorzaeem\n - #3549, Remove verbosity from error logs starting with \"An error occurred...\" and replacing it with \"Failed to...\" - @laurenceisla\n\n### Deprecated\n\n - Support for PostgreSQL versions 9.6, 10 and 11 is deprecated. From this on version onwards, PostgREST will only support non-end-of-life PostgreSQL versions. See https://www.postgresql.org/support/versioning/.\n - `Prefer: params=single-object` is deprecated. Use [a function with a single unnamed JSON parameter](https://postgrest.org/en/latest/references/api/functions.html#function-single-json) instead. - @steve-chavez\n\n### Documentation\n\n - #3289, Add dark mode. Can be toggled by a button in the bottom right corner. - @laurenceisla\n - #3384, Add architecture diagram and documentation - @steve-chavez\n\n## [12.0.3] - 2024-05-09\n\n### Fixed\n\n - #3149, Misleading \"Starting PostgREST..\" logs on schema cache reloading - @steve-chavez\n - #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez\n - #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther\n - #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther\n - #3361, Clarify PGRST204(column not found) error message - @steve-chavez\n - #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem\n - #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla\n - #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem\n\n## [12.0.2] - 2023-12-20\n\n### Fixed\n\n  - #3124, Fix table's media type handlers not working for all schemas - @steve-chavez\n  - #3126, Fix empty row on media type handler function - @steve-chavez\n\n## [12.0.1] - 2023-12-12\n\n### Fixed\n\n - #3054, Fix not allowing special characters in JSON keys - @laurenceisla\n - #2344, Replace JSON parser error with a clearer generic message - @develop7\n - #3100, Add missing in-database configuration option for `jwt-cache-max-lifetime` - @laurenceisla\n - #3089, The any media type handler now sets `Content-Type: application/octet-stream` by default instead of `Content-Type: application/json` - @steve-chavez\n\n## [12.0.0] - 2023-12-01\n\n### Added\n\n - #1614, Add `db-pool-automatic-recovery` configuration to disable connection retrying - @taimoorzaeem\n - #2492, Allow full response control when raising exceptions - @taimoorzaeem, @laurenceisla\n - #2771, #2983, #3062, #3055 Add `Server-Timing` response header - @taimoorzaeem, @develop7, @laurenceisla\n - #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem\n - #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem\n - #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem\n - #2825, SQL handlers for custom media types - @steve-chavez\n   + Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901\n - #2799, Add timezone in Prefer header - @taimoorzaeem\n - #3001, Add `statement_timeout` set on functions - @taimoorzaeem\n - #3045, Apply superuser settings on impersonated roles if they have PostgreSQL 15 `GRANT SET ON PARAMETER` privilege - @steve-chavez\n - #915, Add support for aggregate functions - @timabdulla\n    + The aggregate functions SUM(), MAX(), MIN(), AVG(), and COUNT() are now supported.\n    + It's disabled by default, you can enable it with `db-aggregates-enabled`.\n - #3057, Log all internal database errors to stderr - @laurenceisla\n\n### Fixed\n\n - #3015, Fix unnecessary count() on RPC returning single - @steve-chavez\n - #1070, Fix HTTP status responses for upserts - @taimoorzaeem\n   + `PUT` returns `201` instead of `200` when rows are inserted\n   + `POST` with `Prefer: resolution=merge-duplicates` returns `200` instead of `201` when no rows are inserted\n - #3019, Transaction-Scoped Settings are now shown clearly in the Postgres logs - @laurenceisla\n   + Shows `set_config('pgrst.setting_name', $1)` instead of `setconfig($1, $2)`\n   + Does not apply to role settings and `app.settings.*`\n - #2420, Fix bogus message when listening on port 0 - @develop7\n - #3067, Fix Acquision Timeout errors logging to stderr when `log-level=crit` - @laurenceisla\n\n### Changed\n\n - Removed [raw-media-types config](https://postgrest.org/en/v11.1/references/configuration.html#raw-media-types) - @steve-chavez\n - Removed `application/octet-stream`, `text/plain`, `text/xml` [builtin support for scalar results](https://postgrest.org/en/v11.1/references/api/resource_representation.html#scalar-function-response-format) - @steve-chavez\n - Removed default `application/openapi+json` media type for [db-root-spec](https://postgrest.org/en/v11.1/references/configuration.html#db-root-spec) - @steve-chavez\n - Removed [db-use-legacy-gucs](https://postgrest.org/en/v11.2/references/configuration.html#db-use-legacy-gucs) - @laurenceisla\n   + All PostgreSQL versions now use GUCs in JSON format for [Headers, Cookies and JWT claims](https://postgrest.org/en/v12/references/transactions.html#request-headers-cookies-and-jwt-claims).\n\n## [11.2.2] - 2023-10-25\n\n### Fixed\n\n - #2824, Fix regression by reverting fix that returned 206 when first position = length in a `Range` header - @laurenceisla, @strengthless\n\n## [11.2.1] - 2023-10-03\n\n### Fixed\n\n - #2899, Fix `application/vnd.pgrst.array` not accepted as a valid mediatype - @taimoorzaeem\n - #2524, Fix schema cache and configuration reloading with `NOTIFY` not working on Windows - @diogob, @laurenceisla\n - #2915, Fix duplicate headers in response - @taimoorzaeem\n - #2824, Fix range request with first position same as length return status 206 - @taimoorzaeem\n - #2939, Fix wrong `Preference-Applied` with `Prefer: tx=commit` when transaction is rollbacked - @steve-chavez\n - #2939, Fix `count=exact` not being included in `Preference-Applied` - @steve-chavez\n - #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\n - #2846, Fix error when requesting `Prefer: count=<type>` and doing null filtering on embedded resources - @laurenceisla\n - #2959, Fix setting `default_transaction_isolation` unnecessarily - @steve-chavez\n - #2929, Fix arrow filtering on RPC returning dynamic TABLE with composite type - @steve-chavez\n - #2963, Fix RPCs not embedding correctly when using overloaded functions for computed relationships - @laurenceisla\n - #2970, Fix regression that rejects URI connection strings with certain unescaped characters in the password - @laurenceisla, @steve-chavez\n\n## [11.2.0] - 2023-08-10\n\n### Added\n\n - #2523, Data representations - @aljungberg\n   + Allows for flexible API output formatting and input parsing on a per-column type basis using regular SQL functions configured in the database\n   + Enables greater flexibility in the form and shape of your APIs, both for output and input, making PostgREST a more versatile general-purpose API server\n   + Examples include base64 encode/decode your binary data (like a `bytea` column containing an image), choose whether to present a timestamp column as seconds since the Unix epoch or as an ISO 8601 string, or represent fixed precision decimals as strings, not doubles, to preserve precision\n   + ...and accept the same in `POST/PUT/PATCH` by configuring the reverse transformation(s)\n   + Other use-cases include custom representation of enums, arrays, nested objects, CSS hex colour strings, gzip compressed fields, metric to imperial conversions, and much more\n   + Works when using the `select` parameter to select only a subset of columns, embedding through complex joins, renaming fields, with views and computed columns\n   + Works when filtering on a formatted column without extra indexes by parsing to the canonical representation\n   + Works for data `RETURNING` operations, such as requesting the full body in a POST/PUT/PATCH with `Prefer: return=representation`\n   + Works for batch updates and inserts\n   + Completely optional, define the functions in the database and they will be used automatically everywhere\n   + Data representations preserve the ability to write to the original column and require no extra storage or complex triggers (compared to using `GENERATED ALWAYS` columns)\n   + Note: data representations require Postgres 10 (Postgres 11 if using `IN` predicates); data representations are not implemented for RPC\n - #2647, Allow to verify the PostgREST version in SQL: `select distinct application_name from pg_stat_activity`. - @laurenceisla\n - #2856, Add the `--version` CLI option that prints the version information - @laurenceisla\n - #1655, Improve `details` field of the singular error response - @taimoorzaeem\n - #740, Add `Preference-Applied` in response for `Prefer: return=representation/headers-only/minimal` - @taimoorzaeem\n - #1601, Add optional `nulls=stripped` parameter for mediatypes `application/vnd.pgrst.array+json` and `application/vnd.pgrst.object+json` - @taimoorzaeem\n\n### Fixed\n\n - #2821, Fix OPTIONS not accepting all available media types - @steve-chavez\n - #2834, Fix compilation on Ubuntu by being compatible with GHC 9.0.2 - @steve-chavez\n - #2840, Fix `Prefer: missing=default` with DOMAIN default values - @steve-chavez\n - #2849, Fix HEAD unnecessarily executing aggregates - @steve-chavez\n - #2594, Fix unused index on jsonb/jsonb arrow filter and order (``/bets?data->>contractId=eq.1`` and ``/bets?order=data->>contractId``) - @steve-chavez\n - #2861, Fix character and bit columns with fixed length not inserting/updating properly - @laurenceisla\n   + Fixes the error \"value too long for type character(1)\" when the char length of the column was bigger than one.\n - #2862, Fix null filtering on embedded resource when using a column name equal to the relation name - @steve-chavez\n - #1586, Fix function parameters of type character and bit not ignoring length - @laurenceisla\n   + Fixes the error \"value too long for type character(1)\" when the char length of the parameter was bigger than one.\n - #2881, Fix error when a function returns `RECORD` or `SET OF RECORD` - @laurenceisla\n - #2896, Fix applying superuser settings for impersonated role - @steve-chavez\n\n### Deprecated\n\n - #2863, Deprecate resource embedding target disambiguation - @steve-chavez\n   + The `/table?select=*,other!fk(*)` must be used to disambiguate\n   + The server aids in choosing the `!fk` by sending a `hint` on the error whenever an ambiguous request happens.\n\n## [11.1.0] - 2023-06-07\n\n### Added\n\n - #2786, Limit idle postgresql connection lifetime - @robx\n   + New option `db-pool-max-idletime` (default 30s).\n   + This is equivalent to the old option `db-pool-timeout` of PostgREST 10.0.0.\n   + A config alias for `db-pool-timeout` is included.\n - #2703, Add pre-config function - @steve-chavez\n    + New config option `db-pre-config`(empty by default)\n    + Allows using the in-database configuration without SUPERUSER\n - #2781, When `db-channel-enabled` is false, start automatic connection recovery on a new request when pool connections are closed with `pg_terminate_backend` - @steve-chavez\n    + Mitigates the lack of LISTEN/NOTIFY for schema cache reloading on read replicas.\n\n### Fixed\n\n - #2791, Fix dropping schema cache reload notifications  - @steve-chavez\n - #2801, Stop retrying connection when \"no password supplied\" - @steve-chavez\n\n## [11.0.1] - 2023-04-27\n\n### Fixed\n\n - #2762, Fixes \"permission denied for schema\" error during schema cache load - @steve-chavez\n - #2756, Fix bad error message on generated columns when using `Prefer: missing=default` - @steve-chavez\n - #1139, Allow a 30 second skew for JWT validation - @steve-chavez\n   + It used to be 1 second, which was too strict\n\n## [11.0.0] - 2023-04-16\n\n### Added\n\n - #1414, Add related orders - @steve-chavez\n   + On a many-to-one or one-to-one relationship, you can order a parent by a child column `/projects?select=*,clients(*)&order=clients(name).desc.nullsfirst`\n - #1233, #1907, #2566, Allow spreading embedded resources - @steve-chavez\n   + On a many-to-one or one-to-one relationship, you can unnest a json object with `/projects?select=*,...clients(client_name:name)`\n   + Allows including the join table columns when resource embedding\n   + Allows disambiguating a recursive m2m embed\n   + Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction\n - #2340, Allow embedding without selecting any column - @steve-chavez\n - #2563, Allow `is.null` or `not.is.null` on an embedded resource - @steve-chavez\n   + Offers a more flexible replacement for `!inner`, e.g. `/projects?select=*,clients(*)&clients=not.is.null`\n   + Allows doing an anti join, e.g. `/projects?select=*,clients(*)&clients=is.null`\n   + Allows using or across related tables conditions\n - #1100, Customizable OpenAPI title - @AnthonyFisi\n - #2506, Add `server-trace-header` for tracing HTTP requests.  - @steve-chavez\n   + When the client sends the request header specified in the config it will be included in the response headers.\n - #2694, Make `db-root-spec` stable. - @steve-chavez\n   + This can be used to override the OpenAPI spec with a custom database function\n - #1567, On bulk inserts, missing values can get the column DEFAULT by using the `Prefer: missing=default` header - @steve-chavez\n - #2501, Allow filtering by`IS DISTINCT FROM` using the `isdistinct` operator, e.g. `/people?alias=isdistinct.foo`\n - #1569, Allow `any/all` modifiers on the `eq,like,ilike,gt,gte,lt,lte,match,imatch` operators, e.g. `/tbl?id=eq(any).{1,2,3}` - @steve-chavez\n   - This converts the input into an array type\n - #2561, Configurable role settings - @steve-chavez\n   - Database roles that are members of the connection role get their settings applied, e.g. doing\n     `ALTER ROLE anon SET statement_timeout TO '5s'` will result in that `statement_timeout` getting applied for that role.\n   - Works when switching roles when a JWT is sent\n   - Settings can be reloaded with `NOTIFY pgrst, 'reload config'`.\n - #2468, Configurable transaction isolation level with `default_transaction_isolation` - @steve-chavez\n   - Can be set per function `create function .. set default_transaction_isolation = 'repeatable read'`\n   - Or per role `alter role .. set default_transaction_isolation = 'serializable'`\n\n### Fixed\n\n - #2651, Add the missing `get` path item for RPCs to the OpenAPI output - @laurenceisla\n - #2648, Fix inaccurate error codes with new ones - @laurenceisla\n   + `PGRST204`: Column is not found\n   + `PGRST003`: Timed out when acquiring connection to db\n - #1652, Fix function call with arguments not inlining - @steve-chavez\n - #2705, Fix bug when using the `Range` header on `PATCH/DELETE` - @laurenceisla\n   + Fix the`\"message\": \"syntax error at or near \\\"RETURNING\\\"\"` error\n   + Fix doing a limited update/delete when an `order` query parameter was present\n - #2742, Fix db settings and pg version queries not getting prepared  - @steve-chavez\n - #2618, Fix `PATCH` requests not recognizing embedded filters and using the top-level resource instead - @steve-chavez\n\n### Changed\n\n - #2705, The `Range` header is now only considered on `GET` requests and is ignored for any other method - @laurenceisla\n   + Other methods should use the `limit/offset` query parameters for sub-ranges\n   + `PUT` requests no longer return an error when this header is present (using `limit/offset` still triggers the error)\n - #2733, Remove bulk RPC call with the `Prefer: params=multiple-objects` header. A function with a JSON array or object parameter should be used instead.\n\n## [10.2.0] - 2023-04-12\n\n### Added\n\n - #2663, Limit maximal postgresql connection lifetime - @robx\n   + New option `db-pool-max-lifetime` (default 30m)\n   + `db-pool-acquisition-timeout` is no longer optional and defaults to 10s\n   + Fixes postgresql resource leak with long-lived connections (#2638)\n\n### Fixed\n\n - #2667, Fix `db-pool-acquisition-timeout` not logging to stderr when the timeout is reached - @steve-chavez\n\n## [10.1.2] - 2023-02-01\n\n### Fixed\n\n - #2565, Fix bad M2M embedding on RPC - @steve-chavez\n - #2575, Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions - @laurenceisla\n - #2582, Move explanation about \"single parameters\" from the `message` to the `details` in the error output - @laurenceisla\n - #2569, Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions - @laurenceisla\n - #1405, Add the required OpenAPI items object when the parameter is an array - @laurenceisla\n - #2592, Add upsert headers for POST requests to the OpenAPI output - @laurenceisla\n - #2623, Fix FK pointing to VIEW instead of TABLE in OpenAPI output - @laurenceisla\n - #2622, Consider any PostgreSQL authentication failure as fatal and exit immediately - @michivi\n - #2620, Fix `NOTIFY pgrst` not reloading the db connections catalog cache - @steve-chavez\n\n## [10.1.1] - 2022-11-08\n\n### Fixed\n\n - #2548, Fix regression when embedding views with partial references to multi column FKs - @wolfgangwalther\n - #2558, Fix regression when requesting limit=0 and `db-max-row` is set - @laurenceisla\n - #2542, Return a clear error without hitting the database when trying to update or insert an unknown column with `?columns` - @aljungberg\n\n## [10.1.0] - 2022-10-28\n\n### Added\n\n - #2348, Add `db-pool-acquisition-timeout` configuration option, time in seconds to wait to acquire a connection. - @robx\n\n### Fixed\n\n - #2261, #2349, #2467, Reduce allocations communication with PostgreSQL, particularly for request bodies. - @robx\n - #2401, #2444, Fix SIGUSR1 to fully flush connections pool. - @robx\n - #2428, Fix opening an empty transaction on failed resource embedding - @steve-chavez\n - #2455, Fix embedding the same table multiple times - @steve-chavez\n - #2518, Fix a regression when embedding views where base tables have a different column order for FK columns - @wolfgangwalther\n - #2458, Fix a regression with the location header when inserting into views with PKs from multiple tables - @wolfgangwalther\n - #2356, Fix a regression in openapi output with mode follow-privileges - @wolfgangwalther\n - #2283, Fix infinite recursion when loading schema cache with self-referencing view - @wolfgangwalther\n - #2343, Return status code 200 for PATCH requests which don't affect any rows - @wolfgangwalther\n - #2481, Treat computed relationships not marked SETOF as M2O/O2O relationship - @wolfgangwalther\n - #2534, Fix embedding a computed relationship with a normal relationship - @steve-chavez\n - #2362, Fix error message when [] is used inside select - @wolfgangwalther\n - #2475, Disallow !inner on computed columns - @wolfgangwalther\n - #2285, Ignore leading and trailing spaces in column names when parsing the query string - @wolfgangwalther\n - #2545, Fix UPSERT with PostgreSQL 15 - @wolfgangwalther\n - #2459, Fix embedding views with multiple references to the same base column - @wolfgangwalther\n\n### Changed\n\n - #2444, Removed `db-pool-timeout` option, because this was removed upstream in hasql-pool. - @robx\n - #2343, PATCH requests that don't affect any rows no longer return 404 - @wolfgangwalther\n - #2537, Stricter parsing of query string. Instead of silently ignoring, the parser now throws on invalid syntax like json paths for embeddings, hints for regular columns, empty casts or fts languages, etc. - @wolfgangwalther\n\n### Deprecated\n\n - #1385, Deprecate bulk-calls when including the `Prefer: params=multiple-objects` in the request. A function with a JSON array or object parameter should be used instead for a better performance.\n\n## [10.0.0] - 2022-08-18\n\n### Added\n\n - #1933, #2109, Add a minimal health check endpoint - @steve-chavez\n   + For enabling this, the `admin-server-port` config must be set explicitly\n   + A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.\n   + A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.\n - #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther\n - #1823, Add the ability to run postgrest without any configuration. - @wolfgangwalther\n   + #1991, Add the ability to run without `db-uri` using libpq's PG environment variables to connect. - @wolfgangwalther\n   + #1769, Add the ability to run without `db-schemas`, defaulting to `db-schemas=public`. - @wolfgangwalther\n   + #1689, Add the ability to run without `db-anon-role` disabling anonymous access. - @wolfgangwalther\n - #1543, Allow access to fields of composite types in select=, order= and filters through JSON operators -> and ->>. - @wolfgangwalther\n - #2075, Allow access to array items in ?select=, ?order= and filters through JSON operators -> and ->>. - @wolfgangwalther\n - #2156, #2211, Allow applying `limit/offset` to UPDATE/DELETE to only affect a subset of rows - @steve-chavez\n   + It requires an explicit `order` on a unique column(s)\n - #1917, Add error codes with the `\"PGRST\"` prefix to the error response body to differentiate PostgREST errors from PostgreSQL errors - @laurenceisla\n - #1917, Normalize the error response body by always having the `detail` and `hint` error fields with a `null` value if they are empty - @laurenceisla\n - #2176, Errors raised with `SQLSTATE` now include the message and the code in the response body - @laurenceisla\n - #2236, Support POSIX regular expression operators for row filtering - @enote-kane\n - #2202, Allow returning XML from RPCs - @fjf2002\n - #2268, Allow returning XML from single-column queries - @fjf2002\n - #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002\n - #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez\n   + Requires postgis >= 3.0\n   + Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.\n   + Resource embedding works and the embedded rows will go into the `properties` key\n   + In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter.\n - #1082, Add security definitions to the OpenAPI output - @laurenceisla\n - #2378, Support http OPTIONS method on RPC and root path - @steve-chavez\n - #2354, Allow getting the EXPLAIN plan of a request by using the `Accept: application/vnd.pgrst.plan` header - @steve-chavez\n   + Only allowed if the `db-plan-enabled` config is set to true\n   + Can generate the plan for different media types using the `for` parameter: `Accept: application/vnd.pgrst.plan; for=\"application/vnd.pgrst.object\"`\n   + Different options for the plan can be used with the `options` parameter: `Accept: application/vnd.pgrst.plan; options=analyze|verbose|settings|buffers|wal`\n   + The plan can be obtained in text or json by using different media type suffixes: `Accept: application/vnd.pgrst.plan+text` and `Accept: application/vnd.pgrst.plan+json`.\n - #2144, Support computed relationships which allow extending and overriding relationships for resource embedding - @steve-chavez, @wolfgangwalther\n - #1984, Detect one-to-one relationships for resource embedding - @steve-chavez\n   + Detected when there's a foreign key with a unique constraint or when a foreign key is also a primary key\n\n### Fixed\n\n - #2058, Return 204 No Content without Content-Type for PUT - @wolfgangwalther\n - #2107, Clarify error for failed schema cache load. - @steve-chavez\n   + From `Database connection lost. Retrying the connection` to `Could not query the database for the schema cache. Retrying.`\n - #1771, Fix silently ignoring filter on a non-existent embedded resource - @steve-chavez\n - #2152, Remove functions, which are uncallable because of unnamend arguments from schema cache and OpenAPI output. - @wolfgangwalther\n - #2145, Fix accessing json array fields with -> and ->> in ?select= and ?order=. - @wolfgangwalther\n - #2155, Ignore `max-rows` on POST, PATCH, PUT and DELETE - @steve-chavez\n - #2254, Fix inferring a foreign key column as a primary key column on views - @steve-chavez\n - #2070, Restrict generated many-to-many relationships - @steve-chavez\n   + Only adds many-to-many relationships when: a table has FKs to two other tables and these FK columns are part of the table's PK columns.\n - #2278, Allow casting to types with underscores and numbers(e.g. `select=oid_array::_int4`) - @steve-chavez\n - #2277, #2238, #1643, Prevent views from breaking one-to-many/many-to-one embeds when using column or FK as target - @steve-chavez\n    + When using a column or FK as target for embedding(`/tbl?select=*,col-or-fk(*)`), only tables are now detected and views are not.\n    + You can still use a column or an inferred FK on a view to embed a table(`/view?select=*,col-or-fk(*)`)\n - #2317, Increase the `db-pool-timeout` to 1 hour to prevent frequent high connection latency - @steve-chavez\n - #2341, The search path now correctly identifies schemas with uppercase and special characters in their names (regression) - @laurenceisla\n - #2364, \"404 Not Found\" on nested routes and \"405 Method Not Allowed\" errors no longer start an empty database transaction - @steve-chavez\n - #2342, Fix inaccurate result count when an inner embed was selected after a normal embed in the query string - @laurenceisla\n - #2376, OPTIONS requests no longer start an empty database transaction - @steve-chavez\n - #2395, Allow using columns with dollar sign($) without double quoting in filters and `select` - @steve-chavez\n - #2410, Fix loop crash error on startup in Postgres 15 beta 3. Log: \"UNION types \\\"char\\\" and text cannot be matched\". - @yevon\n - #2397, Fix race conditions managing database connection helper - @robx\n - #2269, Allow `limit=0` in the request query to return an empty array - @gautam1168, @laurenceisla\n - #2401, Ensure database connections can't outlive SIGUSR1 - @robx\n\n### Changed\n\n - #2001, Return 204 No Content without Content-Type for RPCs returning VOID - @wolfgangwalther\n   + Previously, those RPCs would return \"null\" as a body with Content-Type: application/json.\n - #2156, `limit/offset` now limits the affected rows on UPDATE/DELETE  - @steve-chavez\n   + Previously, `limit/offset` only limited the returned rows but not the actual updated rows\n - #2155, `max-rows` is no longer applied on POST/PATCH/PUT/DELETE returned rows - @steve-chavez\n   + This was misleading because the affected rows were not really affected by `max-rows`, only the returned rows were limited\n - #2070, Restrict generated many-to-many relationships - @steve-chavez\n   + A primary key that contains the foreign key columns is now needed for generating many-to-many relationships.\n - #2277, Views now are not detected when embedding using the column or FK as target (`/view?select=*,column(*)`) - @steve-chavez\n   + This embedding form was easily made ambiguous whenever a new view was added.\n   + You can use computed relationships to keep this embedding form working\n - #2312, Using `Prefer: return=representation` no longer returns a `Location` header - @laurenceisla\n - #1984, For the cases where one to one relationships are detected, json objects will be returned instead of json arrays of length 1\n   + If you wish to override this behavior, you can use computed relationships to return arrays again\n   + You can get the newly detected one-to-one relationships by using the `--dump-schema` option and filtering with [jq](https://github.com/jqlang/jq).\n     ```\n     ./postgrest --dump-schema  \\\n     | jq  '[.dbRelationships | .[] | .[1] | .[] | select(.relCardinality.tag == \"O2O\" and .relFTableIsView == false and .relTableIsView == false) | del(.relFTableIsView,.relTableIsView,.tag,.relIsSelf)]'\n     ```\n\n## [9.0.1] - 2022-06-03\n\n### Fixed\n\n- #2165, Fix json/jsonb columns should not have type in OpenAPI spec - @clrnd\n- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther\n- #2077, Fix `is` not working with upper or mixed case values like `NULL, TrUe, FaLsE` - @steve-chavez\n- #2024, Fix schema cache loading when views with XMLTABLE and DEFAULT are present - @wolfgangwalther\n- #1724, Fix wrong CORS header Authentication -> Authorization - @wolfgangwalther\n- #2120, Fix reading database configuration properly when `=` is present in value - @wolfgangwalther\n- #2135, Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. - @wolfgangwalther\n- #2101, Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. - @wolfgangwalther\n- #2153, Fix --dump-schema running with a wrong PG version. - @wolfgangwalther\n- #2042, Keep working when EMFILE(Too many open files) is reached. - @steve-chavez\n- #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. - @laurenceisla\n    + Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.\n- #2239, Fix misleading disambiguation error where the content of the `relationship` key looks like valid syntax - @laurenceisla\n- #2294, Disable parallel GC for better performance on higher core CPUs - @steve-chavez\n- #1076, Fix using CPU while idle - @steve-chavez\n\n## [9.0.0] - 2021-11-25\n\n### Added\n\n - #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla\n - #1878, Add Retry-After hint header when in recovery mode - @gautam1168\n - #1735, Allow calling function with single unnamed param through RPC POST. - @steve-chavez\n   + Enables calling a function with a single json parameter without using `Prefer: params=single-object`\n   + Enables uploading bytea to a function with `Content-Type: application/octet-stream`\n   + Enables uploading raw text to a function with `Content-Type: text/plain`\n - #1938, Allow escaping inside double quotes with a backslash, e.g. `?col=in.(\"Double\\\"Quote\")`, `?col=in.(\"Back\\\\slash\")` - @steve-chavez\n - #1075, Allow filtering top-level resource based on embedded resources filters. This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12`- @steve-chavez, @Iced-Sun\n - #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski\n   + Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims`\n   + PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)\n - #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez\n - #2031, Improve error message for ambiguous embedding and add a relevant hint that includes unambiguous embedding suggestions - @laurenceisla\n\n### Fixed\n\n - #1871, Fix OpenAPI missing default values for String types and identify Array types as \"array\" instead of \"string\" - @laurenceisla\n - #1930, Fix RPC return type handling for `RETURNS TABLE` with a single column. Regression of #1615. - @wolfgangwalther\n - #1938, Fix using single double quotes(`\"`) and backslashes(`/`) as values on the \"in\" operator - @steve-chavez\n - #1992, Fix schema cache query failing with standard_conforming_strings = off - @wolfgangwalther\n\n### Changed\n\n - #1949, Drop support for embedding hints used with '.'(`select=projects.client_id(*)`), '!' should be used instead(`select=projects!client_id(*)`) - @steve-chavez\n - #1783, Partitions (created using `PARTITION OF`) are no longer included in the schema cache. - @laurenceisla\n - #2038, Dropped support for PostgreSQL 9.5 - @wolfgangwalther\n\n## [8.0.0] - 2021-07-25\n\n### Added\n\n - #1525, Allow http status override through response.status guc - @steve-chavez\n - #1512, Allow schema cache reloading with NOTIFY - @steve-chavez\n - #1119, Allow config file reloading with SIGUSR2 - @steve-chavez\n - #1558, Allow 'Bearer' with and without capitalization as authentication schema - @wolfgangwalther\n - #1470, Allow calling RPC with variadic argument by passing repeated params - @wolfgangwalther\n - #1559, No downtime when reloading the schema cache with SIGUSR1 - @steve-chavez\n - #504, Add `log-level` config option. The admitted levels are: crit, error, warn and info - @steve-chavez\n - #1607, Enable embedding through multiple views recursively - @wolfgangwalther\n - #1598, Allow rollback of the transaction with Prefer tx=rollback - @wolfgangwalther\n - #1633, Enable prepared statements for GET filters. When behind a connection pooler, you can disable preparing with `db-prepared-statements=false`\n   + This increases throughput by around 30% for simple GET queries(no embedding, with filters applied).\n - #1729, #1760, Get configuration parameters from the db and allow reloading config with NOTIFY  - @steve-chavez\n - #1824, Allow OPTIONS to generate certain HTTP methods for a DB view - @laurenceisla\n - #1872, Show timestamps in startup/worker logs - @steve-chavez\n - #1881, Add `openapi-mode` config option that allows ignoring roles privileges when showing the OpenAPI output - @steve-chavez\n - CLI options(for debugging):\n   + #1678, Add --dump-config CLI option that prints loaded config and exits - @wolfgangwalther\n   + #1691, Add --example CLI option to show example config file - @wolfgangwalther\n   + #1697, #1723, Add --dump-schema CLI option for debugging purposes - @monacoremo, @wolfgangwalther\n - #1794, (Experimental) Add `request.spec` GUC for db-root-spec - @steve-chavez\n\n### Fixed\n\n - #1592, Removed single column restriction to allow composite foreign keys in join tables - @goteguru\n - #1530, Fix how the PostgREST version is shown in the help text when the `.git` directory is not available - @monacoremo\n - #1094, Fix expired JWTs starting an empty transaction on the db - @steve-chavez\n - #1162, Fix location header for POST request with select= without PK - @wolfgangwalther\n - #1585, Fix error messages on connection failure for localized postgres on Windows - @wolfgangwalther\n - #1636, Fix `application/octet-stream` appending `charset=utf-8` - @steve-chavez\n - #1469, #1638 Fix overloading of functions with unnamed arguments - @wolfgangwalther\n - #1560, Return 405 Method not Allowed for GET of volatile RPC instead of 500 - @wolfgangwalther\n - #1584, Fix RPC return type handling and embedding for domains with composite base type (#1615) - @wolfgangwalther\n - #1608, #1635, Fix embedding through views that have COALESCE with subselect - @wolfgangwalther\n - #1572, Fix parsing of boolean config values for Docker environment variables, now it accepts double quoted truth values (\"true\", \"false\") and numbers(\"1\", \"0\") - @wolfgangwalther\n - #1624, Fix using `app.settings.xxx` config options in Docker, now they can be used as `PGRST_APP_SETTINGS_xxx` - @wolfgangwalther\n - #1814, Fix panic when attempting to run with unix socket on non-unix host and properly close unix domain socket on exit - @monacoremo\n - #1825, Disregard internal junction(in non-exposed schema) when embedding - @steve-chavez\n - #1846, Fix requests for overloaded functions from html forms to no longer hang (#1848) - @laurenceisla\n - #1858, Add a hint and clarification to the no relationship found error - @laurenceisla\n - #1841, Show comprehensive error when an RPC is not found in a stale schema cache - @laurenceisla\n - #1875, Fix Location headers in headers only representation for null PK inserts on views - @laurenceisla\n\n### Changed\n\n - #1522, #1528, #1535, Docker images are now built from scratch based on a the static PostgREST executable (#1494) and with Nix instead of a `Dockerfile`. This reduces the compressed image size from over 30mb to about 4mb - @monacoremo\n - #1461, Location header for POST request is only included when PK is available on the table - @wolfgangwalther\n - #1560, Volatile RPC called with GET now returns 405 Method not Allowed instead of 500 - @wolfgangwalther\n - #1584, #1849 Functions that declare `returns composite_type` no longer return a single object array by default, only functions with `returns setof composite_type` return an array of objects - @wolfgangwalther\n - #1604, Change the default logging level to `log-level=error`. Only requests with a status greater or equal than 500 will be logged. If you wish to go back to the previous behaviour and log all the requests, use `log-level=info` - @steve-chavez\n   + Because currently there's no buffering for logging, defaulting to the `error` level(minimum logging) increases throughput by around 15% for simple GET queries(no embedding, with filters applied).\n - #1617, Dropped support for PostgreSQL 9.4 - @wolfgangwalther\n - #1679, Renamed config settings with fallback aliases. e.g. `max-rows` is now `db-max-rows`, but `max-rows` is still accepted - @wolfgangwalther\n - #1656, Allow `Prefer=headers-only` on POST requests and change default to `minimal` (#1813) - @laurenceisla\n - #1854, Dropped undocumented support for gzip compression (which was surprisingly slow and easily enabled by mistake). In some use-cases this makes Postgres up to 3x faster - @aljungberg\n - #1872, Send startup/worker logs to stderr to differentiate from access logs on stdout - @steve-chavez\n\n## [7.0.1] - 2020-05-18\n\n### Fixed\n\n- #1473, Fix overloaded computed columns on RPC - @wolfgangwalther\n- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther\n- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez\n- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez\n- #1452, Fix PUT restriction for all columns - @steve-chavez\n\n### Changed\n\n- From this version onwards, the release page will only include a single Linux static executable that can be run on any Linux distribution.\n\n## [7.0.0] - 2020-04-03\n\n### Added\n\n- #1417, `Accept: application/vnd.pgrst.object+json` behavior is now enforced for POST/PATCH/DELETE regardless of `Prefer: return=representation/minimal` - @dwagin\n- #1415, Add support for user defined socket permission via `server-unix-socket-mode` config option - @Dansvidania\n- #1383, Add support for HEAD request - @steve-chavez\n- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez, @LorenzHenk\n- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst\n- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez\n- #1168, Allow access to the `Authorization` header through the `request.header.authorization` GUC - @steve-chavez\n- #1435, Add `request.method` and `request.path` GUCs - @steve-chavez\n- #1088, Allow adding headers to GET/POST/PATCH/PUT/DELETE responses through the `response.headers` GUC - @steve-chavez\n- #1427, Allow overriding provided headers(Location, Content-Type, etc) through the `response.headers` GUC - @steve-chavez\n- #1450, Allow multiple schemas to be exposed in one instance. The schema to use can be selected through the headers `Accept-Profile` for GET/HEAD and `Content-Profile` for POST/PATCH/PUT/DELETE - @steve-chavez, @mahmoudkassem\n\n### Fixed\n\n- #1301, Fix self join resource embedding on PATCH - @herulume, @steve-chavez\n- #1389, Fix many to many resource embedding on RPC/PATCH - @steve-chavez\n- #1355, Allow PATCH/DELETE without `return=minimal` on tables with no select privileges - @steve-chavez\n- #1361, Fix embedding a VIEW when its source foreign key is UNIQUE - @bwbroersma\n\n### Changed\n\n- #1385, bulk RPC call now should be done by specifying a `Prefer: params=multiple-objects` header - @steve-chavez\n- #1401, resource embedding now outputs an error when multiple relationships between two tables are found - @steve-chavez\n- #1423, default Unix Socket file mode from 755 to 660 - @dwagin\n- #1430, Remove embedding with duck typed column names `GET /projects?select=client(*)`- @steve-chavez\n  + You can rename the foreign key to `client` to make this request work in the new version: `alter table projects rename constraint projects_client_id_fkey to client`\n- #1413, Change `server-proxy-uri` config option to `openapi-server-proxy-uri` - @steve-chavez\n\n## [6.0.2] - 2019-08-22\n\n### Fixed\n\n- #1369, Change `raw-media-types` to accept a string of comma separated MIME types - @Dansvidania\n- #1368, Fix long column descriptions being truncated at 63 characters in PostgreSQL 12 - @amedeedaboville\n- #1348, Go back to converting plus \"+\" to space \" \" in querystrings by default - @steve-chavez\n\n### Deprecated\n\n- #1348, Deprecate `.` symbol for disambiguating resource embedding(added in #918). The url-safe '!' should be used instead. We refrained from using `+` as part of our syntax because it conflicts with some http clients and proxies.\n\n## [6.0.1] - 2019-07-30\n\n### Added\n\n- #1349, Add user defined raw output media types via `raw-media-types` config option - @Dansvidania\n- #1243, Add websearch_to_tsquery support - @herulume\n\n### Fixed\n\n- #1336, Error when testing on Chrome/Firefox: text/html requested but a single column was not selected - @Dansvidania\n- #1334, Unable to compile v6.0.0 on windows - @steve-chavez\n\n## [6.0.0] - 2019-06-21\n\n### Added\n\n- #1186, Add support for user defined unix socket via `server-unix-socket` config option - @Dansvidania\n- #690, Add `?columns` query parameter for faster bulk inserts, also ignores unspecified json keys in a payload - @steve-chavez\n- #1239, Add support for resource embedding on materialized views - @vitorbaptista\n- #1264, Add support for bulk RPC call - @steve-chavez\n- #1278, Add db-pool-timeout config option - @qu4tro\n- #1285, Abort on wrong database password - @qu4tro\n- #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez\n- #1308, Accept `text/plain` and `text/html` for raw output - @steve-chavez\n\n\n### Fixed\n\n- #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez\n- #1221, Fix embedding other resources when having a self join - @steve-chavez\n- #1242, Fix embedding a view having a select in a where - @steve-chavez\n- #1238, Fix PostgreSQL to OpenAPI type mappings for numeric and character types - @fpusch\n- #1265, Fix query generated on bulk upsert with an empty array - @qu4tro\n- #1273, Fix RPC ignoring unknown arguments by default - @steve-chavez\n- #1257, Fix incorrect status when a PATCH request doesn't find rows to change - @qu4tro\n\n### Changed\n\n- #1288, Change server-host default of 127.0.0.1 to !4\n\n### Deprecated\n\n- #1288, Deprecate `.` symbol for disambiguating resource embedding(added in #918). '+' should be used instead. Though '+' is url safe, certain clients might need to encode it to '%2B'.\n\n### Removed\n\n- #1288, Removed support for schema reloading with SIGHUP, SIGUSR1 should be used instead - @steve-chavez\n\n## [5.2.0] - 2018-12-12\n\n### Added\n\n- #1205, Add support for parsing JSON Web Key Sets - @russelldavies\n- #1203, Add support for reading db-uri from a separate file - @zhoufeng1989\n- #1200, Add db-extra-search-path config for adding schemas to the search_path, solves issues related to extensions created on the public schema - @steve-chavez\n- #1219, Add ability to quote column names on filters - @steve-chavez\n\n### Fixed\n\n- #1182, Fix embedding on views with composite pks - @steve-chavez\n- #1180, Fix embedding on views with subselects in pg10 - @steve-chavez\n- #1197, Allow CORS for PUT - @bkylerussell\n- #1181, Correctly qualify function argument of custom type in public schema - @steve-chavez\n- #1008, Allow columns that contain spaces in filters - @steve-chavez\n\n## [5.1.0] - 2018-08-31\n\n### Added\n\n- #1099, Add support for getting json/jsonb by array index - @steve-chavez\n- #1145, Add materialized view columns to OpenAPI output - @steve-chavez\n- #709, Allow embedding on views with subselects/CTE - @steve-chavez\n- #1148, OpenAPI: add `required` section for the non-nullable columns - @laughedelic\n- #1158, Add summary to OpenAPI doc for RPC functions - @mdr1384\n\n### Fixed\n\n- #1113, Fix UPSERT failing when having a camel case PK column - @steve-chavez\n- #945, Fix slow start-up time on big schemas - @steve-chavez\n- #1129, Fix view embedding when table is capitalized - @steve-chavez\n- #1149, OpenAPI: Change `GET` response type to array - @laughedelic\n- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384\n- #905, Fix intermittent empty replies - @steve-chavez\n- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez\n- #1141, Fix app.settings resetting on pool timeout - @steve-chavez\n\n### Changed\n\n- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez\n- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez\n- #724, Change server-host default of *4 to 127.0.0.1\n\n### Deprecated\n\n- #724, SIGHUP deprecated, SIGUSR1 should be used instead\n\n## [0.5.0.0] - 2018-05-14\n\n### Added\n\n- The configuration (e.g. `postgrest.conf`) now accepts arbitrary settings that will be passed through as session-local database settings. This can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: `app.settings.jwt_secret = \"$(MYAPP_JWT_SECRET)\"` will take `MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as `current_setting('app.settings.jwt_secret')`. Only `app.settings.*` values in the configuration file are treated in this way. - @canadaduane\n- #256, Add support for bulk UPSERT with POST and single UPSERT with PUT - @steve-chavez\n- #1078, Add ability to specify source column in embed - @steve-chavez\n- #821, Allow embeds alias to be used in filters - @steve-chavez\n- #906, Add jspath configurable `role-claim-key` - @steve-chavez\n- #1061, Add foreign tables to OpenAPI output - @rhyamada\n\n### Fixed\n\n- #828, Fix computed column only working in public schema - @steve-chavez\n- #925, Fix RPC high memory usage by using parametrized query and avoiding json encoding - @steve-chavez\n- #987, Fix embedding with self-reference foreign key - @steve-chavez\n- #1044, Fix view parent embedding when having many views - @steve-chavez\n- #781, Fix accepting misspelled desc/asc ordering modificators - @onporat, @steve-chavez\n\n### Changed\n\n- #828, A `SET SCHEMA <db-schema>` is done on each request, this has the following implications:\n  - Computed columns now only work if they belong to the db-schema\n  - Stored procedures might require a `search_path` to work properly, for further details see https://postgrest.org/en/v5.0/api.html#explicit-qualification\n- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez\n- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez\n- #1098, Removed support for:\n  + curly braces `{}` in embeds, i.e. `/clients?select=*,projects{*}` can no longer be used, from now on parens `()` should be used `/clients?select=*,projects(*)` - @steve-chavez\n  + \"in\" operator without parens, i.e. `/clients?id=in.1,2,3` no longer supported, `/clients?id=in.(1,2,3)` should be used - @steve-chavez\n  + \"@@\", \"@>\" and \"<@\" operators, from now on their mnemonic equivalents should be used \"fts\", \"cs\" and \"cd\" respectively - @steve-chavez\n\n## [0.4.4.0] - 2018-01-08\n\n### Added\n\n- #887, #601, #1007, Allow specifying dictionary and plain/phrase tsquery in full text search - @steve-chavez\n- #328, Allow doing GET on rpc - @steve-chavez\n- #917, Add ability to map RAISE errorcode/message to http status - @steve-chavez\n- #940, Add ability to map GUC to http response headers - @steve-chavez\n- #1022, Include git sha in version report - @begriffs\n- Faster queries using json_agg - @ruslantalpa\n\n### Fixed\n\n- #876, Read secret files as binary, discard final LF if any - @eric-brechemier\n- #968, Treat blank proxy uri as missing - @begriffs\n- #933, OpenAPI externals docs url to current version - @steve-chavez\n- #962, OpenAPI don't err on nonexistent schema - @steve-chavez\n- #954, make OpenAPI rpc output dependent on user privileges - @steve-chavez\n- #955, Support configurable aud claim - @statik\n- #996, Fix embedded column conflicts table name - @grotsev\n- #974, Fix RPC error when function has single OUT param - @steve-chavez\n- #1021, Reduce join size in allColumns for faster program start - @nextstopsun\n- #411, Remove the need for pk in &select for parent embed - @steve-chavez\n- #1016, Fix anonymous requests when configured with jwt-aud - @ruslantalpa\n\n## [0.4.3.0] - 2017-09-06\n\n### Added\n\n- #567, Support more JWT signing algorithms, including RSA - @begriffs\n- #889, Allow more than two conditions in a single and/or - @steve-chavez\n- #883, Binary output support for RPC - @steve-chavez\n- #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui\n- #907, Ability to embed using a specific relation when there are multiple between tables - @ruslantalpa\n- #930, Split table comment on newline to get OpenAPI operation summary and description - @daurnimator\n- #938, Support for range operators - @russelldavies\n\n### Fixed\n\n- #877, Base64 secret read from a file ending with a newline - @eric-brechemier\n- #896, Boolean env var interpolation in config file - @begriffs\n- #885, OpenAPI repetition reduced by using more definitions- @ldesgoui\n- #924, Improve relations initialization time - @9too\n- #927, Treat blank pre-request as missing - @begriffs\n\n### Changed\n\n- #938, Deprecate symbol operators with mnemonic names. - @russelldavies\n\n## [0.4.2.0] - 2017-06-11\n\n### Added\n\n- #742, Add connection retrying on startup and SIGHUP - @steve-chavez\n- #652, Add and/or params for complex boolean logic - @steve-chavez\n- #808, Env var interpolation in config file (helps Docker) - @begriffs\n- #878 - CSV output support for RPC - @begriffs\n\n### Fixed\n\n- #822, Treat blank string JWT secret as no secret - @begriffs\n\n## [0.4.1.0] - 2017-04-25\n\n### Added\n- Allow requesting binary output on GET - @steve-chavez\n- Accept clients requesting `Content-Type: application/json` from / - @feynmanliang\n- #493, Updating with empty JSON object makes zero updates @koulakis\n- Make HTTP headers and cookies available as GUCs #800 - @ruslantalpa\n- #701, Ability to quote values on IN filters - @steve-chavez\n- #641, Allow IN filter to have no values - @steve-chavez\n\n### Fixed\n- #827, Avoid Warp reaper, extend socket timeout to 1 hour - @majorcode\n- #791, malformed nested JSON error - @diogob\n- Resource embedding in views referencing tables in public schema - @fab1an\n- #777, Empty body is allowed when calling a non-parameterized RPC - @koulakis\n- #831, Fix proc resource embedding issue with search_path - @steve-chavez\n- #547, Use read-only transaction for stable/immutable RPC - @begriffs\n\n## [0.4.0.0] - 2017-01-19\n\n### Added\n- Allow test database to be on another host - @dsimunic\n- `Prefer: params=single-object` to treat payload as single json argument in RPC - @dsimunic\n- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs\n- Ability to generate an OpenAPI spec behind a proxy - @hudayou\n- Ability to set addresses to listen on - @hudayou\n- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa\n- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms\n- Implement support for singular representation responses for POST/PATCH requests - @ehamberg\n- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon\n- Custom request validation with `--pre-request` argument - @begriffs\n- Ability to order by jsonb keys - @steve-chavez\n- Ability to specify offset for a deeper level - @ruslantalpa\n- Ability to use binary base64 encoded secrets - @TrevorBasinger\n\n### Fixed\n- Do not apply limit to parent items - @ruslantalpa\n- Fix bug in relation detection when selecting parents two levels up by using the name of the FK - @ruslantalpa\n- Customize content negotiation per route - @begriffs\n- Allow using nulls order without explicit order direction - @steve-chavez\n- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez\n- Prevent database memory consumption by prepared statements caches - @ruslantalpa\n- Use specific columns in the RETURNING section - @ruslantalpa\n- Fix columns alias for RETURNING - @steve-chavez\n\n### Changed\n- Replace `Prefer: plurality=singular` with `Accept: application/vnd.pgrst.object` - @begriffs\n- Standardize arrays in responses for `Prefer: return=representation` - @begriffs\n- Calling unknown RPC gives 404, not 400 - @begriffs\n- Use HTTP 400 for raise\\_exception - @begriffs\n- Remove non-OpenAPI schema description - @begriffs\n- Use comma rather than semicolon to separate Prefer header values - @begriffs\n- Omit total query count by default - @begriffs\n- No more reserved `jwt_claims` return type - @begriffs\n- HTTP 401 rather than 400 for expired JWT - @begriffs\n- Remove default JWT secret - @begriffs\n- Use GUC request.jwt.claim.foo rather than postgrest.claims.foo - @begriffs\n- Use config file rather than command line arguments - @begriffs\n\n## [0.3.2.0] - 2016-06-10\n\n### Added\n- Reload database schema on SIGHUP - @begriffs\n- Support \"-\" in column names - @ruslantalpa\n- Support column/node renaming `alias:column` - @ruslantalpa\n- Accept posts from HTML forms - @begriffs\n- Ability to order embedded entities - @ruslantalpa\n- Ability to paginate using &limit and &offset parameters - @ruslantalpa\n- Ability to apply limits to embedded entities and enforce --max-rows on all levels - @ruslantalpa, @begriffs\n- Add allow response header in OPTIONS - @begriffs\n\n### Fixed\n- Return 401 or 403 for access denied rather than 404 - @begriffs\n- Omit Content-Type header for empty body - @begriffs\n- Prevent role from being changed twice - @begriffs\n- Use read-only transaction for read requests - @ruslantalpa\n- Include entities from the same parent table using two different foreign keys - @ruslantalpa\n- Ensure that Location header in 201 response is URL-encoded - @league\n- Fix garbage collector CPU leak - @ruslantalpa et al.\n- Return deleted items when return=representation header is sent - @ruslantalpa\n- Use table default values for empty object inserts - @begriffs\n\n## [0.3.1.1] - 2016-03-28\n\n### Fixed\n- Preserve unicode values in insert,update,rpc (regression) - @begriffs\n- Prevent duplicate call to stored procs (regression) - @begriffs\n- Allow SQL functions to generate registered JWT claims - @begriffs\n- Terminate gracefully on SIGTERM (for use in Docker) - @recmo\n- Relation detection fix for views that depend on multiple tables - @ruslantalpa\n- Avoid count on plurality=singular and allow multiple Prefer values - @ruslantalpa\n\n## [0.3.1.0] - 2016-02-28\n\n### Fixed\n- Prevent query error from infecting later connection - @begriffs, @ruslantalpa, @nikita-volkov, @jwiegley\n\n### Added\n- Applies range headers to RPC calls - @diogob\n\n## [0.3.0.4] - 2016-02-12\n\n### Fixed\n- Improved usage screen - @begriffs\n- Reject non-POSTs to rpc endpoints - @begriffs\n- Throw an error for OPTIONS on nonexistent tables - @calebmer\n- Remove deadlock on simultaneous contentious updates - @ruslantalpa, @begriffs\n\n## [0.3.0.3] - 2016-01-08\n\n### Fixed\n- Fix bug in many-many relation detection - @ruslantalpa\n- Inconsistent escaping of table names in read queries - @calebmer\n\n## [0.3.0.2] - 2015-12-16\n\n### Fixed\n- Miscalculation of time used for expiring tokens - @calebmer\n- Remove bcrypt dependency to fix Windows build - @begriffs\n- Detect relations event when authenticator does not have rights to intermediate tables - @ruslantalpa\n- Ensure db connections released on sigint - @begriffs\n- Fix #396 include records with missing parents - @ruslantalpa\n- `pgFmtIdent` always quotes #388 - @calebmer\n- Default schema, changed from `\"1\"` to `public` - @calebmer\n- #414 revert to separate count query - @ruslantalpa\n- Fix #399, allow inserting in tables with no select privileges using \"Prefer: representation=minimal\" - @ruslantalpa\n\n### Added\n- Allow order by computed columns - @diogob\n- Set max rows in response with --max-rows - @begriffs\n- Selection by column name (can detect if `_id` is not included) - @calebmer\n\n## [0.3.0.1] - 2015-11-27\n\n### Fixed\n- Filter columns on embedded parent items - @ruslantalpa\n\n## [0.3.0.0] - 2015-11-24\n\n### Fixed\n- Use reasonable amount of memory during bulk inserts - @begriffs\n\n### Added\n- Ensure JWT expires - @calebmer\n- Postgres connection string argument - @calebmer\n- Encode JWT for procs that return type `jwt_claims` - @diogob\n- Full text operators `@>`,`<@` - @ruslantalpa\n- Shaping of the response body (filter columns, embed relations) with &select parameter for POST/PATCH - @ruslantalpa\n- Detect relationships between public views and private tables - @calebmer\n- `Prefer: plurality=singular` for selecting single objects - @calebmer\n\n### Removed\n- API versioning feature - @calebmer\n- `--db-x` command line arguments - @calebmer\n- Secure flag - @calebmer\n- PUT request handling - @ruslantalpa\n\n### Changed\n- Embed foreign keys with {} rather than () - @begriffs\n- Remove version number from binary filename in release - @begriffs\n\n## [0.2.12.1] - 2015-11-12\n\n### Fixed\n- Correct order for -> and ->> in a json path - @ruslantalpa\n- Return empty array instead of 500 when a set returning function returns an empty result set - @diogob\n\n## [0.2.12.0] - 2015-10-25\n\n### Added\n- Embed associations, e.g. `/film?select=*,director(*)` - @ruslantalpa\n- Filter columns, e.g. `?select=col1,col2` - @ruslantalpa\n- Does not execute the count total if header \"Prefer: count=none\" - @diogob\n\n### Fixed\n- Tolerate a missing role in user creation - @calebmer\n- Avoid unnecessary text re-encoding - @ruslantalpa\n\n## [0.2.11.1] - 2015-09-01\n\n### Fixed\n- Accepts `*/*` in Accept header - @diogob\n\n## [0.2.11.0] - 2015-08-28\n### Added\n- Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob\n- Call stored procedures\n- Filter NOT IN values, e.g. `?col=notin.1,2,3` - @rall\n- CSV responses to GET requests with `Accept: text/csv` - @diogob\n- Debian init scripts - @mkhon\n- Allow filters by computed columns - @diogob\n\n### Fixed\n- Reset user role on error\n- Compatible with Stack\n- Add materialized views to results in GET / - @diogob\n- Indicate insertable=true for views that are insertable through triggers - @diogob\n- Builds under GHC 7.10\n- Allow the use of columns named \"count\" in relations queried - @diogob\n\n## [0.2.10.0] - 2015-06-03\n### Added\n- Full text search, eg `/foo?text_vector=@@.bar`\n- Include auth id as well as db role to views (for row-level security)\n\n## [0.2.9.1] - 2015-05-20\n### Fixed\n- Put -Werror behind a cabal flag (for CI) so Hackage accepts package\n\n## [0.2.9.0] - 2015-05-20\n### Added\n- Return range headers in PATCH\n- Return PATCHed resources if header \"Prefer: return=representation\"\n- Allow nested objects and arrays in JSON post for jsonb columns\n- JSON Web Tokens - [Federico Rampazzo](https://github.com/framp)\n- Expose PostgREST as a Haskell package\n\n### Fixed\n- Return 404 if no records updated by PATCH\n\n## [0.2.8.0] - 2015-04-17\n### Added\n- Option to specify nulls first or last, eg `/people?order=age.desc.nullsfirst`\n- Filter nulls, `?col=is.null` and `?col=isnot.null`\n- Filter within jsonb, `?col->a->>b=eq.c`\n- Accept CSV in post body for bulk inserts\n\n### Fixed\n- Allow NULL values in posts\n- Show full command line usage on param errors\n\n## [0.2.7.0] - 2015-03-03\n### Added\n- Server response logging\n- Filter IN values, e.g. `?col=in.1,2,3`\n- Return POSTed resource if header \"Prefer: return=representation\"\n- Allow override of default (v1) schema\n\n## [0.2.6.0] - 2015-02-18\n### Added\n- A changelog\n- Filter by substring match, e.g. `?col=like.*hello*` (or ilike for\n  case insensitivity).\n- Access-Control-Expose-Headers for CORS\n\n### Fixed\n- Make filter position match docs, e.g. `?order=col.asc` rather\n  than `?order=asc.col`.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "\n# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at support@postgrest.org.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\n[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available\nat [https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to PostgREST\n\n## Issues\n\nFor questions on how to use PostgREST, please use\n[GitHub discussions](https://github.com/PostgREST/postgrest/discussions).\n\n### Reporting an Issue\n\n* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)\n  and also against the latest [devel release](https://github.com/PostgREST/postgrest/releases/tag/devel).\n  It is possible we already fixed the bug you're experiencing.\n\n* Provide steps to reproduce the issue, including your OS version and\n  the specific database schema that you are using.\n\n* Please include SQL logs for issues involving runtime problems. To obtain logs first\n  [enable logging all statements](http://www.microhowto.info/howto/log_all_queries_to_a_postgresql_server.html),\n  then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).\n\n* If your database schema has changed while the PostgREST server is running,\n  [send the server a `SIGUSR1` signal](http://postgrest.org/en/latest/admin.html#schema-reloading) or restart it to ensure the schema cache\n  is not stale. This sometimes fixes apparent bugs.\n\n## Code\n\nWe have a fully nix-based development environment with many tools for a smooth development workflow available.\nCheck the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.\n\n* All contributions must pass the tests before being merged. When\n  you create a pull request your code will automatically be tested.\n\n* All fixes or features must have a test proving the improvement.\n\n* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](https://github.com/jaspervdj/stylish-haskell)\n  with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every\n  pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but\n  we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.\n\n### Running Tests\n\nFor instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).\n\n### Structuring commits in pull requests\n\nTo simplify reviews, make it easy to split pull requests if deemed necessary, and to maintain clean and meaningful history of changes, you will be asked to update your PR if it does not follow the below rules:\n\n* It must be possible to merge the PR branch into target using `git merge --ff-only`, ie. the source branch must be rebased on top of target.\n* No merge commits in the source branch.\n* All commits in the source branch must be self contained, meaning: it should be possible to treat each commit as a separate PR.\n* Commits in the source branch must contain only related changes (related means the changes target a single problem/goal). For example, any refactorings should be isolated from the actual change implementation into separate commits.\n* Tests, documentation, and changelog updates should be contained in the same commits as the actual code changes they relate to. An exception to this rule is when test or documentation changes are made in separate PR.\n* Commit messages must be prefixed with one of the prefixes defined in [the list used by commit verification scripts](https://github.com/PostgREST/postgrest/blob/main/nix/tools/gitTools.nix#L11).\n* Commit messages should contain a longer description of the purpose of the changes contained in the commit and, for non-trivial changes, a description of the changes themselves.\n\n## AI Policy\n\nWe adhere to [Gentoo's AI policy](https://wiki.gentoo.org/wiki/Project:Council/AI_policy):\n\n> It is expressly forbidden to contribute [...] any content that has been created with the assistance of Natural Language Processing artificial intelligence tools. This motion can be revisited, should a case been made over such a tool that does not pose copyright, ethical and quality concerns.\n\nYou can find more about its rationale [here](https://wiki.gentoo.org/wiki/Project:Council/AI_policy#Rationale).\n"
  },
  {
    "path": "Dockerfile",
    "content": "# PostgREST Docker Hub image for aarch64.\n# The x86-64 is a single-static-binary image built via Nix, see:\n# nix/tools/docker/README.md\n\nFROM ubuntu:noble@sha256:186072bba1b2f436cbb91ef2567abca677337cfc786c86e107d25b7072feef0c AS postgrest\n\nRUN apt-get update -y \\\n    && apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\nCOPY postgrest /usr/bin/postgrest\nRUN chmod +x /usr/bin/postgrest\n\nEXPOSE 3000\n\nUSER 1000\n\n# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly. \n# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop \nCMD [\"postgrest\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014-2026 The PostgREST contributors\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![Logo](static/postgrest.png \"Logo\")\n\n[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)\n[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)\n[![Docker Stars](https://img.shields.io/docker/pulls/postgrest/postgrest.svg)](https://hub.docker.com/r/postgrest/postgrest/)\n[![Build Status](https://github.com/postgrest/postgrest/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)\n[![Coverage Status](https://img.shields.io/codecov/c/github/postgrest/postgrest/main)](https://app.codecov.io/gh/PostgREST/postgrest)\n[![Hackage docs](https://img.shields.io/hackage/v/postgrest.svg?label=hackage)](http://hackage.haskell.org/package/postgrest)\n\nPostgREST serves a fully RESTful API from any existing PostgreSQL\ndatabase. It provides a cleaner, more standards-compliant, faster\nAPI than you are likely to write from scratch.\n\n## Sponsors\n\n<table align=\"center\">\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/cybertec.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/supabase.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.euronodes.com/postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/euronodes.svg\">\n        </a>\n      </td>\n    </tr>\n    <tr></tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/neon.jpg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"static/bytebase.svg\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\nBig thanks to our sponsors! You can join them by supporting PostgREST on [Patreon](https://www.patreon.com/postgrest).\n\n## Usage\n\n1. See the docs for [how to install PostgREST on your platform](https://docs.postgrest.org/en/stable/explanations/install.html). You can also [use Docker](https://docs.postgrest.org/en/stable/explanations/install.html#docker).\n\n2. Invoke for help:\n\n    ```bash\n    postgrest --help\n    ```\n## [Documentation](http://postgrest.org)\n\nLatest documentation is at [postgrest.org](http://postgrest.org). You can contribute to the docs in [PostgREST/postgrest/docs](https://github.com/PostgREST/postgrest/tree/main/docs).\n\n## Performance\n\nTLDR; subsecond response times for up to 2000 requests/sec on Heroku\nfree tier. If you're used to servers written in interpreted languages,\nprepare to be pleasantly surprised by PostgREST performance.\n\nThree factors contribute to the speed. First the server is written\nin [Haskell](https://www.haskell.org/) using the\n[Warp](http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks)\nHTTP server (aka a compiled language with lightweight threads).\nNext it delegates as much calculation as possible to the database\nincluding\n\n* Serializing JSON responses directly in SQL\n* Data validation\n* Authorization\n* Combined row counting and retrieval\n* Data post in single command (`returning *`)\n\nFinally it uses the database efficiently with the\n[Hasql](https://nikita-volkov.github.io/hasql-benchmarks/) library\nby\n\n* Keeping a pool of db connections\n* Using the PostgreSQL binary protocol\n* Being stateless to allow horizontal scaling\n\n## Security\n\nPostgREST [handles\nauthentication](http://postgrest.org/en/stable/auth.html) (via JSON Web\nTokens) and delegates authorization to the role information defined in\nthe database. This ensures there is a single declarative source of truth\nfor security.  When dealing with the database the server assumes the\nidentity of the currently authenticated user, and for the duration of\nthe connection cannot do anything the user themselves couldn't. Other\nforms of authentication can be built on top of the JWT primitive. See\nthe docs for more information.\n\n## Versioning\n\nA robust long-lived API needs the freedom to exist in multiple\nversions. PostgREST does versioning through database schemas. This\nallows you to expose tables and views without making the app brittle.\nUnderlying tables can be superseded and hidden behind public facing\nviews.\n\n## Self-documentation\n\nPostgREST uses the [OpenAPI](https://openapis.org/) standard to\ngenerate up-to-date documentation for APIs. You can use a tool like\n[Swagger-UI](https://github.com/swagger-api/swagger-ui) to render\ninteractive documentation for demo requests against the live API server.\n\nThis project uses HTTP to communicate other metadata as well.  For\ninstance the number of rows returned by an endpoint is reported by -\nand limited with - range headers. More about\n[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).\n\n## Data Integrity\n\nRather than relying on an Object Relational Mapper and custom\nimperative coding, this system requires you to put declarative constraints\ndirectly into your database. Hence no application can corrupt your\ndata (including your API server).\n\nThe PostgREST exposes HTTP interface with safeguards to prevent\nsurprises, such as enforcing idempotent PUT requests.\n\nSee examples of [PostgreSQL\nconstraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)\nand the [API guide](http://postgrest.org/en/stable/api.html).\n\n## Supporting development\n\nYou can help PostgREST ongoing maintenance and development by making a regular donation through Patreon https://www.patreon.com/postgrest\n\nEvery donation will be spent on making PostgREST better for the whole community.\n\n## Contributing\n\nContributions are always welcome and appreciated. Please see the [Contributing guidelines](https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md).\n\n## Thanks\n\nThe PostgREST organization is grateful to:\n\n- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/main/BACKERS.md) who support PostgREST's development.\n- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code\n  and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md).\n\nThe cool logo came from [Mikey Casalaina](https://github.com/casalaina).\n"
  },
  {
    "path": "Setup.hs",
    "content": "-- This file is required by Hackage.\nimport Distribution.Simple\nmain = defaultMain\n"
  },
  {
    "path": "cabal.project",
    "content": "packages: postgrest.cabal\ntests: true\n"
  },
  {
    "path": "cabal.project.freeze",
    "content": "index-state: hackage.haskell.org 2025-10-29T04:02:18Z\n"
  },
  {
    "path": "default.nix",
    "content": "{ system ? builtins.currentSystem\n\n, compiler ? \"ghc948\"\n\n, # Commit of the Nixpkgs repository that we want to use.\n  # It defaults to reading the inputs from flake.lock, which serves\n  # as a compatibility layer for non-flake builds / default.nix / shell.nix.\n  nixpkgsVersion ? let\n    lock = builtins.fromJSON (builtins.readFile ./flake.lock);\n  in\n  {\n    inherit (lock.nodes.nixpkgs.locked) owner repo rev;\n    tarballHash = lock.nodes.nixpkgs.locked.narHash;\n  }\n\n, # Nix files that describe the Nixpkgs repository. We evaluate the expression\n  # using `import` below.\n  nixpkgs ? let inherit (nixpkgsVersion) owner repo rev tarballHash; in\n  builtins.fetchTarball {\n    url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\";\n    sha256 = tarballHash;\n  }\n}:\n\nlet\n  name =\n    \"postgrest\";\n\n  # PostgREST source files, filtered based on the rules in the .gitignore files\n  # and file extensions. We want to include as little as possible, as the files\n  # added here will increase the space used in the Nix store and trigger the\n  # build of new Nix derivations when changed.\n  src =\n    pkgs.lib.sourceFilesBySuffices\n      (pkgs.gitignoreSource ./.)\n      [ \".cabal\" \".hs\" \".lhs\" \"LICENSE\" ];\n\n  allOverlays =\n    import nix/overlays;\n\n  overlays =\n    [\n      allOverlays.build-toolbox\n      allOverlays.checked-shell-script\n      allOverlays.gitignore\n      (allOverlays.haskell-packages { inherit compiler; })\n    ];\n\n  # Evaluated expression of the Nixpkgs repository.\n  pkgs =\n    import nixpkgs { inherit overlays system; };\n\n  postgresqlVersions =\n    [\n      { name = \"pg-17\"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }\n      { name = \"pg-16\"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }\n      { name = \"pg-15\"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }\n      { name = \"pg-14\"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }\n      { name = \"pg-13\"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }\n    ];\n\n  haskellPackages = pkgs.haskell.packages.\"${compiler}\";\n\n  # Dynamic derivation for PostgREST\n  postgrest = pkgs.lib.pipe (haskellPackages.callCabal2nix name src { }) [\n    # To allow ghc-datasize to be used.\n    lib.disableLibraryProfiling\n    # We are never going to use dynamic haskell libraries anyway. \"Dynamic\" refers to how\n    # non-haskell deps are linked. All haskell dependencies are always statically linked.\n    lib.disableSharedLibraries\n  ];\n\n  staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; };\n\n  # Options passed to cabal in dev tools and tests\n  devCabalOptions =\n    \"-f dev --test-show-detail=direct\";\n\n  inherit (pkgs.haskell) lib;\nin\nrec {\n  inherit nixpkgs pkgs;\n\n  # Derivation for the PostgREST Haskell package, including the executable,\n  # libraries and documentation. We disable running the test suite on Nix\n  # builds, as they require a database to be set up. We split the binary\n  # into a separate output, so that the default distribution via flake.nix\n  # has a much smaller closure size.\n  postgrestPackage = pkgs.lib.pipe postgrest [\n    lib.dontCheck\n    lib.enableSeparateBinOutput\n    (haskellPackages.generateOptparseApplicativeCompletions [ \"postgrest\" ])\n  ];\n\n  # Profiled dynamic executable.\n  postgrestProfiled = pkgs.lib.pipe postgrestPackage [\n    lib.enableExecutableProfiling\n    lib.enableLibraryProfiling\n    lib.dontHaddock\n  ];\n\n  inherit (postgrest) env;\n\n  # Tooling for analyzing Haskell imports and exports.\n  hsie =\n    pkgs.callPackage nix/hsie {\n      inherit (pkgs.haskell.packages.\"${compiler}\") ghcWithPackages;\n    };\n\n  ### Tools\n\n  cabalTools =\n    pkgs.callPackage nix/tools/cabalTools.nix { inherit devCabalOptions postgrest; };\n\n  withTools =\n    pkgs.callPackage nix/tools/withTools.nix { inherit postgresqlVersions postgrest; };\n\n  # Development tools.\n  devTools =\n    pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };\n\n  # Documentation tools.\n  docs =\n    pkgs.callPackage nix/tools/docs.nix { };\n\n  # Git tools.\n  gitTools =\n    pkgs.callPackage nix/tools/gitTools.nix { };\n\n  # Load testing tools.\n  loadtest =\n    pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };\n\n  # Utility for updating the pinned version of Nixpkgs.\n  nixpkgsTools =\n    pkgs.callPackage nix/tools/nixpkgsTools.nix { };\n\n  # Scripts for publishing new releases.\n  release =\n    pkgs.callPackage nix/tools/release.nix { };\n\n  # Linting and styling tools.\n  style =\n    pkgs.callPackage nix/tools/style.nix { inherit hsie; };\n\n  # Scripts for running tests.\n  tests =\n    pkgs.callPackage nix/tools/tests.nix {\n      inherit postgrest devCabalOptions withTools;\n      ghc = pkgs.haskell.compiler.\"${compiler}\";\n      inherit (pkgs.haskell.packages.\"${compiler}\") hpc-codecov;\n      inherit (pkgs.haskell.packages.\"${compiler}\") weeder;\n    };\n} // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux rec {\n  # Static executable.\n  inherit (staticHaskellPackage) postgrestStatic;\n  inherit (staticHaskellPackage) packagesStatic;\n\n  # Docker images and loading script.\n  docker =\n    pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };\n}\n"
  },
  {
    "path": "docker-hub-readme.md",
    "content": "# PostgREST\n\n[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)\n[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)\n[![Build Status](https://github.com/postgrest/postgrest/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)\n\nPostgREST serves a fully RESTful API from any existing PostgreSQL database. It\nprovides a cleaner, more standards-compliant, faster API than you are likely to\nwrite from scratch.\n\n## Sponsors\n\n<table align=\"center\">\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage\" target=\"_blank\">\n          <img width=\"296px\" src=\"https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.euronodes.com/postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg\">\n        </a>\n      </td>\n    </tr>\n    <tr></tr>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg\">\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a href=\"https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest\" target=\"_blank\">\n          <img width=\"296px\" src=\"https://raw.githubusercontent.com/PostgREST/postgrest/main/static/bytebase.svg\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\n# Usage\n\nTo learn how to use this container, see the [PostgREST Docker\ndocumentation](https://postgrest.org/en/stable/install.html#docker).\n\nYou can configure the PostgREST image by setting\n[environment variables](https://postgrest.org/en/stable/configuration.html).\n\n# How this image is built\n\nThe image is built from scratch using\n[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a\n`Dockerfile`, which yields a highly secure and optimized image. This is also why\nno commands are listed in the image history. See the [PostgREST\nrepository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for\ndetails on the build process and how to inspect the image.\n\nThis does not apply to the arm64 variant, which is based on Ubuntu.\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "_build\nPipfile.lock\n*.aux\n*.log\n_diagrams/db.pdf\nmisspellings\nunuseddict\n*.mo\n"
  },
  {
    "path": "docs/README.md",
    "content": "# PostgREST documentation https://postgrest.org/\n\nPostgREST docs use the reStructuredText format, check this [cheatsheet](https://github.com/ralsina/rst-cheatsheet/blob/master/rst-cheatsheet.rst) to get acquainted with it.\n\nTo build the docs locally, see [the Nix development readme](/nix/README.md#documentation).\n\n## Documentation structure\n\nThis documentation is structured according to tutorials-howtos-topics-references. For more details on the rationale of this structure, \nsee https://www.divio.com/blog/documentation.\n\n## Translating\n\nTo create `.po` files for translation into a new language pass the language code as the first argument to `postgrest-docs-build`.\n\nExample to add German/de:\n\n```\npostgrest-docs-build de\n```\n\nThe livereload server also supports a language/locale argument to show the translated docs during translation:\n\n```\npostgrest-docs-serve de\n```\n\nSpellcheck is currently only available for the default language.\n"
  },
  {
    "path": "docs/_diagrams/README.md",
    "content": "## ERD\n\nThe ER diagrams were created with https://github.com/BurntSushi/erd/.\n\nYou can go download erd from https://github.com/BurntSushi/erd/releases and then do:\n\n```bash\n./erd_static-x86-64 -i ./er/film.er -o ../_static/film.png\n```\n\nThe fonts used belong to the GNU FreeFont family. You can download them here: http://ftp.gnu.org/gnu/freefont/\n\n## UML\n\nThe UML diagrams are created with https://plantuml.com/.\n\nPlantUML only creates one diagram per file.\nThat's why we need to create another one for dark mode.\nFor example, for the file [uml/arch.uml](uml/arch.uml) there's [uml/dark/arch-dark.uml](uml/dark/arch-dark.uml) which includes the first one:\n\n```bash\nplantuml -tsvg uml/arch.uml -o ../../_static\nplantuml -tsvg -darkmode uml/dark/arch-dark.uml -o ../../../_static\n```\n"
  },
  {
    "path": "docs/_diagrams/er/boxoffice.er",
    "content": "entity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Box_Office]\n*bo_date\n*+film_id\ngross_revenue\n\n[Films]\n*id\n+director_id\ntitle\n`...`\n\nBox_Office +--1 Films\n"
  },
  {
    "path": "docs/_diagrams/er/employees.er",
    "content": "# Build using: -e ortho\n\nentity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Employees]\n*id\nfirst_name\nlast_name\n+supervisor_id\n\nEmployees 1--* Employees\n"
  },
  {
    "path": "docs/_diagrams/er/film.er",
    "content": "entity {font: \"FreeSans\"}\nrelationship {font: \"FreeSerif\"}\n\n[Films]\n*id\n+director_id\ntitle\nyear\nrating\nlanguage\n\n[Directors]\n*id\nfirst_name\nlast_name\n\n[Actors]\n*id\nfirst_name\nlast_name\n\n[Roles]\n*+film_id\n*+actor_id\ncharacter\n\n[Competitions]\n*id\nname\nyear\n\n[Nominations]\n*+competition_id\n*+film_id\nrank\n\n[Technical_Specs]\n*+film_id\nruntime\ncamera\nsound\n\nRoles *--1 Actors\nRoles *--1 Films\n\nNominations *--1 Competitions\nNominations *--1 Films\n\nFilms *--1 Directors\n\nFilms 1--1 Technical_Specs\n"
  },
  {
    "path": "docs/_diagrams/er/orders.er",
    "content": "# Build using: -e ortho\n\nentity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Addresses]\n*id\nname\ncity\nstate\npostal_code\n\n[Orders]\n*id\nname\n+billing_address_id\n+shipping_address_id\n\nOrders *--1 Addresses\nOrders *--1 Addresses\n"
  },
  {
    "path": "docs/_diagrams/er/premieres.er",
    "content": "entity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Premieres]\n*id\nlocation\ndate\n+film_id\n\n[Films]\n*id\n+director_id\ntitle\n`...`\n\nPremieres *--1 Films\n"
  },
  {
    "path": "docs/_diagrams/er/presidents.er",
    "content": "# Build using: -e ortho\n\nentity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Presidents]\n*id\nfirst_name\nlast_name\n+predecessor_id\n\nPresidents 1--? Presidents\n"
  },
  {
    "path": "docs/_diagrams/er/users.er",
    "content": "# Build using: -e ortho\n\nentity {font: \"FreeSans\"}\nrelationship {font: \"FreeMono\"}\n\n[Users]\n*id\nfirst_name\nlast_name\nusername\n\n[Subscriptions]\n*+subscriber_id\n*+subscribed_id\ntype\n\nUsers 1--* Subscriptions\nSubscriptions *--1 Users\n"
  },
  {
    "path": "docs/_diagrams/uml/arch.uml",
    "content": "@startuml\n\nskinparam backgroundColor transparent\n\npackage \"PostgREST\" {\n  () HTTP as HTTPAPI\n  HTTPAPI  - [Auth]\n  [Auth] -r.> [ApiRequest]\n  [ApiRequest] -r.> [Plan]\n  [Plan] -r.> [Query]\n  [Query] - () \"Connection Pool\" : \"\\t\"\n  [Plan] -u-> [Schema Cache]:uses\n  [Schema Cache] <- () Listener : reloads\n\n  () HTTP as HTTPADMIN\n  [Admin]  -r- () HTTPADMIN\n  [Config] -l- () CLI\n\n  [Config] <-r~ Listener\n\n  HTTPADMIN -[hidden]r- CLI\n  [Schema Cache] -l[hidden]- [Config]\n  [Schema Cache] -l[hidden]- [Admin]\n  [Schema Cache] -l[hidden]- CLI\n}\n\n\ndatabase \"PostgreSQL\" {\n  node Authorization {\n    rectangle \"Roles, GRANT, RLS\"\n  }\n  node \"API schema\" as API {\n    rectangle \"Functions, Views\"\n  }\n  rectangle \"Tables, extensions\" as tbs\n  API -d- tbs\n\n  API -l[hidden]- Authorization\n}\n\n:user:\nhexagon Proxy\n:user: .r-> Proxy : request with JWT\nHTTPAPI <.l- Proxy\n\nhexagon ExternalAuth\nExternalAuth -u[hidden]- Proxy\n:user: .r-> ExternalAuth : login\n:user: <.r- ExternalAuth : JWT\n\n:operator: .d-> HTTPADMIN\n:operator: .d-> CLI\n\n\nPostgreSQL <.developer : \"\\t\"\nListener -r.> \"PostgreSQL\"\n\"Connection Pool\" -r.> \"PostgreSQL\" : \"\\t\\t\"\n\nnote bottom of Auth\n  Validates the JWT\nend note\n\nnote bottom of ApiRequest\n  Parses the URL syntax\nend note\n\nnote bottom of Plan\n  Generates internal AST\nend note\n\nnote bottom of Query\n  Generates the SQL\nend note\n\nnote top of Listener\n  LISTEN session\nend note\n\nurl of ExternalAuth is [[../explanations/external_auth.html]]\nurl of Admin is [[../references/admin_server.html#admin-server]]\nurl of API is [[../explanations/schema_isolation.html]]\nurl of Auth is [[../references/auth.html#authn]]\nurl of ApiRequest is [[../explanations/architecture.html#api-request]]\nurl of Plan is [[../explanations/architecture.html#plan]]\nurl of Query is [[../explanations/architecture.html#query]]\nurl of Authorization is [[../explanations/db_authz.html]]\nurl of CLI is [[../references/cli.html#cli]]\nurl of \"Connection Pool\" is [[../references/connection_pool.html]]\nurl of Config is [[../references/configuration.html#configuration]]\nurl of HTTPADMIN is [[../explanations/architecture.html#http]]\nurl of HTTPAPI is [[../explanations/architecture.html#http]]\nurl of Listener is [[../references/listener.html#listener]]\nurl of Proxy is [[../explanations/nginx.html]]\nurl of \"Schema Cache\" is [[../references/schema_cache.html#schema-cache]]\n\n@enduml\n"
  },
  {
    "path": "docs/_diagrams/uml/dark/arch-dark.uml",
    "content": "@startuml\n!include ../arch.uml\n@enduml\n"
  },
  {
    "path": "docs/_diagrams/uml/dark/sch-iso-dark.uml",
    "content": "@startuml\n!include ../sch-iso.uml\n@enduml\n"
  },
  {
    "path": "docs/_diagrams/uml/sch-iso.uml",
    "content": "@startuml\n\nskinparam backgroundColor transparent\nskinparam linetype ortho\n\nskinparam node {\n  backgroundColor transparent\n  borderThickness 1\n}\n\ndatabase \"PostgreSQL\" {\n  node public {\n    rectangle tables_public as \"tables\"\n  }\n\n  node extensions as \"**extensions**\" {\n  }\n\n  node API as \"<size:20>api\" {\n    rectangle vf_api as \"views + functions\"\n  }\n\n  tables_public <-- vf_api\n  extensions <-- vf_api\n}\n\nvf_api <-[thickness=3]-> () PostgREST\n\n@enduml\n"
  },
  {
    "path": "docs/_static/css/custom.css",
    "content": ".wy-nav-content {\n  max-width: initial;\n}\n\n#postgrest-documentation > h1 {\n  display: none;\n}\n\ndiv.wy-menu.rst-pro {\n  display: none !important;\n}\n\ndiv.highlight {\n  background: #fff !important;\n}\n\ndiv.line-block {\n  margin-bottom: 0px !important;\n}\n\n#sponsors {\n  text-align: center;\n}\n\n#sponsors h2 {\n  text-align: left;\n}\n\n#sponsors img{\n  margin: 10px;\n  width: 13em; /* \".. image::\" does not apply width properly to SVGs */\n}\n\n#thanks{\n  text-align: center;\n}\n\n#thanks img{\n  margin: 10px;\n}\n\n#thanks h2{\n  text-align: left;\n}\n\n#thanks p{\n  text-align: left;\n}\n\n#thanks ul{\n  text-align: left;\n}\n\n.image-container {\n  max-width: 800px;\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n  margin-bottom: 24px;\n}\n\n.wy-table-responsive table td {\n    white-space: normal !important;\n}\n\n.wy-table-responsive {\n    overflow: visible !important;\n}\n\n#tutorials span.caption-text {\n    display: none;\n}\n\n#references span.caption-text {\n    display: none;\n}\n\n#explanations span.caption-text {\n    display: none;\n}\n\n#how-tos span.caption-text {\n    display: none;\n}\n\n#ecosystem span.caption-text {\n    display: none;\n}\n\n#integrations span.caption-text {\n    display: none;\n}\n\n#api span.caption-text {\n    display: none;\n}\n\n/* Tweaks for dark mode from extension: sphinx-rtd-dark-theme */\n\nhtml[data-theme=\"dark\"] .highlight {\n  background-color: #17181c !important;\n}\n\nhtml[data-theme=\"dark\"] .sphinx-tabs-tab {\n  color: var(--dark-link-color);\n}\n\nhtml[data-theme=\"dark\"] .sphinx-tabs-panel {\n  border: 1px solid #404040;\n  border-top: 0;\n  background: #141414;\n}\n\nhtml[data-theme=\"dark\"] .sphinx-tabs-tab[aria-selected=\"true\"] {\n  border: 1px solid #404040;\n  border-bottom: 1px solid #141414;\n  background-color: #141414;\n}\n\nhtml[data-theme=\"dark\"] [role=\"tablist\"] {\n  border-bottom: 1px solid #404040;\n}\n\nhtml[data-theme=\"dark\"] .btn-neutral {\n  color: white !important;\n}\n\nhtml[data-theme=\"dark\"] .img-dark {\n  display: inline;\n}\n\nhtml:not([data-theme=\"dark\"]) .img-dark {\n  display: none;\n}\n\nhtml[data-theme=\"dark\"] .img-light {\n  display: none;\n}\n\nhtml:not([data-theme=\"dark\"]) .img-light {\n  display: inline;\n}\n\nhtml[data-theme=\"dark\"] .img-translucent img {\n  background-color: #cccccc;\n}\n\n.img-translucent img {\n  transition: background-color 0.3s;\n  margin-bottom: 24px;\n}\n\n.svg-container-md {\n  max-width: 400px;\n}\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# PostgREST documentation build configuration file, created by\n# sphinx-quickstart on Sun Oct  9 16:53:00 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\nimport os\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n# sys.path.insert(0, os.path.abspath('.'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx_tabs.tabs\",\n    \"sphinx_copybutton\",\n    \"sphinxext.opengraph\",\n    \"sphinx_rtd_dark_mode\",\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n# source_suffix = ['.rst', '.md']\nsource_suffix = \".rst\"\n\n# The encoding of source files.\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = \"index\"\n\n# This is overridden by readthedocs with the version tag anyway\nversion = \"devel\"\n# To avoid repetition in <title> we set this to an empty string.\nrelease = \"\"\n\n# General information about the project.\nproject = \"PostgREST \" + version\nauthor = \"The PostgREST contributors\"\ncopyright = \"2017, \" + author\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = \"en\"\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n# today = ''\n# Else, today_fmt is used as the format for a strftime call.\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\", \"shared/*.rst\"]\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = \"sphinx\"\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\nhtml_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n# html_title = u'PostgREST v0.4.0.0'\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n# html_logo = None\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\nhtml_favicon = \"_static/favicon.ico\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n# html_extra_path = []\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\n# html_last_updated_fmt = None\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n# html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n# html_domain_indices = True\n\n# If false, no index is generated.\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# 'ja' uses this config value.\n# 'zh' user can custom change `jieba` dictionary path.\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = \"PostgRESTdoc\"\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #'papersize': 'letterpaper',\n    # The font size ('10pt', '11pt' or '12pt').\n    #'pointsize': '10pt',\n    # Additional stuff for the LaTeX preamble.\n    #'preamble': '',\n    # Latex figure (float) alignment\n    #'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, \"PostgREST.tex\", \"PostgREST Documentation\", author, \"manual\"),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n# latex_appendices = []\n\n# If false, no module index is generated.\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [(master_doc, \"postgrest\", \"PostgREST Documentation\", [author], 1)]\n\n# If true, show URL addresses after external links.\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (\n        master_doc,\n        \"PostgREST\",\n        \"PostgREST Documentation\",\n        author,\n        \"PostgREST\",\n        \"REST API for any PostgreSQL database\",\n        \"Web\",\n    ),\n]\n\n# Documents to append as an appendix to all manuals.\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n# texinfo_no_detailmenu = False\n\n# -- Custom setup ---------------------------------------------------------\n\n\ndef setup(app):\n    app.add_css_file(\"css/custom.css\")\n\n\nuser_agent = (\n    \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0\"\n)\n\nlinkcheck_ignore = [\n    # 403 only in CI / GitHub Actions\n    r\"https://www.patreon.com/postgrest\",\n    r\"https://blog.frankel.ch/poor-man-api\",\n    r\"https://www.cybertec-postgresql.com/.*\",\n    # Odd SSL error\n    r\"https://www.dripdepot.com\",\n    r\"https://www.euronodes.com\",\n    # New GitHub UI delays comment load, so anchor fails\n    r\"https://github.com/.*#issuecomment\",\n    # Random 500 Internal Server Error\n    r\"https://jwt.io\",\n]\n\n# sphinx-tabs configuration\nsphinx_tabs_disable_tab_closing = True\n\n# sphinx_rtd_dark_mode configuration\ndefault_dark_mode = False\n\n# sphinxext-opengraph configuration\n\nogp_image = \"_images/logo.png\"\nogp_use_first_image = True\nogp_enable_meta_description = True\nogp_description_length = 300\n\n## RTD sets html_baseurl, ensures we use the correct env for canonical URLs\n## Useful to generate correct meta tags for Open Graph\n## Refs: https://github.com/readthedocs/readthedocs.org/issues/10226, https://github.com/urllib3/urllib3/pull/3064\nhtml_baseurl = os.environ.get(\"READTHEDOCS_CANONICAL_URL\", \"/\")\n"
  },
  {
    "path": "docs/ecosystem.rst",
    "content": ".. _community_tutorials:\n\nCommunity Tutorials\n-------------------\n\n* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -\n  In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.\n\n* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.\n\n* `\"CodeLess\" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -\n  A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).\n\n* `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request <https://blog.fgribreau.com/2020/11/how-postgresql-triggers-works-when.html>`_ - A tutorial to see how the old and new values are set or not when doing a PATCH request to PostgREST.\n\n* `REST Data Service on YugabyteDB / PostgreSQL <https://dev.to/yugabyte/rest-data-service-on-yugabytedb-postgresql-5f2h>`_\n\n* `Build data-driven applications with Workers and PostgreSQL <https://developers.cloudflare.com/workers/tutorials/postgres/>`_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers.\n\n* `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.\n\n.. * `Accessing a PostgreSQL database in Godot 4 via PostgREST <https://peterkingsbury.com/2022/08/16/godot-postgresql-postgrest/>`_\n\n.. _templates:\n\nTemplates\n---------\n\n* `compose-postgrest <https://github.com/mattddowney/compose-postgrest>`_ - docker-compose setup with Nginx and HTML example\n* `svelte-postgrest-template <https://github.com/guyromm/svelte-postgrest-template>`_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth\n\n.. _eco_example_apps:\n\nExample Apps\n------------\n\n* `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS\n* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js\n* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address\n* `fullstack template <https://github.com/jenstroeger/fullstack-webapp-template>`_ - a complete fullstack webapp template using PG as db and message queue, Python and Dramatiq to implement async jobs, db migrations, test runners, and more.\n* `general <https://github.com/PierreRochard/general>`_ - example auth back-end\n* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository\n* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel\n* `prospector <https://github.com/sfcta/prospector>`_ - data warehouse and visualization platform\n\n.. _devops:\n\nDevOps\n------\n\n* `cloudgov-demo-postgrest <https://github.com/GSA/cloudgov-demo-postgrest>`_ - demo for a federally-compliant REST API on cloud.gov\n* `cloudstark/helm-charts <https://github.com/cloudstark/helm-charts/tree/master/postgrest>`_ - helm chart to deploy PostgREST to a Kubernetes cluster via a Deployment and Service\n* `cyril-sabourault/postgrest-cloud-run <https://github.com/cyril-sabourault/postgrest-cloud-run>`_ - expose a PostgreSQL database on Cloud SQL using Cloud Run\n* `eyberg/postgrest <https://repo.ops.city/v2/packages/eyberg/postgrest/10.1.1/x86_64/show>`_ - run PostgREST as a Nanos unikernel\n* `jbkarle/postgrest <https://github.com/jbkarle/postgrest>`_ - helm chart with a demo database for development and test purposes\n\n.. _eco_external_notification:\n\nExternal Notification\n---------------------\n\nThese are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for further processing. This allows functions to initiate actions outside the database such as sending emails.\n\n* `pg-notify-stdout <https://github.com/mkleczek/pg-notify-stdout>`_ - writes notifications to standard output (use in shell scripts etc.)\n* `pg-notify-webhook <https://github.com/vbalasu/pg-notify-webhook>`_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY\n* `pgsql-listen-exchange <https://github.com/gmr/pgsql-listen-exchange>`_ - RabbitMQ\n* `postgres-websockets <https://github.com/diogob/postgres-websockets>`_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY\n* `postgresql2websocket <https://github.com/frafra/postgresql2websocket>`_ - Websockets\n\n\n.. _eco_extensions:\n\nExtensions\n----------\n\n* `aiodata <https://github.com/Exahilosys/aiodata>`_ - Python, event-based proxy and caching client.\n* `pg-safeupdate <https://github.com/eradman/pg-safeupdate>`_ - prevent full-table updates or deletes\n* `postgrest-node <https://github.com/seveibar/postgrest-node>`_ - Run a PostgREST server in Node.js via npm module\n* `PostgREST-writeAPI <https://github.com/ppKrauss/PostgREST-writeAPI>`_ - generate Nginx rewrite rules to fit an OpenAPI spec\n\n.. _clientside_libraries:\n\nClient-Side Libraries\n---------------------\n\n* `postgrest-csharp <https://github.com/supabase-community/postgrest-csharp>`_ - C#\n* `postgrest-dart <https://github.com/supabase/postgrest-dart>`_ - Dart\n* `postgrest-ex <https://github.com/supabase-community/postgrest-ex>`_ - Elixir\n* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go\n* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript\n* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin\n* `postgrest-py <https://github.com/supabase/postgrest-py>`_ - Python\n* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust\n* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift\n* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.\n* `vue-postgrest <https://github.com/technowledgy/vue-postgrest>`_ - Vue.js\n\n"
  },
  {
    "path": "docs/explanations/architecture.rst",
    "content": "Architecture\n############\n\nThis page describes the architecture of PostgREST.\n\nBird's Eye View\n===============\n\nYou can click on the components to navigate to their respective documentation.\n\n  .. container:: img-dark\n\n    .. See https://github.com/sphinx-doc/sphinx/issues/2240#issuecomment-187366626\n\n    .. raw:: html\n\n      <object width=\"100%\" data=\"../_static/arch-dark.svg\" type=\"image/svg+xml\"></object>\n\n  .. container:: img-light\n\n    .. raw:: html\n\n      <object width=\"100%\" data=\"../_static/arch.svg\" type=\"image/svg+xml\"></object>\n\n\nCode Map\n========\n\nThis section talks briefly about various important modules.\n\nMain\n----\n\nThe starting point of the program is `Main.hs <https://github.com/PostgREST/postgrest/blob/main/main/Main.hs>`_.\n\nCLI\n---\n\nMain then calls `CLI.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/CLI.hs>`_, which is in charge of :ref:`cli`.\n\nApp\n---\n\n`App.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/App.hs>`_ is then in charge of composing the different modules.\n\nAuth\n----\n\n`Auth.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Auth.hs>`_ is in charge  of :ref:`authn`.\n\nApi Request\n-----------\n\n`ApiRequest.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/ApiRequest.hs>`_ is in charge of parsing the URL query string (following PostgREST syntax), the request headers, and the request body.\n\nA request might be rejected at this level if it's invalid. For example when providing an unknown media type to PostgREST or using an unknown HTTP method.\n\nPlan\n----\n\nUsing the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.\n\nA request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.\n\nQuery\n-----\n\n`Query.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Query.hs>`_ generates the SQL queries (parametrized and prepared) required to satisfy the user request.\n\nOnly at this stage a connection from the pool might be used.\n\nSchema Cache\n------------\n\n`SchemaCache.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/SchemaCache.hs>`_ is in charge of :ref:`schema_cache`.\n\nConfig\n------\n\n`Config.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Config.hs>`_ is in charge of :ref:`configuration`.\n\nAdmin\n-----\n\n`Admin.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.\n\nHTTP\n----\n\nThe HTTP server is provided by `Warp <https://aosabook.org/en/posa/warp.html>`_.\n\nListener\n--------\n\n`Listener.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Listener.hs>`_ is in charge of the :ref:`listener`.\n"
  },
  {
    "path": "docs/explanations/db_authz.rst",
    "content": ".. _db_authz:\n\nDatabase Authorization\n######################\n\nDatabase authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles.\n\nUsers and Groups\n================\n\nA role can be thought of as either a database user, or a group of database users, depending on how the role is set up.\n\nRoles for Each Web User\n-----------------------\n\nPostgREST can accommodate either viewpoint. If you treat a role as a single user then :ref:`user_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.\n\nYou can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.\n\n.. code-block:: postgres\n\n  CREATE TABLE chat (\n    message_uuid    UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n    message_time    TIMESTAMP NOT NULL DEFAULT now(),\n    message_from    NAME      NOT NULL DEFAULT current_user,\n    message_to      NAME      NOT NULL,\n    message_subject VARCHAR(64) NOT NULL,\n    message_body    TEXT\n  );\n\n  ALTER TABLE chat ENABLE ROW LEVEL SECURITY;\n\nWe want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the ``message_from`` column with another person's name.\n\nPostgreSQL allows us to set this policy with row-level security:\n\n.. code-block:: postgres\n\n  CREATE POLICY chat_policy ON chat\n    USING ((message_to = current_user) OR (message_from = current_user))\n    WITH CHECK (message_from = current_user)\n\nAnyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding.\n\n.. warning::\n\n   Roles are namespaced per-cluster rather than per-database so they may be prone to collision.\n\nWeb Users Sharing Role\n----------------------\n\nAlternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email.\n\n.. code:: json\n\n  {\n    \"role\": \"webuser\",\n    \"email\": \"john@doe.com\"\n  }\n\nSQL code can access claims through PostgREST :ref:`tx_settings`. For instance to get the email claim, call this function:\n\n.. code:: sql\n\n  current_setting('request.jwt.claims', true)::json->>'email';\n\n.. note::\n\n  For PostgreSQL < 14\n\n  .. code:: sql\n\n    current_setting('request.jwt.claim.email', true);\n\nThis allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this ``current_setting`` rather than ``current_user``. The second ``'true'`` argument tells ``current_setting`` to return NULL if the setting is missing from the current configuration.\n\nHybrid User-Group Roles\n-----------------------\n\nYou can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it:\n\n.. code-block:: postgres\n\n  CREATE ROLE webuser NOLOGIN;\n  -- grant this role access to certain tables etc\n\n  CREATE ROLE user000 NOLOGIN;\n  GRANT webuser TO user000;\n  -- now user000 can do whatever webuser can\n\n  GRANT user000 TO authenticator;\n  -- allow authenticator to switch into user000 role\n  -- (the role itself has nologin)\n\nSchemas\n=======\n\nYou must explicitly allow roles to access the exposed schemas in :ref:`db-schemas`.\n\n.. code-block:: postgres\n\n   GRANT USAGE ON SCHEMA api TO webuser;\n\nTables\n======\n\nTo let web users access tables you must grant them privileges for the operations you want them to do.\n\n.. code-block:: postgres\n\n  GRANT\n    SELECT\n  , INSERT\n  , UPDATE(message_body)\n  , DELETE\n  ON chat TO webuser;\n\nYou can also choose on which table columns the operation is valid. In the above example, the web user can only update the ``message_body`` column.\n\n.. _func_privs:\n\nFunctions\n=========\n\nBy default, when a function is created, the privilege to execute it is not restricted by role. The function access is ``PUBLIC`` — executable by all roles (more details at `PostgreSQL Privileges page <https://www.postgresql.org/docs/current/ddl-priv.html>`_). This is not ideal for an API schema. To disable this behavior, you can run the following SQL statement:\n\n.. code-block:: postgres\n\n  ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;\n\nThis will change the privileges for all functions created in the future in all schemas. Currently there is no way to limit it to a single schema. In our opinion it's a good practice anyway.\n\n.. note::\n\n    It is however possible to limit the effect of this clause only to functions you define. You can put the above statement at the beginning of the API schema definition, and then at the end reverse it with:\n\n    .. code-block:: postgres\n\n        ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC;\n\n    This will work because the :code:`alter default privileges` statement has effect on function created *after* it is executed. See `PostgreSQL alter default privileges <https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html>`_ for more details.\n\nAfter that, you'll need to grant EXECUTE privileges on functions explicitly:\n\n.. code-block:: postgres\n\n   GRANT EXECUTE ON FUNCTION login TO anonymous;\n   GRANT EXECUTE ON FUNCTION signup TO anonymous;\n\nYou can also grant execute on all functions in a schema to a higher privileged role:\n\n.. code-block:: postgres\n\n    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO web_user;\n\nSecurity definer\n----------------\n\nA function is executed with the privileges of the user who calls it. This means that the user has to have all permissions to do the operations the function performs.\nIf the function accesses private database objects, your :ref:`API roles <roles>` won't be able to successfully execute the function.\n\nAnother option is to define the function with the :code:`SECURITY DEFINER` option. Then only one permission check will take place, the permission to call the function, and the operations in the function will have the authority of the user who owns the function itself.\n\n.. code-block:: postgres\n\n  -- login as a user which has privileges on the private schemas\n\n  -- create a sample function\n  create or replace function login(email text, pass text, out token text) as $$\n  begin\n    -- access to a private schema called 'auth'\n    select auth.user_role(email, pass) into _role;\n    -- other operations\n    -- ...\n  end;\n  $$ language plpgsql security definer;\n\nNote the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation <https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY>`_ for more details.\n\nViews\n=====\n\nViews are invoked with the privileges of the view owner, much like functions with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security <https://www.postgresql.org/docs/current/ddl-rowsecurity.html>`_ policies will be bypassed.\n\nIf you're on PostgreSQL >= 15, this behavior can be changed by specifying the ``security_invoker`` option.\n\n.. code-block:: postgres\n\n  CREATE VIEW sample_view WITH (security_invoker = true) AS\n  SELECT * FROM sample_table;\n\nOn PostgreSQL < 15, you can create a non-SUPERUSER role and make this role the view's owner.\n\n.. code-block:: postgres\n\n  CREATE ROLE api_views_owner NOSUPERUSER NOBYPASSRLS;\n  ALTER VIEW sample_view OWNER TO api_views_owner;\n\n"
  },
  {
    "path": "docs/explanations/external_auth.rst",
    "content": ".. _external_auth:\n\nExternal Authentication\n-----------------------\n\nJWT from Auth0\n~~~~~~~~~~~~~~\n\nAn external service like `Auth0 <https://auth0.com/>`_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows.\n\nTo use Auth0, create `an application <https://auth0.com/docs/get-started/applications>`_ for your app and `an API <https://auth0.com/docs/get-started/apis>`_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier <https://auth0.com/docs/get-started/apis/api-settings>`_ by setting it with the `audience parameter <https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience>`_  during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret.\n"
  },
  {
    "path": "docs/explanations/install.rst",
    "content": ".. _install:\n\nInstallation\n############\n\nThe release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_.\nThe Linux binary is a static executable that can be run on any Linux distribution.\n\nYou can also use your OS package manager.\n\n.. include:: ../shared/installation.rst\n\n.. _pg-dependency:\n\nSupported PostgreSQL versions\n=============================\n\n=============== =================================\n**Supported**   PostgreSQL >= 13\n=============== =================================\n\nPostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.\n\n\nRunning PostgREST\n=================\n\nIf you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable.\n\n.. code-block:: bash\n\n  # For UNIX platforms\n  tar Jxf postgrest-[version]-[platform].tar.xz\n\n  # On Windows you should unzip the file\n\nNow you can run PostgREST with the :code:`--help` flag to see usage instructions:\n\n.. code-block:: bash\n\n  # Running postgrest binary\n  ./postgrest --help\n\n  # Running postgrest installed from a package manager\n  postgrest --help\n\n  # You should see a usage help message\n\nThe PostgREST server reads a configuration file as its only argument:\n\n.. code:: bash\n\n  postgrest /path/to/postgrest.conf\n\n  # You can also generate a sample config file with\n  # postgrest -e > postgrest.conf\n  # You'll need to edit this file and remove the usage parts for postgrest to read it\n\nFor a complete reference of the configuration file, see :ref:`configuration`.\n\n.. note::\n\n  If you see a dialog box like this on Windows, it may be that the :code:`pg_config` program is not in your system path.\n\n  .. image:: ../_static/win-err-dialog.png\n\n  It usually lives in :code:`C:\\Program Files\\PostgreSQL\\<version>\\bin`. See this `article <https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/>`_ about how to modify the system path.\n\n  To test that the system path is set correctly, run ``pg_config`` from the command line. You should see it output a list of paths.\n\nDocker\n======\n\nYou can get the `official PostgREST Docker image <https://hub.docker.com/r/postgrest/postgrest>`_ with:\n\n.. code-block:: bash\n\n  # pull the latest version\n  docker pull postgrest/postgrest\n\n  # to pull a particular version, use one of the versions on https://hub.docker.com/r/postgrest/postgrest/tags\n  docker pull postgrest/postgrest:<version>\n\nTo configure the container image, use :ref:`env_variables_config`.\n\nThere are two ways to run the PostgREST container: with an existing external database, or through docker-compose.\n\nContainerized PostgREST with native PostgreSQL\n----------------------------------------------\n\nThe first way to run PostgREST in Docker is to connect it to an existing native database on the host.\n\n.. code-block:: bash\n\n  # Run the server\n  docker run --rm --net=host \\\n    -e PGRST_DB_URI=\"postgres://app_user:password@localhost/postgres\" \\\n    postgrest/postgrest\n\nThe database connection string above is just an example. Adjust the role and password as necessary. You may need to edit PostgreSQL's :code:`pg_hba.conf` to grant the user local login access.\n\n.. note::\n\n  Docker on Mac does not support the :code:`--net=host` flag. Instead you'll need to create an IP address alias to the host. Requests for the IP address from inside the container are unable to resolve and fall back to resolution by the host.\n\n  .. code-block:: bash\n\n    sudo ifconfig lo0 10.0.0.10 alias\n\n  You should then use 10.0.0.10 as the host in your database connection string. Also remember to include the IP address in the :code:`listen_address` within postgresql.conf. For instance:\n\n  .. code-block:: bash\n\n    listen_addresses = 'localhost,10.0.0.10'\n\n  You might also need to add a new IPv4 local connection within pg_hba.conf. For instance:\n\n  .. code-block:: bash\n\n    host    all             all             10.0.0.10/32            trust\n\n  The docker command will then look like this:\n\n  .. code-block:: bash\n\n    # Run the server\n    docker run --rm -p 3000:3000 \\\n      -e PGRST_DB_URI=\"postgres://app_user:password@10.0.0.10/postgres\" \\\n      postgrest/postgrest\n\n.. _pg-in-docker:\n\nContainerized PostgREST *and* db with docker-compose\n----------------------------------------------------\n\nTo avoid having to install the database at all, you can run both it and the server in containers and link them together with docker-compose. Use this configuration:\n\n.. code-block:: yaml\n\n  # docker-compose.yml\n\n  version: '3'\n  services:\n    server:\n      image: postgrest/postgrest\n      ports:\n        - \"3000:3000\"\n      environment:\n        PGRST_SERVER_HOST: 0.0.0.0 # necessary for `postgrest --ready` flag to work\n        PGRST_DB_URI: postgres://app_user:password@db:5432/app_db\n        PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000\n      depends_on:\n        - db\n    db:\n      image: postgres\n      ports:\n        - \"5432:5432\"\n      environment:\n        POSTGRES_DB: app_db\n        POSTGRES_USER: app_user\n        POSTGRES_PASSWORD: password\n    # Uncomment this if you want to persist the data.\n    # volumes:\n    #   - \"./pgdata:/var/lib/postgresql/data\"\n\nGo into the directory where you saved this file and run :code:`docker-compose up`. You will see the logs of both the database and PostgREST, and be able to access the latter on port 3000.\n\nIf you want to have a visual overview of your API in your browser you can add swagger-ui to your :code:`docker-compose.yml`:\n\n.. code-block:: yaml\n\n  # in services:\n    swagger:\n      image: swaggerapi/swagger-ui\n      ports:\n        - \"8080:8080\"\n      expose:\n        - \"8080\"\n      environment:\n        API_URL: http://localhost:3000/\n\nWith this you can see the swagger-ui in your browser on port 8080.\n\n.. _docker_cpu_contraint:\n\nDocker Resource Constraints\n---------------------------\n\nPostgREST does not support ``--cpus`` `constraint option <https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler>`_.\n\nAs a workaround, you may use the `GHC RTS <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/runtime_control.html#runtime-system-rts-options>`_ ``-N`` option. For instance, to limit it to 2 CPU cores, do:\n\n.. code::\n\n  # Set environment variable GHCRTS set to \"-N2\"\n  docker run --rm -p 3000:3000 \\\n    -e PGRST_DB_URI=\"postgres://app_user:password@10.0.0.10/postgres\" \\\n    -e GHCRTS=\"-N2\"\n    postgrest/postgrest\n\n.. _build_source:\n\nBuilding from Source\n====================\n\nWhen a pre-built binary does not exist for your system you can build the project from source.\n\nYou can build PostgREST from source with `Stack <https://github.com/commercialhaskell/stack>`_. It will install any necessary Haskell dependencies on your system.\n\n* `Install Stack <https://docs.haskellstack.org/en/stable/#how-to-install-stack>`_ for your platform\n* Install Library Dependencies\n\n  =====================  =======================================\n  Operating System       Dependencies\n  =====================  =======================================\n  Ubuntu/Debian          libpq-dev, libgmp-dev, zlib1g-dev\n  CentOS/Fedora/Red Hat  postgresql-devel, zlib-devel, gmp-devel\n  BSD                    postgresql12-client\n  macOS                  libpq, gmp\n  =====================  =======================================\n\n* Build and install binary\n\n  .. code-block:: bash\n\n    git clone https://github.com/PostgREST/postgrest.git\n    cd postgrest\n\n    # adjust local-bin-path to taste\n    stack build --install-ghc --copy-bins --local-bin-path /usr/local/bin\n\n.. note::\n\n   - If building fails and your system has less than 1GB of memory, try adding a swap file.\n   - `--install-ghc` flag is only needed for the first build and can be omitted in the subsequent builds.\n\n* Check that the server is installed: :code:`postgrest --help`.\n"
  },
  {
    "path": "docs/explanations/nginx.rst",
    "content": ".. _nginx:\n\nNginx\n=====\n\nPostgREST is a fast way to construct a RESTful API. Its default behavior is great for scaffolding in development. When it's time to go to production it works great too, as long as you take precautions.\nPostgREST is a small sharp tool that focuses on performing the API-to-database mapping. We rely on a reverse proxy like Nginx for additional safeguards.\n\nThe first step is to create an Nginx configuration file that proxies requests to an underlying PostgREST server.\n\n.. code-block:: nginx\n\n  http {\n    # ...\n    # upstream configuration\n    upstream postgrest {\n      server localhost:3000;\n    }\n    # ...\n    server {\n      # ...\n      # expose to the outside world\n      location /api/ {\n        default_type  application/json;\n        proxy_hide_header Content-Location;\n        add_header Content-Location  /api/$upstream_http_content_location;\n        proxy_set_header  Connection \"\";\n        proxy_http_version 1.1;\n        proxy_pass http://postgrest/;\n      }\n      # ...\n    }\n  }\n\n.. note::\n\n  For ubuntu, if you already installed nginx through :code:`apt` you can add this to the config file in\n  :code:`/etc/nginx/sites-enabled/default`.\n\n.. _https:\n\nHTTPS\n-----\n\nPostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL database. To keep the code small and focused we do not implement HTTPS. Use a reverse proxy such as NGINX to add this, `here's how <https://nginx.org/en/docs/http/configuring_https_servers.html>`_.\n\nRate Limiting\n-------------\n\nNginx supports \"leaky bucket\" rate limiting (see `official docs <https://nginx.org/en/docs/http/ngx_http_limit_req_module.html>`_). Using standard Nginx configuration, routes can be grouped into *request zones* for rate limiting. For instance we can define a zone for login attempts:\n\n.. code-block:: nginx\n\n  limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;\n\nThis creates a shared memory zone called \"login\" to store a log of IP addresses that access the rate limited urls. The space reserved, 10 MB (:code:`10m`) will give us enough space to store a history of 160k requests. We have chosen to allow only allow one request per second (:code:`1r/s`).\n\nNext we apply the zone to certain routes, like a hypothetical function called :code:`login`.\n\n.. code-block:: nginx\n\n  location /rpc/login/ {\n    # apply rate limiting\n    limit_req zone=login burst=5;\n  }\n\nThe burst argument tells Nginx to start dropping requests if more than five queue up from a specific IP.\n\nNginx rate limiting is general and indiscriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation <custom_validation>` function.\n\nAlternate URL Structure\n-----------------------\n\nAs discussed in :ref:`singular_plural`, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like :code:`/people/1`. It would be specified instead as\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?id=eq.1\" \\\n    -H \"Accept: application/vnd.pgrst.object+json\"\n\nThis allows compound primary keys and makes the intent for singular response independent of a URL convention.\n\nNginx rewrite rules allow you to simulate the familiar URL convention. The following example adds a rewrite rule for all table endpoints, but you'll want to restrict it to those tables that have a numeric simple primary key named \"id.\"\n\n.. code-block:: nginx\n\n  # support /endpoint/:id url style\n  location ~ ^/([a-z_]+)/([0-9]+) {\n\n    # make the response singular\n    proxy_set_header Accept 'application/vnd.pgrst.object+json';\n\n    # assuming an upstream named \"postgrest\"\n    proxy_pass http://postgrest/$1?id=eq.$2;\n\n  }\n\n.. TODO\n.. Administration\n..   API Versioning\n..   HTTP Caching\n..   Upgrading\n"
  },
  {
    "path": "docs/explanations/schema_isolation.rst",
    "content": ".. _schema_isolation:\n\nSchema Isolation\n================\n\nA PostgREST instance exposes all the tables, views, and functions of a single `PostgreSQL schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ (a namespace of database objects). This means private data or implementation details can go inside different private schemas and be invisible to HTTP clients.\n\nIt is recommended that you don't expose tables on your API schema. Instead expose views and functions which insulate the internal details from the outside world.\nThis allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning.\n\n.. container:: svg-container-md\n\n  .. container:: img-dark\n\n    .. See https://github.com/sphinx-doc/sphinx/issues/2240#issuecomment-187366626\n\n    .. raw:: html\n\n      <object width=\"100%\" data=\"../_static/sch-iso-dark.svg\" type=\"image/svg+xml\"></object>\n\n  .. container:: img-light\n\n    .. raw:: html\n\n      <object width=\"100%\" data=\"../_static/sch-iso.svg\" type=\"image/svg+xml\"></object>\n"
  },
  {
    "path": "docs/how-tos/create-soap-endpoint.rst",
    "content": ".. _create_soap_endpoint:\n\nCreate a SOAP endpoint\n======================\n\n:author: `fjf2002 <https://github.com/fjf2002>`_\n\nPostgREST supports :ref:`custom_media`. With a bit of work, SOAP endpoints become possible.\n\nMinimal Example\n---------------\n\nThis example will simply return the request body, inside a tag ``therequestbodywas``.\n\nAdd the following function to your PostgreSQL database:\n\n.. code-block:: postgres\n\n   create domain \"text/xml\" as pg_catalog.xml;\n\n   CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS \"text/xml\" AS $$\n   DECLARE\n     nsarray CONSTANT text[][] := ARRAY[\n       ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']\n     ];\n   BEGIN\n     RETURN xmlelement(\n       NAME \"soapenv:Envelope\",\n       XMLATTRIBUTES('http://schemas.xmlsoap.org/soap/envelope/' AS \"xmlns:soapenv\"),\n       xmlelement(NAME \"soapenv:Header\"),\n       xmlelement(\n         NAME \"soapenv:Body\",\n         xmlelement(\n           NAME theRequestBodyWas,\n           (xpath('/soapenv:Envelope/soapenv:Body', $1, nsarray))[1]\n         )\n       )\n    );\n   END;\n   $$ LANGUAGE plpgsql;\n\nDo not forget to refresh the :ref:`PostgREST schema cache <schema_reloading>`.\n\nUse ``curl`` for a first test:\n\n.. code-block:: bash\n\n    curl http://localhost:3000/rpc/my_soap_endpoint \\\n        --header 'Content-Type: text/xml' \\\n        --header 'Accept: text/xml' \\\n        --data-binary @- <<XML\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n      <soapenv:Header/>\n      <soapenv:Body>\n        <mySOAPContent>\n          My SOAP Content\n        </mySOAPContent>\n      </soapenv:Body>\n    </soapenv:Envelope>\n    XML\n\nThe output should contain the original request body within the ``therequestbodywas`` entity,\nand should roughly look like:\n\n.. code-block:: xml\n\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n      <soapenv:Header/>\n      <soapenv:Body>\n        <therequestbodywas>\n          <soapenv:Body xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n            <mySOAPContent>\n              My SOAP Content\n            </mySOAPContent>\n          </soapenv:Body>\n        </therequestbodywas>\n      </soapenv:Body>\n    </soapenv:Envelope>\n\nA more elaborate example\n------------------------\n\nHere we have a SOAP service that converts a fraction to a decimal value,\nwith pass-through of PostgreSQL errors to the SOAP response.\nPlease note that in production you probably should not pass through plain database errors\npotentially disclosing internals to the client, but instead handle the errors directly.\n\n\n.. code-block:: postgres\n\n   -- helper function\n   CREATE OR REPLACE FUNCTION _soap_envelope(body xml)\n    RETURNS xml\n    LANGUAGE sql\n   AS $function$\n     SELECT xmlelement(\n       NAME \"soapenv:Envelope\",\n       XMLATTRIBUTES('http://schemas.xmlsoap.org/soap/envelope/' AS \"xmlns:soapenv\"),\n       xmlelement(NAME \"soapenv:Header\"),\n       xmlelement(NAME \"soapenv:Body\", body)\n     );\n   $function$;\n\n   -- helper function\n   CREATE OR REPLACE FUNCTION _soap_exception(\n     faultcode text,\n     faultstring text\n   )\n    RETURNS xml\n    LANGUAGE sql\n   AS $function$\n     SELECT _soap_envelope(\n       xmlelement(NAME \"soapenv:Fault\",\n         xmlelement(NAME \"faultcode\", faultcode),\n         xmlelement(NAME \"faultstring\", faultstring)\n       )\n     );\n   $function$;\n\n   CREATE OR REPLACE FUNCTION fraction_to_decimal(xml)\n    RETURNS \"text/xml\"\n    LANGUAGE plpgsql\n   AS $function$\n   DECLARE\n     nsarray CONSTANT text[][] := ARRAY[\n       ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']\n     ];\n     exc_msg text;\n     exc_detail text;\n     exc_hint text;\n     exc_sqlstate text;\n   BEGIN\n     -- simulating a statement that results in an exception:\n     RETURN _soap_envelope(xmlelement(\n       NAME \"decimalValue\",\n       (\n         (xpath('/soapenv:Envelope/soapenv:Body/fraction/numerator/text()', $1, nsarray))[1]::text::int\n         /\n         (xpath('/soapenv:Envelope/soapenv:Body/fraction/denominator/text()', $1, nsarray))[1]::text::int\n       )::text::xml\n     ));\n   EXCEPTION WHEN OTHERS THEN\n     GET STACKED DIAGNOSTICS\n       exc_msg := MESSAGE_TEXT,\n       exc_detail := PG_EXCEPTION_DETAIL,\n       exc_hint := PG_EXCEPTION_HINT,\n       exc_sqlstate := RETURNED_SQLSTATE;\n     RAISE WARNING USING\n       MESSAGE = exc_msg,\n       DETAIL = exc_detail,\n       HINT = exc_hint;\n     RETURN _soap_exception(faultcode => exc_sqlstate, faultstring => concat(exc_msg, ', DETAIL: ', exc_detail, ', HINT: ', exc_hint));\n   END\n   $function$;\n\nLet's test the ``fraction_to_decimal`` service with illegal values:\n\n.. code-block:: bash\n\n    curl http://localhost:3000/rpc/fraction_to_decimal \\\n      --header 'Content-Type: text/xml' \\\n      --header 'Accept: text/xml' \\\n      --data-binary @- <<XML\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n      <soapenv:Header/>\n      <soapenv:Body>\n        <fraction>\n          <numerator>42</numerator>\n          <denominator>0</denominator>\n        </fraction>\n      </soapenv:Body>\n    </soapenv:Envelope>\n    XML\n\nThe output should roughly look like:\n\n.. code-block:: xml\n\n   <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">\n     <soapenv:Header/>\n     <soapenv:Body>\n       <soapenv:Fault>\n         <faultcode>22012</faultcode>\n         <faultstring>division by zero, DETAIL: , HINT: </faultstring>\n       </soapenv:Fault>\n     </soapenv:Body>\n   </soapenv:Envelope>\n\nReferences\n----------\n\nFor more information concerning PostgREST, cf.\n\n- :ref:`function_single_unnamed`\n- :ref:`custom_media`. See :ref:`any_handler`, if you need to support an ``application/soap+xml`` media type or if you want to respond with XML without sending a media type.\n- :ref:`Nginx reverse proxy <nginx>`\n\nFor SOAP reference, visit\n\n- the specification at https://www.w3.org/TR/soap/\n- shorter more practical advice is available at https://www.w3schools.com/xml/xml_soap.asp\n"
  },
  {
    "path": "docs/how-tos/providing-html-content-using-htmx.rst",
    "content": "\n.. _providing_html_htmx:\n\nProviding HTML Content Using Htmx\n=================================\n\n:author: `Laurence Isla <https://github.com/laurenceisla>`_\n\nThis how-to shows a way to return HTML content and use the `htmx library <https://htmx.org/>`_ to handle the AJAX requests.\nHtmx expects an HTML response and uses it to replace an element inside the DOM (see the `htmx introduction <https://htmx.org/docs/#introduction>`_ in the docs).\n\n.. image:: ../_static/how-tos/htmx-demo.gif\n\n.. warning::\n\n  This is a proof of concept showing what can be achieved using both technologies.\n  We are working on `plmustache <https://github.com/PostgREST/plmustache>`_ which will further improve the HTML aspect of this how-to.\n\nPreparatory Configuration\n-------------------------\n\nWe will make a to-do app based on the :ref:`tut0`, so make sure to complete it before continuing.\n\nTo simplify things, we won't be using authentication, so grant all permissions on the ``todos`` table to the ``web_anon`` user.\n\n.. code-block:: postgres\n\n  grant all on api.todos to web_anon;\n  grant usage, select on sequence api.todos_id_seq to web_anon;\n\nNext, add the ``text/html`` as a :ref:`custom_media`. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)\nand return a raw HTML document file.\n\n.. code-block:: postgres\n\n  create domain \"text/html\" as text;\n\nCreating an HTML Response\n-------------------------\n\nLet's create a function that returns a basic HTML file, using `Pico CSS <https://picocss.com>`_ for styling and\n`Ionicons <https://ionic.io/ionicons>`_ to show some icons later.\n\n.. code-block:: postgres\n\n  create or replace function api.index() returns \"text/html\" as $$\n    select $html$\n      <!DOCTYPE html>\n      <html>\n      <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        <title>PostgREST + HTMX To-Do List</title>\n        <!-- Pico CSS for CSS styling -->\n        <link href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@next/css/pico.min.css\" rel=\"stylesheet\" />\n      </head>\n      <body>\n        <main class=\"container\">\n          <article>\n            <h5 style=\"text-align: center;\">\n              PostgREST + HTMX To-Do List\n            </h5>\n          </article>\n        </main>\n        <!-- Script for Ionicons icons -->\n        <script type=\"module\" src=\"https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js\"></script>\n        <script nomodule src=\"https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js\"></script>\n      </body>\n      </html>\n    $html$;\n  $$ language sql;\n\nThe web browser will open the web page at ``http://localhost:3000/rpc/index``.\n\n.. image:: ../_static/how-tos/htmx-simple.jpg\n\n.. _html_htmx_list_create:\n\nListing and Creating To-Dos\n---------------------------\n\nNow, let's show a list of the to-dos already inserted in the database.\nFor that, we'll also need a function to help us sanitize the HTML content that may be present in the task.\n\n.. code-block:: postgres\n\n  create or replace function api.sanitize_html(text) returns text as $$\n    select replace(replace(replace(replace(replace($1, '&', '&amp;'), '\"', '&quot;'),'>', '&gt;'),'<', '&lt;'), '''', '&apos;')\n  $$ language sql;\n\n  create or replace function api.html_todo(api.todos) returns text as $$\n    select format($html$\n      <div>\n        <%2$s>\n          %3$s\n        </%2$s>\n      </div>\n      $html$,\n      $1.id,\n      case when $1.done then 's' else 'span' end,\n      api.sanitize_html($1.task)\n    );\n  $$ language sql stable;\n\n  create or replace function api.html_all_todos() returns text as $$\n    select coalesce(\n      string_agg(api.html_todo(t), '<hr/>' order by t.id),\n      '<p><em>There is nothing else to do.</em></p>'\n    )\n    from api.todos t;\n  $$ language sql;\n\nThese two functions are used to build the to-do list template. We won't use them as PostgREST endpoints.\n\n- The ``api.html_todo`` function uses the table ``api.todos`` as a parameter and formats each item into a list element ``<li>``.\n  The PostgreSQL `format <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>`_ is useful to that end.\n  It replaces the values according to the position in the template, e.g. ``%1$s`` will be replaced with the value of ``$1.id`` (the first parameter).\n\n- The ``api.html_all_todos`` function returns the ``<ul>`` wrapper for all the list elements.\n  It uses `string_arg <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ to concatenate all the to-dos in a single text value.\n  It also returns an alternative message, instead of a list, when the ``api.todos`` table is empty.\n\nNext, let's add an endpoint to register a to-do in the database and modify the ``/rpc/index`` page accordingly.\n\n.. code-block:: postgres\n\n  create or replace function api.add_todo(_task text) returns \"text/html\" as $$\n    insert into api.todos(task) values (_task);\n    select api.html_all_todos();\n  $$ language sql;\n\n  create or replace function api.index() returns \"text/html\" as $$\n    select $html$\n      <!DOCTYPE html>\n      <html>\n      <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        <title>PostgREST + HTMX To-Do List</title>\n        <!-- Pico CSS for CSS styling -->\n        <link href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@next/css/pico.min.css\" rel=\"stylesheet\"/>\n        <!-- htmx for AJAX requests -->\n        <script src=\"https://unpkg.com/htmx.org\"></script>\n      </head>\n      <body>\n        <main class=\"container\"\n              style=\"max-width: 600px\"\n              hx-headers='{\"Accept\": \"text/html\"}'>\n          <article>\n            <h5 style=\"text-align: center;\">\n              PostgREST + HTMX To-Do List\n            </h5>\n            <form hx-post=\"/rpc/add_todo\"\n                  hx-target=\"#todo-list-area\"\n                  hx-trigger=\"submit\"\n                  hx-on=\"htmx:afterRequest: this.reset()\">\n              <input type=\"text\" name=\"_task\" placeholder=\"Add a todo...\">\n            </form>\n            <div id=\"todo-list-area\">\n              $html$\n                || api.html_all_todos() ||\n              $html$\n            <div>\n          </article>\n        </main>\n        <!-- Script for Ionicons icons -->\n        <script type=\"module\" src=\"https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js\"></script>\n        <script nomodule src=\"https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js\"></script>\n      </body>\n      </html>\n    $html$;\n  $$ language sql;\n\n- The ``/rpc/add_todo`` endpoint allows us to add a new to-do using the ``_task`` parameter and returns an ``html`` with all the to-dos in the database.\n\n- The ``/rpc/index`` now adds the ``hx-headers='{\"Accept\": \"text/html\"}'`` tag to the ``<body>``.\n  This will make sure that all htmx elements inside the body send this header, otherwise PostgREST won't recognize it as HTML.\n\n  There is also a ``<form>`` element that uses the htmx library. Let's break it down:\n\n  + ``hx-post=\"/rpc/add_todo\"``: sends an AJAX POST request to the ``/rpc/add_todo`` endpoint, with the value of the ``_task`` from the ``<input>`` element.\n\n  + ``hx-target=\"#todo-list-area\"``: the HTML content returned from the request will go inside ``<div id=\"todo-list-area\"></div>`` (which is the list of to-dos).\n\n  + ``hx-trigger=\"submit\"``: htmx will do this request when submitting the form (by pressing enter while inside the ``<input>``).\n\n  + ``hx-on=\"htmx:afterRequest: this.reset()\">``: this is a Javascript command that clears the form `after the request is done <https://htmx.org/events/#htmx:afterRequest>`_.\n\nWith this, the ``http://localhost:3000/rpc/index`` page lists all the todos and adds new ones by submitting tasks in the input element.\nDon't forget to refresh the :ref:`schema cache <schema_reloading>`.\n\n.. image:: ../_static/how-tos/htmx-insert.gif\n\nEditing and Deleting To-Dos\n---------------------------\n\nNow, let's modify ``api.html_todo`` and make it more functional.\n\n.. code-block:: postgres\n\n  create or replace function api.html_todo(api.todos) returns text as $$\n    select format($html$\n      <div class=\"grid\">\n        <div id=\"todo-edit-area-%1$s\">\n          <form id=\"edit-task-state-%1$s\"\n                hx-post=\"/rpc/change_todo_state\"\n                hx-vals='{\"_id\": %1$s, \"_done\": %4$s}'\n                hx-target=\"#todo-list-area\"\n                hx-trigger=\"click\">\n            <%2$s style=\"cursor: pointer\">\n              %3$s\n            </%2$s>\n          </form>\n        </div>\n        <div style=\"text-align: right\">\n          <button class=\"outline\"\n                  hx-get=\"/rpc/html_editable_task\"\n                  hx-vals='{\"_id\": \"%1$s\"}'\n                  hx-target=\"#todo-edit-area-%1$s\"\n                  hx-trigger=\"click\">\n            <span>\n              <ion-icon name=\"create\"></ion-icon>\n            </span>\n          </button>\n          <button class=\"outline contrast\"\n                  hx-post=\"/rpc/delete_todo\"\n                  hx-vals='{\"_id\": %1$s}'\n                  hx-target=\"#todo-list-area\"\n                  hx-trigger=\"click\">\n            <span>\n              <ion-icon name=\"trash\" style=\"color: #f87171\"></ion-icon>\n            </span>\n          </button>\n        </div>\n      </div>\n      $html$,\n      $1.id,\n      case when $1.done then 's' else 'span' end,\n      api.sanitize_html($1.task),\n      (not $1.done)::text\n    );\n  $$ language sql stable;\n\nLet's deconstruct the new htmx features added:\n\n- The ``<form>`` element is configured as follows:\n\n  + ``hx-post=\"/rpc/change_todo_state\"``: does an AJAX POST request to that endpoint. It will toggle the ``done`` state of the to-do.\n\n  + ``hx-vals='{\"_id\": %1$s, \"_done\": %4$s}'``: adds the parameters to the request.\n    This is an alternative to using hidden inputs inside the ``<form>``.\n\n  + ``hx-trigger=\"click\"``: htmx does the request after clicking on the element.\n\n- For the first ``<button>``:\n\n  + ``hx-get=\"/rpc/html_editable_task\"``: it does an AJAX GET request to that endpoint.\n    It returns an HTML with an input that will allow us to edit the task.\n\n  + ``hx-target=\"#todo-edit-area\"``: the returned HTML will replace the element with this id.\n    In this case, this replaces an individual task, not the whole list.\n\n  + ``hx-vals='{\"id\": \"eq.%1$s\"}'``: adds the query parameters to the GET request.\n    Note that this needs the ``eq.`` operator because it represents a table column not a function parameter.\n\n- For the second ``<button>``:\n\n  + ``hx-post=\"/rpc/delete_todo\"``: this post request will delete the corresponding to-do.\n\nClicking on the first button will enable the task editing.\nThat's why we create the ``api.html_editable_task`` function as an endpoint:\n\n.. code-block:: postgres\n\n  create or replace function api.html_editable_task(_id int) returns \"text/html\" as $$\n    select format ($html$\n    <form id=\"edit-task-%1$s\"\n          hx-post=\"/rpc/change_todo_task\"\n          hx-headers='{\"Accept\": \"text/html\"}'\n          hx-vals='{\"_id\": %1$s}'\n          hx-target=\"#todo-list-area\"\n          hx-trigger=\"submit,focusout\">\n      <input id=\"task-%1$s\" type=\"text\" name=\"_task\" value=\"%2$s\" autofocus>\n    </form>\n    $html$,\n      id,\n      api.sanitize_html(task)\n    )\n    from api.todos\n    where id = _id;\n  $$ language sql;\n\nIn this example, this will return an input field that allows us to edit the corresponding to-do task.\n\nFinally, let's add the endpoints that will modify and delete the to-dos in the database.\n\n.. code-block:: postgres\n\n  create or replace function api.change_todo_state(_id int, _done boolean) returns \"text/html\" as $$\n    update api.todos set done = _done where id = _id;\n    select api.html_all_todos();\n  $$ language sql;\n\n  create or replace function api.change_todo_task(_id int, _task text) returns \"text/html\" as $$\n    update api.todos set task = _task where id = _id;\n    select api.html_all_todos();\n  $$ language sql;\n\n  create or replace function api.delete_todo(_id int) returns \"text/html\" as $$\n    delete from api.todos where id = _id;\n    select api.html_all_todos();\n  $$ language sql;\n\nAll of those functions return an HTML list of to-dos that will replace the outdated one:\n\n- The ``api.change_todo_state`` function updates the ``done`` column using the ``_id`` and the ``_done`` values from the request.\n\n- The ``api.delete_todo`` function deletes a to-do using the ``_id`` value from the request.\n\n- The ``api.change_todo_task`` function modifies the ``task`` column  using the ``_id`` and the ``_task`` value from the request.\n\nAfter refreshing the :ref:`schema cache <schema_reloading>`, the page at ``http://localhost:3000/rpc/index`` will allow us to edit, delete and complete any to-do.\n\n.. image:: ../_static/how-tos/htmx-edit-delete.gif\n\nWith that, we completed the to-do list functionality.\n"
  },
  {
    "path": "docs/how-tos/providing-images-for-img.rst",
    "content": ".. _providing_img:\n\nProviding images for ``<img>``\n==============================\n\n:author: `pkel <https://github.com/pkel>`_\n\nIn this how-to, you will learn how to create an endpoint for providing images to HTML :code:`<img>` tags without client side JavaScript. In fact, the presented technique is suitable for providing not only images, but arbitrary files.\n\nWe will start with a minimal example that highlights the general concept.\nAfterwards we present a more detailed solution that fixes a few shortcomings of the first approach.\n\n.. warning::\n\n   Be careful when saving binaries in the database, having a separate storage service for these is preferable in most cases. See `Storing Binary files in the Database <https://wiki.postgresql.org/wiki/BinaryFilesInDB>`_.\n\nMinimal Example\n---------------\n\nFirst, we need a public table for storing the files.\n\n.. code-block:: postgres\n\n   create table files(\n     id   int primary key\n   , blob bytea\n   );\n\nLet's assume this table contains an image of two cute kittens with id 42. We can retrieve this image in binary format from our PostgREST API by using :ref:`custom_media`:\n\n.. code-block:: postgres\n\n   create domain \"application/octet-stream\" as bytea;\n\n   create or replace function file(id int) returns \"application/octet-stream\" as $$\n     select blob from files where id = file.id;\n   $$ language sql;\n\nNow we can request the RPC endpoint :code:`/rpc/file?id=42` with the :code:`Accept: application/octet-stream` header.\n\n\n.. code-block:: bash\n\n   curl \"localhost:3000/rpc/file?id=42\" -H \"Accept: application/octet-stream\"\n\n\nUnfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work. That's because browsers do not send the required :code:`Accept: application/octet-stream` header.\nInstead, the :code:`Accept: image/webp` header is sent by many web browsers by default.\n\nLuckily we can change the accepted media type in the function like so:\n\n.. code-block:: postgres\n\n   create domain \"image/webp\" as bytea;\n\n   create or replace function file(id int) returns \"image/webp\" as $$\n     select blob from files where id = file.id;\n   $$ language sql;\n\nNow, the image will be displayed in the HTML page:\n\n.. code-block:: html\n\n   <img src=\"http://localhost:3000/file?id=42\" alt=\"Cute Kittens\"/>\n\nImproved Version\n----------------\n\nThe basic solution has some shortcomings:\n\n1.  The response :code:`Content-Type` header is set to :code:`image/webp`.\n    This might be a problem if you want to specify a different format for the file.\n2.  Download requests (e.g. Right Click -> Save Image As) to :code:`/files?select=blob&id=eq.42` will propose :code:`files` as filename.\n    This might confuse users.\n3.  Requests to the binary endpoint are not cached.\n    This will cause unnecessary load on the database.\n\nThe following improved version addresses these problems.\nFirst, in addition to the minimal example, we need to store the media types and names of our files in the database.\n\n.. code-block:: postgres\n\n   alter table files\n     add column type text generated always as (byteamagic_mime(substr(blob, 0, 4100))) stored,\n     add column name text;\n\nThis uses the :code:`byteamagic_mime()` function from the `pg_byteamagic extension <https://github.com/nmandery/pg_byteamagic>`_ to automatically generate the type in the :code:`files` table. To guess the type of a file, it's generally enough to look at the beginning of the file, which is more efficient.\n\nNext, we set modify the function to set the content type and filename.\nWe use this opportunity to configure some basic, client-side caching.\nFor production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy <nginx>`.\n\n.. code-block:: postgres\n\n   create domain \"*/*\" as bytea;\n\n   create function file(id int) returns \"*/*\" as\n   $$\n     declare headers text;\n     declare blob bytea;\n     begin\n       select format(\n         '[{\"Content-Type\": \"%s\"},'\n          '{\"Content-Disposition\": \"inline; filename=\\\"%s\\\"\"},'\n          '{\"Cache-Control\": \"max-age=259200\"}]'\n         , files.type, files.name)\n       from files where files.id = file.id into headers;\n       perform set_config('response.headers', headers, true);\n       select files.blob from files where files.id = file.id into blob;\n       if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS\n       then return(blob);\n       else raise sqlstate 'PT404' using\n         message = 'NOT FOUND',\n         detail = 'File not found',\n         hint = format('%s seems to be an invalid file id', file.id);\n       end if;\n     end\n   $$ language plpgsql;\n\nWith this, we can obtain the cat image from :code:`/rpc/file?id=42`. Thus, the resulting HTML will be:\n\n.. code-block:: html\n\n   <img src=\"http://localhost:3000/rpc/file?id=42\" alt=\"Cute Kittens\"/>\n"
  },
  {
    "path": "docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst",
    "content": ".. _sql-user-management-using-postgres-users-and-passwords:\n\nSQL User Management using postgres' users and passwords\n=======================================================\n\n:author: `fjf2002 <https://github.com/fjf2002>`_\n\n\nThis is an alternative to chapter :ref:`sql_user_management`, solely using the PostgreSQL built-in table `pg_catalog.pg_authid <https://www.postgresql.org/docs/current/catalog-pg-authid.html>`_ for user management. This means\n\n- no dedicated user table (aside from :code:`pg_authid`) is required\n\n- PostgreSQL's users and passwords (i. e. the stuff in :code:`pg_authid`) are also used at the PostgREST level.\n\n.. note::\n  Only PostgreSQL users with SCRAM-SHA-256 password hashes (the default since PostgreSQL v14) are supported.\n\n.. warning::\n\n  This is experimental. We can't give you any guarantees, especially concerning security. Use at your own risk.\n\n\n\nWorking with pg_authid and SCRAM-SHA-256 hashes\n-----------------------------------------------\n\nAs in :ref:`sql_user_management`, we create a :code:`basic_auth` schema:\n\n.. code-block:: postgres\n\n  -- We put things inside the basic_auth schema to hide\n  -- them from public view. Certain public procs/views will\n  -- refer to helpers and tables inside.\n  CREATE SCHEMA basic_auth;\n\n\nAs in :ref:`sql_user_management`, we create the :code:`pgcrypto` and :code:`pgjwt` extensions. Here we prefer to put the extensions in its own schemas:\n\n.. code-block:: postgres\n\n  CREATE SCHEMA ext_pgcrypto;\n  ALTER SCHEMA ext_pgcrypto OWNER TO postgres;\n  CREATE EXTENSION pgcrypto WITH SCHEMA ext_pgcrypto;\n\n\nConcerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`jwt-from-sql`.\n\n.. code-block:: postgres\n\n  CREATE SCHEMA ext_pgjwt;\n  ALTER SCHEMA ext_pgjwt OWNER TO postgres;\n  CREATE EXTENSION pgjwt WITH SCHEMA ext_pgjwt;\n\n\nIn order to be able to work with postgres' SCRAM-SHA-256 password hashes, we also need the PBKDF2 key derivation function. Luckily there is `a PL/pgSQL implementation on stackoverflow <https://stackoverflow.com/a/72805848>`_:\n\n.. code-block:: postgres\n\n  CREATE FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) RETURNS bytea\n      LANGUAGE plpgsql IMMUTABLE\n      AS $$\n  DECLARE\n    hash_length integer;\n    block_count integer;\n    output bytea;\n    the_last bytea;\n    xorsum bytea;\n    i_as_int32 bytea;\n    i integer;\n    j integer;\n    k integer;\n  BEGIN\n    algorithm := lower(algorithm);\n    CASE algorithm\n    WHEN 'md5' then\n      hash_length := 16;\n    WHEN 'sha1' then\n      hash_length = 20;\n    WHEN 'sha256' then\n      hash_length = 32;\n    WHEN 'sha512' then\n      hash_length = 64;\n    ELSE\n      RAISE EXCEPTION 'Unknown algorithm \"%\"', algorithm;\n    END CASE;\n    --\n    block_count := ceil(desired_length::real / hash_length::real);\n    --\n    FOR i in 1 .. block_count LOOP\n      i_as_int32 := E'\\\\000\\\\000\\\\000'::bytea || chr(i)::bytea;\n      i_as_int32 := substring(i_as_int32, length(i_as_int32) - 3);\n      --\n      the_last := salt::bytea || i_as_int32;\n      --\n      xorsum := ext_pgcrypto.HMAC(the_last, pw::bytea, algorithm);\n      the_last := xorsum;\n      --\n      FOR j IN 2 .. count LOOP\n        the_last := ext_pgcrypto.HMAC(the_last, pw::bytea, algorithm);\n\n        -- xor the two\n        FOR k IN 1 .. length(xorsum) LOOP\n          xorsum := set_byte(xorsum, k - 1, get_byte(xorsum, k - 1) # get_byte(the_last, k - 1));\n        END LOOP;\n      END LOOP;\n      --\n      IF output IS NULL THEN\n        output := xorsum;\n      ELSE\n        output := output || xorsum;\n      END IF;\n    END LOOP;\n    --\n    RETURN substring(output FROM 1 FOR desired_length);\n  END $$;\n\n  ALTER FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) OWNER TO postgres;\n\n\nAnalogous to how :ref:`sql_user_management` creates the function :code:`basic_auth.user_role`, we create a helper function to check the user's password, here with another name and signature (since we want the username, not an email address).\nBut contrary to :ref:`sql_user_management`, this function does not use a dedicated :code:`users` table with passwords, but instead utilizes the built-in table `pg_catalog.pg_authid <https://www.postgresql.org/docs/current/catalog-pg-authid.html>`_:\n\n.. code-block:: postgres\n\n  CREATE FUNCTION basic_auth.check_user_pass(username text, password text) RETURNS name\n      LANGUAGE sql\n      AS\n  $$\n    SELECT rolname AS username\n    FROM pg_authid\n    -- regexp-split scram hash:\n    CROSS JOIN LATERAL regexp_match(rolpassword, '^SCRAM-SHA-256\\$(.*):(.*)\\$(.*):(.*)$') AS rm\n    -- identify regexp groups with sane names:\n    CROSS JOIN LATERAL (SELECT rm[1]::integer AS iteration_count, decode(rm[2], 'base64') as salt, decode(rm[3], 'base64') AS stored_key, decode(rm[4], 'base64') AS server_key, 32 AS digest_length) AS stored_password_part\n    -- calculate pbkdf2-digest:\n    CROSS JOIN LATERAL (SELECT basic_auth.pbkdf2(salt, check_user_pass.password, iteration_count, digest_length, 'sha256')) AS digest_key(digest_key)\n    -- based on that, calculate hashed passwort part:\n    CROSS JOIN LATERAL (SELECT ext_pgcrypto.digest(ext_pgcrypto.hmac('Client Key', digest_key, 'sha256'), 'sha256') AS stored_key, ext_pgcrypto.hmac('Server Key', digest_key, 'sha256') AS server_key) AS check_password_part\n    WHERE rolpassword IS NOT NULL\n      AND pg_authid.rolname = check_user_pass.username\n      -- verify password:\n      AND check_password_part.stored_key = stored_password_part.stored_key\n      AND check_password_part.server_key = stored_password_part.server_key;\n  $$;\n\n  ALTER FUNCTION basic_auth.check_user_pass(username text, password text) OWNER TO postgres;\n\n\n\nPublic User Interface\n---------------------\n\nAnalogous to :ref:`sql_user_management`, we create a login function which takes a username and password and returns a JWT if the credentials match a user in the internal table.\nHere we use the username instead of the email address to identify a user.\n\n\nLogins\n~~~~~~\n\nAs described in :ref:`jwt-from-sql`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.\n\n\n.. code-block:: postgres\n\n  -- if you are not using psql, you need to replace :DBNAME with the current database's name.\n  ALTER DATABASE :DBNAME SET \"app.jwt_secret\" to 'reallyreallyreallyreallyverysafe';\n\n\n  CREATE FUNCTION public.login(username text, password text, OUT token text)\n      LANGUAGE plpgsql security definer\n      AS $$\n  DECLARE\n    _role name;\n  BEGIN\n    -- check email and password\n    SELECT basic_auth.check_user_pass(username, password) INTO _role;\n    IF _role IS NULL THEN\n      RAISE invalid_password USING message = 'invalid user or password';\n    END IF;\n    --\n    SELECT ext_pgjwt.sign(\n        row_to_json(r), current_setting('app.jwt_secret')\n      ) AS token\n      FROM (\n        SELECT login.username as role,\n          extract(epoch FROM now())::integer + 60*60 AS exp\n      ) r\n      INTO token;\n  END;\n  $$;\n\n  ALTER FUNCTION public.login(username text, password text) OWNER TO postgres;\n\n\n\nPermissions\n~~~~~~~~~~~\n\nAnalogous to :ref:`sql_user_management`:\nYour database roles need access to the schema, tables, views and functions in order to service HTTP requests.\nRecall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and\nanonymous roles. Below is an example of permissions that allow anonymous users to attempt to log in.\n\n\n.. code-block:: postgres\n\n  CREATE ROLE anon NOINHERIT;\n  CREATE role authenticator NOINHERIT LOGIN PASSWORD 'secret';\n  GRANT anon TO authenticator;\n\n  GRANT EXECUTE ON FUNCTION public.login(username text, password text) TO anon;\n\n\nSince the above :code:`login` function is defined as `security definer <https://www.postgresql.org/docs/current/sql-createfunction.html#id-1.9.3.67.10.2>`_,\nthe anonymous user :code:`anon` doesn't need permission to access the table :code:`pg_catalog.pg_authid` .\n:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details.\n\nChoose a secure password for role :code:`authenticator`.\nDo not forget to configure PostgREST to use the :code:`authenticator` user to connect, and to use the :code:`anon` user as anonymous user.\n\n\nTesting\n-------\n\nLet us create a sample user:\n\n.. code-block:: postgres\n\n  CREATE ROLE foo PASSWORD 'bar';\n\n\nTest at the SQL level\n~~~~~~~~~~~~~~~~~~~~~\n\nExecute:\n\n.. code-block:: postgres\n\n  SELECT * FROM public.login('foo', 'bar');\n\n\nThis should return a single scalar field like:\n\n::\n\n                                                              token\n  -----------------------------------------------------------------------------------------------------------------------------\n  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZm9vIiwiZXhwIjoxNjY4MTg4ODQ3fQ.idBBHuDiQuN_S7JJ2v3pBOr9QypCliYQtCgwYOzAqEk\n  (1 row)\n\n\nTest at the REST level\n~~~~~~~~~~~~~~~~~~~~~~\nAn API request to call this function would look like:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/login\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"username\": \"foo\", \"password\": \"bar\" }'\n\nThe response would look like the snippet below. Try decoding the token at `jwt.io <https://jwt.io/>`_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!)\n\n.. code:: json\n\n  {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VwcCIsImV4cCI6MTY2ODE4ODQzN30.WSytcouNMQe44ZzOQit2AQsqTKFD5mIvT3z2uHwdoYY\"\n  }\n\n\n\nA more sophisticated test at the REST level\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nLet's add a table, intended for the :code:`foo` user:\n\n\n.. code-block:: postgres\n\n  CREATE TABLE public.foobar(foo int, bar text, baz float);\n  ALTER TABLE public.foobar owner TO postgres;\n\n\nNow try to get the table's contents with:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/foobar\"\n\n\nThis should fail --- of course, we haven't specified the user, thus PostgREST falls back to the :code:`anon` user and denies access.\nAdd an :code:`Authorization` header. Please use the token value from the login function call above instead of the one provided below.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/foobar\" \\\n    -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZm9vIiwiZXhwIjoxNjY4MTkyMjAyfQ.zzdHCBjfkqDQLQ8D7CHO3cIALF6KBCsfPTWgwhCiHCY\"\n\n\nThis will fail again --- we get :code:`Permission denied to set role`. We forgot to allow the authenticator role to switch into this user by executing:\n\n.. code-block:: postgres\n\n  GRANT foo TO authenticator;\n\n\nRe-execute the last REST request. We fail again --- we also forgot to grant permissions for :code:`foo` on the table. Execute:\n\n.. code-block:: postgres\n\n   GRANT SELECT ON TABLE public.foobar TO foo;\n\nNow the REST request should succeed. An empty JSON array :code:`[]` is returned.\n"
  },
  {
    "path": "docs/how-tos/sql-user-management.rst",
    "content": ".. _sql_user_management:\n\nSQL User Management\n===================\n\nAs mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It's also possible to support logins entirely through SQL. It's a fair bit of work, so get ready.\n\nStoring Users and Passwords\n---------------------------\n\nThe following table, functions, and triggers will live in a :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information.\n\nFirst we'll need a table to keep track of our users:\n\n.. code:: sql\n\n  -- We put things inside the basic_auth schema to hide\n  -- them from public view. Certain public procs/views will\n  -- refer to helpers and tables inside.\n\n  create table\n  basic_auth.users (\n    email    text primary key check ( email ~* '^.+@.+\\..+$' ),\n    pass     text not null check (length(pass) < 512),\n    role     name not null check (length(role) < 512)\n  );\n\nWe would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it.\n\n.. code-block:: postgres\n\n  create function\n  basic_auth.check_role_exists() returns trigger as $$\n  begin\n    if not exists (select 1 from pg_roles as r where r.rolname = new.role) then\n      raise foreign_key_violation using message =\n        'unknown database role: ' || new.role;\n      return null;\n    end if;\n    return new;\n  end\n  $$ language plpgsql;\n\n  create constraint trigger ensure_user_role_exists\n    after insert or update on basic_auth.users\n    for each row\n    execute procedure basic_auth.check_role_exists();\n\nNext we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table.\n\n.. code-block:: postgres\n\n  create extension pgcrypto;\n\n  create function\n  basic_auth.encrypt_pass() returns trigger as $$\n  begin\n    if tg_op = 'INSERT' or new.pass <> old.pass then\n      new.pass = crypt(new.pass, gen_salt('bf'));\n    end if;\n    return new;\n  end\n  $$ language plpgsql;\n\n  create trigger encrypt_pass\n    before insert or update on basic_auth.users\n    for each row\n    execute procedure basic_auth.encrypt_pass();\n\nWith the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct.\n\n.. code-block:: postgres\n\n  create function\n  basic_auth.user_role(email text, pass text) returns name\n    language plpgsql\n    as $$\n  begin\n    return (\n    select role from basic_auth.users\n     where users.email = user_role.email\n       and users.pass = crypt(user_role.pass, users.pass)\n    );\n  end;\n  $$;\n\n.. _public_ui:\n\nPublic User Interface\n---------------------\n\nIn the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table.\n\nPermissions\n~~~~~~~~~~~\n\nYour database roles need access to the schema, tables, views and functions in order to service HTTP requests.\nRecall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and\nanonymous roles. Below is an example of permissions that allow anonymous users to create accounts and attempt to log in.\n\n.. code-block:: postgres\n\n  create role anon noinherit;\n  create role authenticator noinherit;\n  grant anon to authenticator;\n\nThen, add ``db-anon-role`` to the configuration file to allow anonymous requests.\n\n.. code:: ini\n\n  db-anon-role = \"anon\"\n\n.. _jwt-from-sql:\n\nJWT from SQL\n~~~~~~~~~~~~\n\nYou can create JWT tokens in SQL using the `pgjwt extension <https://github.com/michelp/pgjwt>`_. It's simple and requires only pgcrypto. If you're on an environment like Amazon RDS which doesn't support installing new extensions, you can still manually run the `SQL inside pgjwt <https://github.com/michelp/pgjwt/blob/master/pgjwt--0.1.1.sql>`_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need.\n\nNext write a function that returns the token. The one below returns a token with a hard-coded role, which expires five minutes after it was issued. Note this function has a hard-coded secret as well.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION jwt_test(OUT token text) AS $$\n    SELECT public.sign(\n      row_to_json(r), 'reallyreallyreallyreallyverysafe'\n    ) AS token\n    FROM (\n      SELECT\n        'my_role'::text as role,\n        extract(epoch from now())::integer + 300 AS exp\n    ) r;\n  $$ LANGUAGE sql;\n\nPostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``.\n\n.. note::\n\n  To avoid hard-coding the secret in functions, save it as a property of the database.\n\n  .. code-block:: postgres\n\n    -- run this once\n    ALTER DATABASE mydb SET \"app.jwt_secret\" TO 'reallyreallyreallyreallyverysafe';\n\n    -- then all functions can refer to app.jwt_secret\n    SELECT sign(\n      row_to_json(r), current_setting('app.jwt_secret')\n    ) AS token\n    FROM ...\n\nLogins\n~~~~~~\n\nAs described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.\n\n.. code-block:: postgres\n\n  -- login should be on your exposed schema\n  create function\n  login(email text, pass text, out token text) as $$\n  declare\n    _role name;\n  begin\n    -- check email and password\n    select basic_auth.user_role(email, pass) into _role;\n    if _role is null then\n      raise invalid_password using message = 'invalid user or password';\n    end if;\n\n    select sign(\n        row_to_json(r), 'reallyreallyreallyreallyverysafe'\n      ) as token\n      from (\n        select _role as role, login.email as email,\n           extract(epoch from now())::integer + 60*60 as exp\n      ) r\n      into token;\n  end;\n  $$ language plpgsql security definer;\n\n  grant execute on function login(text,text) to anon;\n\nSince the above :code:`login` function is defined as `security definer <https://www.postgresql.org/docs/current/sql-createfunction.html#id-1.9.3.67.10.2>`_,\nthe anonymous user :code:`anon` doesn't need permission to read the :code:`basic_auth.users` table. It doesn't even need permission to access the :code:`basic_auth` schema.\n:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details.\n\nAn API request to call this function would look like:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/login\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"email\": \"foo@bar.com\", \"pass\": \"foobar\" }'\n\nThe response would look like the snippet below. Try decoding the token at `jwt.io <https://jwt.io/>`_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!)\n\n.. code:: json\n\n  {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno\"\n  }\n\n\nAlternatives\n~~~~~~~~~~~~\n\nSee the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`.\n"
  },
  {
    "path": "docs/how-tos/working-with-postgresql-data-types.rst",
    "content": ".. _working_with_types:\n\nWorking with PostgreSQL data types\n==================================\n\n:author: `Laurence Isla <https://github.com/laurenceisla>`_\n\nPostgREST makes use of PostgreSQL string representations to work with data types. Thanks to this, you can use special values, such as ``now`` for timestamps, ``yes`` for booleans or time values including the time zones. This page describes how you can take advantage of these string representations and some alternatives to perform operations on different PostgreSQL data types.\n\n.. contents::\n  :local:\n  :depth: 1\n\n.. NOTE: Titles are ordered alphabetically. New entries should respect this order.\n\nArrays\n------\n\nTo handle `array types <https://www.postgresql.org/docs/current/arrays.html>`_ you can use string representation or JSON array format.\n\n.. code-block:: postgres\n\n  create table movies (\n    id int primary key,\n    title text not null,\n    tags text[],\n    performance_times time[]\n  );\n\nYou can insert a new value using string representation.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/movies\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"id\": 1,\n      \"title\": \"Paddington\",\n      \"tags\": \"{family,comedy,not streamable}\",\n      \"performance_times\": \"{12:40,15:00,20:00}\"\n    }\n  EOF\n\nOr you could send the same data using JSON array format:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/movies\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"id\": 1,\n      \"title\": \"Paddington\",\n      \"tags\": [\"family\", \"comedy\", \"not streamable\"],\n      \"performance_times\": [\"12:40\", \"15:00\", \"20:00\"]\n    }\n  EOF\n\nTo query the data you can use arrow operators. See :ref:`composite_array_columns`.\n\nMultidimensional Arrays\n~~~~~~~~~~~~~~~~~~~~~~~\n\nSimilarly to one-dimensional arrays, both the string representation and JSON array format are allowed.\n\n.. code-block:: postgres\n\n  -- This new column stores the cinema, floor and auditorium numbers in that order\n  alter table movies\n  add column cinema_floor_auditorium int[][][];\n\nYou can now update the item using JSON array format:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/movies?id=eq.1\" \\\n    -X PATCH -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"cinema_floor_auditorium\": [ [ [1,2], [6,7] ], [ [3,5], [8,9] ] ]\n    }\n  EOF\n\nThen, for example, to query the auditoriums that are located in the first cinema (position 0 in the array) and on the second floor (position 1 in the next inner array), we can use the arrow operators this way:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/movies?select=title,auditorium:cinema_floor_auditorium->0->1&id=eq.1\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"Paddington\",\n      \"auditorium\": [6,7]\n    }\n  ]\n\nBytea\n-----\n\nTo send raw binary to PostgREST you need a function with a single unnamed parameter of `bytea type <https://www.postgresql.org/docs/current/datatype-binary.html>`_.\n\n.. code-block:: postgres\n\n   create table files (\n     id int primary key generated always as identity,\n     file bytea\n   );\n\n   create function upload_binary(bytea) returns void as $$\n     insert into files (file) values ($1);\n   $$ language sql;\n\nLet's download the PostgREST logo for our test.\n\n.. code-block:: bash\n\n   curl \"https://postgrest.org/en/latest/_images/logo.png\" -o postgrest-logo.png\n\nNow, to send the file ``postgrest-logo.png`` we need to set the ``Content-Type: application/octet-stream`` header in the request:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/upload_binary\" \\\n    -X POST -H \"Content-Type: application/octet-stream\" \\\n    --data-binary \"@postgrest-logo.png\"\n\nTo get the image from the database, use :ref:`custom_media` like so:\n\n.. code-block:: postgres\n\n  create domain \"image/png\" as bytea;\n\n  create or replace get_image(id int) returns \"image/png\" as $$\n    select file from files where id = $1;\n  $$ language sql;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/get_image?id=1\" \\\n    -H \"Accept: image/png\"\n\nSee :ref:`providing_img` for a step-by-step example on how to handle images in HTML.\n\n.. warning::\n\n   Be careful when saving binaries in the database, having a separate storage service for these is preferable in most cases. See `Storing Binary files in the Database <https://wiki.postgresql.org/wiki/BinaryFilesInDB>`_.\n\nComposite Types\n---------------\n\nWith PostgREST, you have two options to handle `composite type columns <https://www.postgresql.org/docs/current/rowtypes.html>`_.\n\n.. code-block:: postgres\n\n  create type dimension as (\n    length decimal(6,2),\n    width decimal (6,2),\n    height decimal (6,2),\n    unit text\n  );\n\n  create table products (\n    id int primary key,\n    size dimension\n  );\n\n  insert into products (id, size)\n  values (1, '(5.0,5.0,10.0,\"cm\")');\n\nOn one hand you can insert values using string representation.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/products\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    { \"id\": 2, \"size\": \"(0.7,0.5,1.8,\\\"m\\\")\" }\n  EOF\n\nOr you could insert the same data in JSON format.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/products\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"id\": 2,\n      \"size\": {\n        \"length\": 0.7,\n        \"width\": 0.5,\n        \"height\": 1.8,\n        \"unit\": \"m\"\n      }\n    }\n  EOF\n\nYou can also query the data using arrow operators. See :ref:`composite_array_columns`.\n\nEnums\n-----\n\nYou can handle `Enumerated Types <https://www.postgresql.org/docs/current/datatype-enum.html>`_ using string representations:\n\n.. code-block:: postgres\n\n  create type letter_size as enum ('s','m','l','xl');\n\n  create table products (\n    id int primary key generated always as identity,\n    name text,\n    size letter_size\n  );\n\nTo insert or update the value use a string:\n\n.. code-block:: bash\n\n  curl -X POST \"http://localhost:3000/products\" \\\n    -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    { \"name\": \"t-shirt\", \"size\": \"l\" }\n  EOF\n\nYou can then query and filter the enum using the compatible :ref:`operators <operators>`.\nFor example, to get all the products larger than `m` and ordering them by their size:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/products?select=name,size&size=gt.m&order=size\"\n\n.. code-block:: json\n\n  [\n    {\n      \"name\": \"t-shirt\",\n      \"size\": \"l\"\n    },\n    {\n      \"name\": \"hoodie\",\n      \"size\": \"xl\"\n    }\n  ]\n\n\nhstore\n------\n\nYou can work with data types belonging to additional supplied modules such as `hstore <https://www.postgresql.org/docs/current/hstore.html>`_.\n\n.. code-block:: postgres\n\n  -- Activate the hstore module in the current database\n  create extension if not exists hstore;\n\n  create table countries (\n    id int primary key,\n    name hstore unique\n  );\n\nThe ``name`` column will have the name of the country in different formats. You can insert values using the string representation for that data type:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/countries\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    [\n      { \"id\": 1, \"name\": \"common => Egypt, official => \\\"Arab Republic of Egypt\\\", native => مصر\" },\n      { \"id\": 2, \"name\": \"common => Germany, official => \\\"Federal Republic of Germany\\\", native => Deutschland\" }\n    ]\n  EOF\n\nNotice that the use of ``\"`` in the value of the ``name`` column needs to be escaped using a backslash ``\\``.\n\nYou can also query and filter the value of a ``hstore`` column using the arrow operators, as you would do for a :ref:`JSON column<json_columns>`. For example, if you want to get the native name of Egypt:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/countries?select=name->>native&name->>common=like.Egypt\"\n\n.. code-block:: json\n\n  [{ \"native\": \"مصر\" }]\n\nJSON\n----\n\nTo work with a ``json`` type column, you can handle the value as a JSON object.\n\n.. code-block:: postgres\n\n  create table products (\n    id int primary key,\n    name text unique,\n    extra_info json\n  );\n\nYou can insert a new product using a JSON object for the ``extra_info`` column:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/products\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"id\": 1,\n      \"name\": \"Canned fish\",\n      \"extra_info\": {\n        \"expiry_date\": \"2025-12-31\",\n        \"exportable\": true\n      }\n    }\n  EOF\n\nTo query and filter the data see :ref:`json_columns` for a complete reference.\n\n.. _ww_postgis:\n\nPostGIS\n-------\n\nYou can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).\n\n.. code-block:: postgres\n\n  -- Activate the postgis module in the current database\n  create extension if not exists postgis;\n\n  create table coverage (\n    id int primary key,\n    name text unique,\n    area geometry\n  );\n\nTo add areas in polygon format, you can use string representation:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/coverage\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    [\n      { \"id\": 1, \"name\": \"small\", \"area\": \"SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))\" },\n      { \"id\": 2, \"name\": \"big\", \"area\": \"SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))\" }\n    ]\n  EOF\n\nNow, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/coverage\" \\\n    -H \"Accept: application/geo+json\"\n\n.. code-block:: json\n\n  {\n    \"type\": \"FeatureCollection\",\n    \"features\": [\n      {\n        \"type\": \"Feature\",\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [[0,0],[1,0],[1,1],[0,1],[0,0]]\n          ]\n        },\n        \"properties\": {\n          \"id\": 1,\n          \"name\": \"small\"\n        }\n      },\n      {\n        \"type\": \"Feature\",\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [[0,0],[10,0],[10,10],[0,10],[0,0]]\n          ]\n        },\n        \"properties\": {\n          \"id\": 2,\n          \"name\": \"big\"\n        }\n      }\n    ]\n  }\n\nIf you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.\n\n.. code-block:: postgres\n\n  alter table coverage\n    add square_units double precision generated always as ( st_area(area) ) stored;\n\nIn the case that you are using older PostGIS versions, then creating a function is your best option:\n\n.. code-block:: postgres\n\n  create or replace function coverage_geo_collection() returns json as $$\n    select\n      json_build_object(\n        'type', 'FeatureCollection',\n        'features', json_agg(\n          json_build_object(\n            'type', 'Feature',\n            'geometry', st_AsGeoJSON(c.area)::json,\n            'properties', json_build_object('id', c.id, 'name', c.name)\n          )\n        )\n      )\n    from coverage c;\n  $$ language sql;\n\nNow this query will return the same results:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/coverage_geo_collection\"\n\n.. code-block:: json\n\n  {\n    \"type\": \"FeatureCollection\",\n    \"features\": [\n      {\n        \"type\": \"Feature\",\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [[0,0],[1,0],[1,1],[0,1],[0,0]]\n          ]\n        },\n        \"properties\": {\n          \"id\": 1,\n          \"name\": \"small\"\n        }\n      },\n      {\n        \"type\": \"Feature\",\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [[0,0],[10,0],[10,10],[0,10],[0,0]]\n          ]\n        },\n        \"properties\": {\n          \"id\": 2,\n          \"name\": \"big\"\n        }\n      }\n    ]\n  }\n\nRanges\n------\n\nPostgREST allows you to handle `ranges <https://www.postgresql.org/docs/current/rangetypes.html>`_.\n\n.. code-block:: postgres\n\n   create table events (\n     id int primary key,\n     name text unique,\n     duration tsrange\n   );\n\nTo insert a new event, specify the ``duration`` value as a string representation of the ``tsrange`` type:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/events\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"id\": 1,\n      \"name\": \"New Year's Party\",\n      \"duration\": \"['2022-12-31 11:00','2023-01-01 06:00']\"\n    }\n  EOF\n\nYou can use range :ref:`operators <operators>` to filter the data. But, in this case, requesting a filter like ``events?duration=cs.2023-01-01`` will return an error, because PostgreSQL needs an explicit cast from string to timestamp. A workaround is to use a range starting and ending in the same date:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/events?duration=cs.\\[2023-01-01,2023-01-01\\]\"\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 1,\n      \"name\": \"New Year's Party\",\n      \"duration\": \"[\\\"2022-12-31 11:00:00\\\",\\\"2023-01-01 06:00:00\\\"]\"\n    }\n  ]\n\n.. _casting_range_to_json:\n\nCasting a Range to a JSON Object\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs you may have noticed, the ``tsrange`` value is returned as a string literal. To return it as a JSON value, first you need to create a function that will do the conversion from a ``tsrange`` type:\n\n.. code-block:: postgres\n\n   create or replace function tsrange_to_json(tsrange) returns json as $$\n     select json_build_object(\n       'lower', lower($1)\n     , 'upper', upper($1)\n     , 'lower_inc', lower_inc($1)\n     , 'upper_inc', upper_inc($1)\n     );\n   $$ language sql;\n\nThen, create the cast using this function:\n\n.. code-block:: postgres\n\n   create cast (tsrange as json) with function tsrange_to_json(tsrange) as assignment;\n\nFinally, do the request :ref:`casting the range column <casting_columns>`:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/events?select=id,name,duration::json\"\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 1,\n      \"name\": \"New Year's Party\",\n      \"duration\": {\n        \"lower\": \"2022-12-31T11:00:00\",\n        \"upper\": \"2023-01-01T06:00:00\",\n        \"lower_inc\": true,\n        \"upper_inc\": true\n      }\n    }\n  ]\n\n.. note::\n\n   If you don't want to modify casts for built-in types, an option would be to `create a custom type <https://www.postgresql.org/docs/current/sql-createtype.html>`_\n   for your own ``tsrange`` and add its own cast.\n\n   .. code-block:: postgres\n\n      create type mytsrange as range (subtype = timestamp, subtype_diff = tsrange_subdiff);\n\n      -- define column types and casting function analogously to the above example\n      -- ...\n\n      create cast (mytsrange as json) with function mytsrange_to_json(mytsrange) as assignment;\n\nTimestamps\n----------\n\nYou can use the **time zone** to filter or send data if needed.\n\n.. code-block:: postgres\n\n  create table reports (\n    id int primary key\n    , due_date timestamptz\n  );\n\nSuppose you are located in Sydney and want create a report with the date in the local time zone. Your request should look like this:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/reports\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '[{ \"id\": 1, \"due_date\": \"2022-02-24 11:10:15 Australia/Sydney\" },{ \"id\": 2, \"due_date\": \"2022-02-27 22:00:00 Australia/Sydney\" }]'\n\nSomeone located in Cairo can retrieve the data using their local time, too:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/reports?due_date=eq.2022-02-24+02:10:15+Africa/Cairo\"\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 1,\n      \"due_date\": \"2022-02-23T19:10:15-05:00\"\n    }\n  ]\n\nThe response has the date in the time zone configured by the server: ``UTC -05:00`` (see :ref:`prefer_timezone`).\n\nYou can use other comparative filters and also all the `PostgreSQL special date/time input values <https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-SPECIAL-TABLE>`_ as illustrated in this example:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/reports?or=(and(due_date.gte.today,due_date.lte.tomorrow),and(due_date.gt.-infinity,due_date.lte.epoch))\"\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 2,\n      \"due_date\": \"2022-02-27T06:00:00-05:00\"\n    }\n  ]\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. title:: PostgREST Documentation\n\nPostgREST Documentation\n=======================\n\n.. container:: image-container\n\n  .. figure:: ../static/postgrest.png\n\n.. image:: https://img.shields.io/github/stars/postgrest/postgrest.svg?style=social\n  :target: https://github.com/PostgREST/postgrest\n\n.. image:: https://img.shields.io/github/v/release/PostgREST/postgrest.svg\n  :target: https://github.com/PostgREST/postgrest/releases\n\n.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg\n  :target: https://hub.docker.com/r/postgrest/postgrest/\n\n.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854\n  :target: https://www.patreon.com/postgrest\n\n|\n\nPostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.\n\nSponsors\n--------\n\n.. container:: image-container\n\n  .. container:: img-dark\n\n    .. image:: ../static/cybertec-dark.svg\n      :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest\n\n  .. container:: img-light\n\n    .. image:: ../static/cybertec.svg\n      :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest\n\n  .. container:: img-dark\n\n    .. image:: ../static/supabase-dark.svg\n      :target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage\n\n  .. container:: img-light\n\n    .. image:: ../static/supabase.svg\n      :target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage\n\n  .. container:: img-dark\n\n    .. image:: ../static/euronodes.svg\n      :target: https://www.euronodes.com/postgrest\n\n  .. container:: img-light\n\n    .. image:: ../static/euronodes.svg\n      :target: https://www.euronodes.com/postgrest\n\n  |\n\n  .. container:: img-dark\n\n    .. image:: ../static/neon-dark.jpg\n      :target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest\n\n  .. container:: img-light\n\n    .. image:: ../static/neon.jpg\n      :target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest\n\n  .. container:: img-dark\n\n    .. image:: ../static/bytebase-dark.svg\n      :target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest\n\n  .. container:: img-light\n\n    .. image:: ../static/bytebase.svg\n      :target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest\n\n  .. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround\n     to create space and center the logos. It's not easy to layout with restructuredText.\n\n  .. image:: _static/empty.png\n    :target: #sponsors\n\n|\n\nDatabase as Single Source of Truth\n----------------------------------\n\nUsing PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.\n\nDeclarative Programming\n-----------------------\n\nIt's easier to ask PostgreSQL to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to database objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier to set constraints than to litter code with sanity checks.\n\nLeak-proof Abstraction\n----------------------\n\nThere is no ORM involved. Creating new views happens in SQL with known performance implications. A database administrator can now create an API from scratch with no custom programming.\n\nOne Thing Well\n--------------\n\nPostgREST has a focused scope. It works well with other tools like Nginx. This forces you to cleanly separate the data-centric CRUD operations from other concerns. Use a collection of sharp tools rather than building a big ball of mud.\n\nGetting Support\n----------------\n\nThe project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.\n\nReleases\n--------\n\nPostgREST follows ``MAJOR.PATCH`` two-part versioning:\n\n- ``MAJOR``: feature release, may deprecate or remove things.\n- ``PATCH``: fix/security release only; no features, no behavior changes.\n\nStarting from ``v14.0``, only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.\n\nAll the releases are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.\n\nTutorials\n---------\n\nAre you new to PostgREST? This is the place to start!\n\n.. toctree::\n   :glob:\n   :caption: Tutorials\n   :maxdepth: 1\n\n   tutorials/*\n\nAlso have a look at :ref:`install` and :ref:`community_tutorials`.\n\nReferences\n----------\n\nTechnical references for PostgREST's functionality.\n\n.. toctree::\n   :glob:\n   :caption: References\n   :name: references\n   :maxdepth: 1\n\n   references/auth.rst\n   references/api.rst\n   references/cli.rst\n   references/transactions.rst\n   references/connection_pool.rst\n   references/schema_cache.rst\n   references/errors.rst\n   references/configuration.rst\n   references/observability.rst\n   references/*\n\nExplanations\n------------\n\nKey concepts in PostgREST.\n\n.. toctree::\n   :glob:\n   :caption: Explanations\n   :name: explanations\n   :maxdepth: 1\n\n   explanations/*\n\nHow-tos\n-------\n\nRecipes that'll help you address specific use-cases.\n\n.. toctree::\n   :glob:\n   :caption: How-to guides\n   :name: how-tos\n   :maxdepth: 1\n\n   how-tos/sql-user-*\n   how-tos/working-*\n   how-tos/*\n\n.. _intgrs:\n\nIntegrations\n------------\n\n.. toctree::\n   :glob:\n   :caption: Integrations\n   :name: integrations\n   :maxdepth: 1\n\n   integrations/*\n\nEcosystem\n---------\n\nPostgREST has a growing ecosystem of examples, libraries, and experiments. Here is a selection.\n\n.. toctree::\n   :caption: Ecosystem\n   :name: ecosystem\n   :maxdepth: 1\n\n   ecosystem.rst\n\nIn Production\n-------------\n\nHere are some companies that use PostgREST in production.\n\n* `Catarse <https://www.catarse.me>`_\n* `Drip Depot <https://www.dripdepot.com>`_\n* `Image-charts <https://www.image-charts.com>`_\n* `Netwo <https://www.netwo.io>`_\n* `Nimbus <https://www.nimbusfacility.com/sg/home>`_\n  - See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.\n* `OpenBooking <https://openbooking.ch>`_\n* `Supabase <https://supabase.com>`_\n\nTestimonials\n------------\n\n  \"It's so fast to develop, it feels like cheating!\"\n\n  -- François-Guillaume Ribreau\n\n  \"I just have to say that, the CPU/Memory usage compared to our\n  Node.js/Waterline ORM based API is ridiculous.  It's hard to even push\n  it over 60/70 MB while our current API constantly hits 1GB running on 6\n  instances (dynos).\"\n\n  -- Louis Brauer\n\n  \"I really enjoyed the fact that all of a sudden I was writing\n  microservices in SQL DDL (and v8 JavaScript functions). I dodged so\n  much boilerplate. The next thing I knew, we pulled out a full rewrite\n  of a Spring+MySQL legacy app in 6 months. Literally 10x faster, and\n  code was super concise. The old one took 3 years and a team of 4\n  people to develop.\"\n\n  -- Simone Scarduzio\n\n  \"I like the fact that PostgREST does one thing, and one thing well.\n  While PostgREST takes care of bridging the gap between our HTTP server\n  and PostgreSQL database, we can focus on the development of our API in\n  a single language: SQL. This puts the database in the center of our\n  architecture, and pushed us to improve our skills in SQL programming\n  and database design.\"\n\n  -- Eric Bréchemier, Data Engineer, eGull SAS\n\n  \"PostgREST is performant, stable, and transparent. It allows us to\n  bootstrap projects really fast, and to focus on our data and application\n  instead of building out the ORM layer. In our k8s cluster, we run a few\n  pods per schema we want exposed, and we scale up/down depending on demand.\n  Couldn't be happier.\"\n\n  -- Anupam Garg, Datrium, Inc.\n\nContributing\n------------\n\nPlease see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md>`_ in the main PostgREST repository.\n\n.. raw:: html\n\n  <script type=\"text/javascript\">\n    let hash = window.location.hash;\n\n    const redirects = {\n      // Tables and Views\n      '#release-notes': '#releases',\n    };\n\n    let willRedirectTo = redirects[hash];\n\n    if (willRedirectTo) {\n      window.location.href = willRedirectTo;\n    }\n  </script>\n"
  },
  {
    "path": "docs/integrations/pg-safeupdate.rst",
    "content": "pg-safeupdate\n#############\n\n.. _block_fulltable:\n\nBlock Full-Table Operations\n---------------------------\n\nIf the :ref:`active role <user_impersonation>` can delete table rows then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/logs?time=lt.1991-08-06\" -X DELETE\n\nNote that it's very easy to delete the **entire table** by omitting the query parameter!\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/logs\" -X DELETE\n\nThis can happen accidentally such as by switching a request from a GET to a DELETE. To protect against accidental operations use the `pg-safeupdate <https://github.com/eradman/pg-safeupdate>`_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN <https://pgxn.org/>`_ network:\n\n.. code-block:: bash\n\n  sudo -E pgxn install safeupdate\n\n  # then add this to postgresql.conf:\n  # shared_preload_libraries='safeupdate';\n\nThis does not protect against malicious actions, since someone can add a url parameter that does not affect the result set. To prevent this you must turn to database permissions, forbidding the wrong people from deleting rows, and using `row-level security <https://www.postgresql.org/docs/current/ddl-rowsecurity.html>`_ if finer access control is required.\n"
  },
  {
    "path": "docs/integrations/systemd.rst",
    "content": "systemd\n=======\n\nFor Linux distributions that use **systemd** (Ubuntu, Debian, Arch Linux) you can create a daemon in the following way.\n\nFirst, create postgrest configuration in ``/etc/postgrest/config``\n\n.. code-block:: ini\n\n  db-uri = \"postgres://<your_user>:<your_password>@localhost:5432/<your_db>\"\n  db-schemas = \"<your_exposed_schema>\"\n  db-anon-role = \"<your_anon_role>\"\n  jwt-secret = \"<your_secret>\"\n\nCreate a dedicated  ``postgrest`` user with:\n\n.. code-block:: ini\n\n  sudo useradd -M -U -d /nonexistent -s /usr/sbin/nologin postgrest\n\nThen create the systemd service file in ``/etc/systemd/system/postgrest.service``\n\n.. code-block:: ini\n\n  [Unit]\n  Description=REST API for any PostgreSQL database\n  After=postgresql.service\n\n  [Service]\n  User=postgrest\n  Group=postgrest\n  ExecStart=/bin/postgrest /etc/postgrest/config\n  ExecReload=/bin/kill -SIGUSR1 $MAINPID\n\n  [Install]\n  WantedBy=multi-user.target\n\nAfter that, you can enable the service at boot time and start it with:\n\n.. code-block:: bash\n\n  systemctl enable postgrest\n  systemctl start postgrest\n\n  ## For reloading the service\n  ## systemctl restart postgrest\n\n.. _file_descriptors:\n\nFile Descriptors\n----------------\n\nFile descriptors are kernel resources that are used by HTTP connections (among others). File descriptors are limited per process. The kernel default limit is 1024, which is increased in some Linux distributions.\nWhen under heavy traffic, PostgREST can reach this limit and start showing ``No file descriptors available`` errors. To clear these errors, you can increase the process' file descriptor limit.\n\n.. code-block:: ini\n\n  [Service]\n  LimitNOFILE=10000\n"
  },
  {
    "path": "docs/postgrest.dict",
    "content": "personal_ws-1.1 en 0 utf-8\napi\nAPI's\nAPIs\nAPISIX\nAST\nasync\naud\nAuth\nauth\nauthenticator\nbackoff\nbooleans\nBOM\nBytea\nCardano\ncd\nCDNs\ncentric\nCLI\nCMS\ncoercible\nconf\nCloudflare\nconfig\ncors\nCORS\ncryptographically\nCSV\ndurations\nDDL\nDOM\nDSL\nDevOps\nDramatiq\ndockerize\nenum\nEnums\nEntra\neq\nETH\nEthereum\nEveryLayout\nfilename\nFreeBSD\nfts\nfullstack\nGeoJSON\nGithub\nGoogle\ngrantor\nGraphQL\ngte\nGUC\nHaskell\nHMAC\nhtmx\nHtmx\nHomebrew\nhstore\nHTTP\nHTTPS\nHV\nInlining\ninlined\nIntegrations\nidletime\nIDLETIME\nilike\nimatch\nio\nIP\nisdistinct\nJS\njs\nJSON\nJSPath\nJWK\nJWT\njwt\nKeycloak\nKubernetes\nlocalhost\nlogin\nlookups\nLogins\nLIBPQ\nlogins\nlon\nlt\nlte\nmacOS\nmisprediction\nmulti\nnamespace\nnamespaced\nNanos\nneq\nnginx\nnixpkgs\nnpm\nnxl\nnxr\nOAuth\nORM\nObservability\nOkta\nOpenAPI\nopenapi\nov\nparametrized\npassphrase\nPBKDF\nPgBouncer\npgcrypto\npgjwt\npgrst\npgrstX\nPGRSTX\npgSQL\nauthid\nphfts\nphraseto\nplainto\nplfts\npoolers\nPostGIS\nPostgreSQL\nPostgreSQL's\nPostgREST\npostgres\npostgrest\nPostgREST's\npre\npreflight\nplpgsql\npsql\nRabbitMQ\nRDS\nreallyreallyreallyreallyverysafe\nRedux\nrefactor\nreloadable\nReloadable\nrequester's\nRESTful\nRLS\nRPC\nRSA\nsafeupdate\nsavepoint\nschemas\nschema's\nSHA\nsignup\nSIGUSR\nsl\nSQL\nsql\nSQLSTATE\nsr\nSSL\nstateful\nstdout\nsupervisees\nSvelteKit\nsystemd\ntodo\ntodos\ntos\ntsquery\ntx\nTypeScript\nUI\nui\nunicode\nunikernel\nunix\nupdatable\nunfulfillable\nunselected\nUntyped\nUPSERT\nUpsert\nupsert\nuri\nurl\nurlencoded\nurls\nvariadic\nverifier\nversioning\nVondra\nVue\nwebapp\nwebhooks\nwebsearch\nWebsockets\nwebuser\nwfts\nwww\n"
  },
  {
    "path": "docs/references/admin_server.rst",
    "content": ".. _admin_server:\n\nAdmin Server\n############\n\nPostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port`.\n\n.. _health_check:\n\nHealth Check\n============\n\nYou can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.\n\nTwo endpoints ``live`` and ``ready`` will then be available. Both these endpoints reply with a status code and empty response body.\n\n.. important::\n\n  If you have a machine with multiple network interfaces and multiple PostgREST instances in the same port, you need to specify a unique :ref:`hostname <server-host>`\n  in the configuration of each PostgREST instance for the health check to work correctly. Don't use the special values(``!4``, ``*``, etc) in this case because the health check\n  could report a false positive.\n\nLive\n----\n\nThe ``live`` endpoint verifies if PostgREST is running on its configured port. A request will return ``200 OK`` if PostgREST is alive or ``500`` otherwise.\n\nFor instance, to verify if PostgREST is running while the ``admin-server-port`` is set to ``3001``:\n\n.. code-block:: bash\n\n  curl -I \"http://localhost:3001/live\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n\nReady\n-----\n\nAdditionally to the ``live`` check, the ``ready`` endpoint checks the state of the :ref:`connection_pool` and the :ref:`schema_cache`. A request will return ``200 OK`` if both are good or ``503`` if not.\n\n.. code-block:: bash\n\n  curl -I \"http://localhost:3001/ready\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n\nPostgREST will try to recover from the ``503`` state with :ref:`automatic_recovery`.\n\nMetrics\n=======\n\nProvides :ref:`metrics`.\n\nRuntime Schema Cache\n====================\n\nProvides the ``schema_cache`` endpoint that prints the runtime :ref:`schema_cache`.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3001/schema_cache\"\n\n.. code-block:: json\n\n  {\n    \"dbMediaHandlers\": [\"...\"],\n    \"dbRelationships\": [\"...\"],\n    \"dbRepresentations\": [\"...\"],\n    \"dbRoutines\": [\"...\"],\n    \"dbTables\": [\"...\"],\n    \"dbTimezones\": [\"...\"]\n  }\n"
  },
  {
    "path": "docs/references/api/aggregate_functions.rst",
    "content": ".. _aggregate_functions:\n\nAggregate Functions\n###################\n\nPostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``.\nPlease refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.\n\n.. note::\n Aggregate functions are *disabled* by default in PostgREST, because they can create performance problems without appropriate safeguards.\n See :ref:`db-aggregates-enabled` for further details.\n\nTo use an aggregate function, append it to a column in the ``select`` parameter, like so:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=amount.sum()\"\n\nThis will return a ``sum`` of all the values of the ``amount`` column in a single row:\n\n.. code-block:: json\n\n  [\n    {\n      \"sum\": 1234.56\n    }\n  ]\n\nYou can ``select`` multiple aggregate functions at the same time (you may need to :ref:`rename them <renaming_columns>` to disambiguate).\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=total_amount:amount.sum(),avg_amount:amount.avg(),total_quantity:quantity.sum()\"\n\n.. note::\n  Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`.\n  However they are not compatible with :ref:`domain_reps` for the moment.\n  Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.\n\nAutomatic ``GROUP BY``\n======================\n\nIn SQL, a ``GROUP BY`` clause is required to aggregate the selected columns.\nHowever, PostgREST handles grouping automatically if the columns are already present in the ``select`` parameter.\nFor instance:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date\"\n\nThis will get the sum and average of the amounts grouped by each unique value in the ``order_date`` column:\n\n.. code-block:: json\n\n  [\n    {\n      \"sum\": 1234.56,\n      \"avg\": 123.45,\n      \"order_date\": \"2023-01-01\"\n    },\n    {\n      \"sum\": 2345.67,\n      \"avg\": 234.56,\n      \"order_date\": \"2023-01-02\"\n    }\n  ]\n\nThe ``count()`` Aggregate\n=========================\n\n.. note::\n  Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter.\n  While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended**.\n  Please use ``count()`` (with parentheses) instead.\n\n``count()`` is a special case because it can be used with or without an aggregated column. For example:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=count(),observation_count:observation.count(),order_date\"\n\n.. code-block:: json\n\n  [\n    {\n      \"count\": 4,\n      \"observation_count\": 2,\n      \"order_date\": \"2023-01-01\"\n    },\n    {\n      \"count\": 2,\n      \"observation_count\": 1,\n      \"order_date\": \"2023-01-02\"\n    }\n  ]\n\nNote that there is a difference between the result of ``count()`` and ``observation.count()``.\nThe former counts the whole row, while the latter counts the non ``NULL`` values of the ``observation`` column (both grouped by ``order_date``).\nThis is due to how PostgreSQL itself implements the ``count()`` function.\n\nCasting Aggregates\n==================\n\nIt is :ref:`possible to cast <casting_columns>` the aggregated column or the aggregate itself, or both at the same time.\n\nCasting the Aggregated Column\n-----------------------------\n\nFor example, let's say that ``orders`` has an ``order_details`` :ref:`JSON column <json_columns>` with a ``tax_amount`` key.\nWe cannot sum ``tax_amount`` directly because using ``->`` or ``->>`` will return the data in ``json`` or ``text`` format.\nSo we need to cast it to a compatible type (e.g. ``numeric``) right before the aggregate function:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()\"\n\n.. code-block:: json\n\n  [\n    {\n      \"sum\": 1234.56\n    }\n  ]\n\nCasting the Aggregate\n---------------------\n\nFor instance, if we wanted to round the average of the ``amount`` column, we could do so by casting ``avg()`` to an ``int``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=amount.avg()::int\"\n\n.. code-block:: json\n\n  [\n    {\n      \"avg\": 201\n    }\n  ]\n\nAggregates and Resource Embedding\n=================================\n\nYou can group an aggregate function by an :ref:`embedded resource <resource_embedding>` and also use the aggregates inside them.\n\nGrouping by an Embedded Resource\n--------------------------------\n\nSimilar to grouping by columns, aggregate functions can also be grouped by embedded resources.\nFor example, let's say that the ``orders`` table is related to a ``customers`` table.\nTo get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, we would do the following:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=amount.sum(),customers(name)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"sum\": 100,\n      \"customers\": {\n        \"name\": \"Customer A\"\n      }\n    },\n    {\n      \"sum\": 200,\n      \"customers\": {\n        \"name\": \"Customer B\"\n      }\n    }\n  ]\n\nThe previous example uses a \"to-one\" relationship, but this can be done on \"to-many\" relationships as well (although there are few obvious use cases).\n\nThis also works in a similar way for :ref:`spread embedded resources <spread_embed>`.\nFor example, ``select=amount.sum(),...customers(name)`` would sum the ``amount`` grouped by the ``name`` column.\n\nUsing Aggregates Inside Embedded Resources\n------------------------------------------\n\nUsing the relationship from the previous example, let's take all the ``customers`` and embed their ``orders``.\nIf we also want to get the total ``amount`` grouped by the ``order_date`` of the ``orders``, we would do the following:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/customers?select=name,city,state,orders(amount.sum(),order_date)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"name\": \"Customer A\",\n      \"city\": \"New York\",\n      \"state\": \"NY\",\n      \"orders\": [\n        {\n          \"sum\": 215.22,\n          \"order_date\": \"2023-09-01\"\n        },\n        {\n          \"sum\": 905.73,\n          \"order_date\": \"2023-09-02\"\n        }\n      ]\n    },\n    {\n      \"name\": \"Customer B\",\n      \"city\": \"Los Angeles\",\n      \"state\": \"CA\",\n      \"orders\": [\n        {\n          \"sum\": 329.71,\n          \"order_date\": \"2023-09-01\"\n        },\n        {\n          \"sum\": 425.87,\n          \"order_date\": \"2023-09-03\"\n        }\n      ]\n    }\n  ]\n\nNote that the aggregate is done within the embedded resource ``orders``.\nIt is not affected by any of the columns from the top-level relationship ``customers``.\n\nAggregates in To-One Spreads\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAll the aggregates inside a :ref:`one-to-one or many-to-one spread embedded resource <spread_to_one_embed>` will be hoisted to the top-level relationship.\nIn other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=order_date,...customers(subscription_date.max(),subscription_date.min())\n\nThis will take the ``max`` and ``min`` subscription date of every customer and group it by the ``order_date`` column:\n\n.. code-block:: json\n\n  [\n    {\n      \"order_date\": \"2023-11-01\",\n      \"max\": \"2023-10-15\",\n      \"min\": \"2013-10-01\"\n    },\n    {\n      \"order_date\": \"2023-11-02\",\n      \"max\": \"2023-10-30\",\n      \"min\": \"2016-02-11\"\n    }\n  ]\n\n.. note::\n\n  Aggregates inside to-many spreads are not supported\n"
  },
  {
    "path": "docs/references/api/computed_fields.rst",
    "content": ".. _computed_cols:\n\nComputed Fields\n###############\n\nComputed fields are virtual columns that are not stored in a table. PostgreSQL makes it possible to implement them using functions on table types.\n\n.. code-block:: postgres\n\n  CREATE TABLE people (\n    first_name text\n  , last_name  text\n  , job        text\n  );\n\n  -- a computed field that combines data from two columns\n  CREATE FUNCTION full_name(people)\n  RETURNS text AS $$\n    SELECT $1.first_name || ' ' || $1.last_name;\n  $$ LANGUAGE SQL;\n\nHorizontal Filtering on Computed Fields\n=======================================\n\n:ref:`h_filter` can be applied to computed fields. For example, we can do a :ref:`fts` on :code:`full_name`:\n\n.. code-block:: postgres\n\n  -- (optional) you can add an index on the computed field to speed up the query\n  CREATE INDEX people_full_name_idx ON people\n    USING GIN (to_tsvector('english', full_name(people)));\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?full_name=fts.Beckett\"\n\n.. code-block:: json\n\n  [\n    {\"first_name\": \"Samuel\", \"last_name\": \"Beckett\", \"job\": \"novelist\"}\n  ]\n\nVertical Filtering on Computed Fields\n=====================================\n\nComputed fields won't appear on the response by default but you can use :ref:`v_filter` to include them:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=full_name,job\"\n\n.. code-block:: json\n\n  [\n    {\"full_name\": \"Samuel Beckett\", \"job\": \"novelist\"}\n  ]\n\nOrdering on Computed Fields\n===========================\n\n:ref:`ordering` on computed fields is also possible:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?order=full_name.desc\"\n\n.. important::\n\n  Computed fields must be created in the :ref:`exposed schema <db-schemas>` or in a schema in the :ref:`extra search path <db-extra-search-path>` to be used in this way. When placing the computed field in the :ref:`exposed schema <db-schemas>` you can use an **unnamed** parameter, as in the example above, to prevent it from being exposed as an :ref:`RPC <functions>` under ``/rpc``.\n\n.. note::\n\n   - PostgreSQL 12 introduced `generated columns <https://www.postgresql.org/docs/12/ddl-generated-columns.html>`_, which can also compute a value based on other columns. However they're stored, not virtual.\n   - \"computed fields\" are documented on https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-USAGE (search for \"computed fields\")\n   - On previous PostgREST versions this feature was documented with the name of \"computed columns\".\n"
  },
  {
    "path": "docs/references/api/cors.rst",
    "content": ".. _cors:\n\nCORS\n####\n\nBy default, PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. This behavior can be configured by using :ref:`server_cors_allowed_origins`.\n\n\nIt also handles `preflight requests <https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request>`_ done by the browser, which are cached using the returned ``Access-Control-Max-Age: 86400`` header (86400 seconds = 24 hours). This is useful to reduce the latency of the subsequent requests.\n\nA ``POST`` preflight request would look like this:\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/items\" \\\n    -X OPTIONS \\\n    -H \"Origin: http://example.com\" \\\n    -H \"Access-Control-Request-Method: POST\" \\\n    -H \"Access-Control-Request-Headers: Content-Type\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Access-Control-Allow-Origin: http://example.com\n  Access-Control-Allow-Credentials: true\n  Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD\n  Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Accept-Language, Content-Language\n  Access-Control-Max-Age: 86400\n\n.. _allowed_origins:\n\nAllowed Origins\n===============\n\nWith the following config setting, PostgREST will accept CORS requests from domains :code:`http://example.com` and :code:`http://example2.com`.\n\n\n.. code-block::\n  \n  server-cors-allowed-origins=\"http://example.com, http://example2.com\"\n"
  },
  {
    "path": "docs/references/api/domain_representations.rst",
    "content": ".. _domain_reps:\n\nDomain Representations\n######################\n\nDomain Representations separates \"how the data is presented\" from \"how the data is stored\". It works by creating `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and `casts <https://www.postgresql.org/docs/current/sql-createcast.html>`_, the latter act on the former to present and receive the data in different formats.\n\n.. contents::\n   :depth: 1\n   :local:\n   :backlinks: none\n\nCustom Domain\n=============\n\nSuppose you want to use a ``uuid`` type for a primary key and want to present it shortened to web users.\n\nFor this, let's create a domain based on ``uuid``.\n\n.. code-block:: postgres\n\n  create domain app_uuid as uuid;\n\n  -- and use it as our table PK.\n  create table profiles(\n    id   app_uuid\n  , name text\n  );\n\n  -- some data for the example\n  insert into profiles values ('846c4ffd-92ce-4de7-8d11-8e29929f4ec4', 'John Doe');\n\nDomain Response Format\n======================\n\nWe can shorten the ``uuid`` with ``base64`` encoding. Let's use JSON as our response format for this example.\n\nTo change the domain format for JSON, create a function that converts ``app_uuid`` to ``json``.\n\n.. code-block:: postgres\n\n  -- the name of the function is arbitrary\n  CREATE OR REPLACE FUNCTION json(app_uuid) RETURNS json AS $$\n    select to_json(encode(uuid_send($1),'base64'));\n  $$ LANGUAGE SQL IMMUTABLE;\n\n  -- check it works\n  select json('846c4ffd-92ce-4de7-8d11-8e29929f4ec4'::app_uuid);\n              json\n  ----------------------------\n   \"hGxP/ZLOTeeNEY4pkp9OxA==\"\n\nThen create a CAST to tell PostgREST to convert it automatically whenever a JSON response is requested.\n\n.. code-block:: postgres\n\n  CREATE CAST (app_uuid AS json) WITH FUNCTION json(app_uuid) AS IMPLICIT;\n\nWith this you can obtain the data in the shortened format.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/profiles\" \\\n    -H \"Accept: application/json\"\n\n.. code-block:: json\n\n  [{\"id\":\"hGxP/ZLOTeeNEY4pkp9OxA==\",\"name\":\"John Doe\"}]\n\n.. note::\n\n  - Casts on domains are ignored by PostgreSQL, their interpretation is left to the application. We're discussing the possibility of including the Domain Representations behavior on `pgsql-hackers <https://www.postgresql.org/message-id/flat/CAGRrpzZKa%2BGu91j1SOvN3tM1f-7Gh_w441c5nAX1QqdH3Q31Lg%40mail.gmail.com>`_.\n  - It would make more sense to use ``base58`` encoding as it's URL friendly but for simplicity we use ``base64`` (supported natively in PostgreSQL).\n\n.. important::\n\n  After creating a cast over a domain, you must refresh PostgREST schema cache. See :ref:`schema_reloading`.\n\nDomain Filter Format\n====================\n\nFor :ref:`h_filter` to work with the shortened format, you need a different conversion.\n\nPostgREST considers the URL query string to be, in the most generic sense, ``text``. So let's create a function that converts ``text`` to ``app_uuid``.\n\n.. code-block:: postgres\n\n  -- the name of the function is arbitrary\n  CREATE OR REPLACE FUNCTION app_uuid(text) RETURNS app_uuid AS $$\n    select substring(decode($1,'base64')::text from 3)::uuid;\n  $$ LANGUAGE SQL IMMUTABLE;\n\n  -- plus a CAST to tell PostgREST to use this function\n  CREATE CAST (text AS app_uuid) WITH FUNCTION app_uuid(text) AS IMPLICIT;\n\nNow you can filter as usual.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/profiles?id=eq.ZLOTeeNEY4pkp9OxA==\" \\\n    -H \"Accept: application/json\"\n\n.. code-block:: json\n\n  [{\"id\":\"hGxP/ZLOTeeNEY4pkp9OxA==\",\"name\":\"John Doe\"}]\n\n.. note::\n\n  If there's no CAST from ``text`` to ``app_uuid`` defined, the filter will still work with the native uuid format (``846c4ffd-92ce-4de7-8d11-8e29929f4ec4``).\n\nDomain Request Body Format\n==========================\n\nTo accept the shortened format in a JSON request body, for example when creating a new record, define a ``json`` to ``app_uuid`` conversion.\n\n.. code-block:: postgres\n\n  -- the name of the function is arbitrary\n  CREATE OR REPLACE FUNCTION app_uuid(json) RETURNS public.app_uuid AS $$\n    -- here we reuse the previous app_uuid(text) function\n    select app_uuid($1 #>> '{}');\n  $$ LANGUAGE SQL IMMUTABLE;\n\n  CREATE CAST (json AS public.app_uuid) WITH FUNCTION app_uuid(json) AS IMPLICIT;\n\nNow we can :ref:`insert` (or :ref:`update`) as usual.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/profiles\" \\\n    -H \"Prefer: return=representation\" \\\n    -H \"Content-Type: application/json\" \\\n    -d @- <<JSON\n\n  {\"id\":\"zH7HbFJUTfy/GZpwuirpuQ==\",\"name\":\"Jane Doe\"}\n\n  JSON\n\nThe response:\n\n.. code-block:: json\n\n  [{\"id\":\"zH7HbFJUTfy/GZpwuirpuQ==\",\"name\":\"Jane Doe\"}]\n\nNote that on the database side we have our regular ``uuid`` format.\n\n.. code-block:: postgres\n\n  select * from profiles;\n\n                    id                  |   name\n  --------------------------------------+----------\n   846c4ffd-92ce-4de7-8d11-8e29929f4ec4 | John Doe\n   cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9 | Jane Doe\n  (2 rows)\n\n.. note::\n\n  If there's no CAST from ``json`` to ``app_uuid`` defined, the request body will still work with the native uuid format (``cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9``).\n\nAdvantages over Views\n=====================\n\n`Views <https://www.postgresql.org/docs/current/sql-createview.html>`_ also allow us to change the format of the underlying type. However they come with drawbacks that increase complexity.\n\n1) Formatting the column in the view makes it `non-updatable <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ since Postgres doesn't know how to reverse the transform. This can be worked around using INSTEAD OF triggers.\n2) When filtering by this column, we get full table scans for the same reason (also applies to :ref:`computed_cols`) . The performance loss here can be avoided with a computed index, or using a materialized generated column.\n3) If the formatted column is used as a foreign key, PostgREST can no longer detect that relationship and :ref:`resource_embedding` breaks. This can be worked around with :ref:`computed_relationships`.\n\nDomain Representations avoid all the above drawbacks. Their only drawback is that for existing tables, you have to change the column types. But this should be a fast operation since domains are binary coercible with their underlying types. A table rewrite won't be required.\n\n.. note::\n\n  Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``.\n\n  Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets \"how the data is presented\" dictate \"how the data is stored\" which would be backwards.\n"
  },
  {
    "path": "docs/references/api/functions.rst",
    "content": ".. _functions:\n\nFunctions as RPC\n================\n\n*\"A single resource can be the equivalent of a database function, with the power to abstract state changes over any number of storage items\"* -- `Roy T. Fielding <https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-743>`_\n\nFunctions can perform any operation allowed by PostgreSQL (read data, modify data, :ref:`raise errors <raise_error>`, and even DDL operations). Every function in the :ref:`exposed schema <schemas>` and accessible by the :ref:`active database role <roles>` is executable under the :code:`/rpc` prefix.\n\nIf they return table types, functions can:\n\n- Use all the same :ref:`read filters as Tables and Views <read>` (horizontal/vertical filtering, counts, limits, etc.).\n- Use :ref:`Resource Embedding <function_embed>`, if the returned table type has relationships to other tables.\n\n.. note::\n\n  Why the ``/rpc`` prefix? PostgreSQL allows a table or view to have the same name as a function. The prefix allows us to avoid routes collisions.\n\n.. warning::\n\n  `Stored Procedures <https://www.postgresql.org/docs/current/xproc.html>`_ are not supported.\n\nCalling with POST\n-----------------\n\nTo supply arguments in an API call, include a JSON object in the request payload. Each key/value of the object will become an argument.\n\nFor instance, assume we have created this function in the database.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION add_them(a integer, b integer)\n  RETURNS integer AS $$\n   SELECT a + b;\n  $$ LANGUAGE SQL IMMUTABLE;\n\n.. important::\n\n  Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`.\n\nThe client can call it by posting an object like\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/add_them\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"a\": 1, \"b\": 2 }'\n\n.. code-block:: json\n\n  3\n\n.. note::\n\n  PostgreSQL converts identifier names to lowercase unless you quote them like:\n\n  .. code-block:: postgres\n\n    CREATE FUNCTION \"someFunc\"(\"someParam\" text) ...\n\nCalling with GET\n----------------\n\nIf the function doesn't modify the database, it will also run under the GET method (see :ref:`access_mode`).\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/add_them?a=1&b=2\"\n\nThe function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.\n\nIf the function is defined to have default values for the parameters then arguments for these parameters can be omitted in the request. For instance:\n\n.. code-block:: postgres\n\n  CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest')\n  RETURNS TEXT AS $$\n    SELECT 'Hello ' || username || '!';\n  $$ LANGUAGE SQL IMMUTABLE;\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/rpc/greet_user\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Context-Type: application/json; charset=utf-8\n\n  \"Hello guest!\"\n\n.. _function_single_json:\n\nFunctions with an array of JSON objects\n----------------------------------------------\n\nIf you want to pass multiple JSON objects to a Postgres function (an array of objects), you can create a function with a parameter of type ``json`` or ``jsonb``.\n\nWithin the curl request, this JSON must be embedded in an object where they key matches the same name as the function's ``json`` or ``jsonb`` parameter.\nThis will allow you to loop over the array of JSON objects within the Postgres function.\n\nThis practice may allow you to reduce the number of ``curl`` requests required to accomplish a task.\n\nFor instance, assume we have created this function in the database.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION update_data(p_json jsonb)\n  RETURNS void AS $$\n  DECLARE\n    json_item json;\n  BEGIN\n    FOR json_item IN SELECT jsonb_array_elements(p_json) LOOP\n      UPDATE data_table SET data_text_column = (json_item->>'data_text')::text \n        WHERE data_int_column = (json_item->>'data_int')::integer;\n    END LOOP;\n  END;\n  $$ LANGUAGE SQL IMMUTABLE;\n\nA ``curl`` request using the POST method would look like the following:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/update_data\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"p_json\": [ { \"data_text\": \"one\", \"data_int\": \"1\" }, { \"data_text\": \"two\", \"data_int\": \"2\" } ] }'\n\nFunctions with a single unnamed JSON parameter\n----------------------------------------------\n\nIf you want the JSON request body to be sent as a single argument, you can create a function with a single unnamed ``json`` or ``jsonb`` parameter.\nFor this the ``Content-Type: application/json`` header must be included in the request.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION mult_them(json) RETURNS int AS $$\n    SELECT ($1->>'x')::int * ($1->>'y')::int\n  $$ LANGUAGE SQL;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/mult_them\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"x\": 4, \"y\": 2 }'\n\n.. code-block:: json\n\n  8\n\n.. note::\n\n If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.\n\n.. _function_single_unnamed:\n\nFunctions with a single unnamed parameter\n-----------------------------------------\n\nYou can make a POST request to a function with a single unnamed parameter to send raw ``bytea``, ``text`` or ``xml`` data.\n\nTo send raw XML, the parameter type must be ``xml`` and the header ``Content-Type: text/xml`` must be included in the request.\n\nTo send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request.\n\n.. code-block:: postgres\n\n  CREATE TABLE files(blob bytea);\n\n  CREATE FUNCTION upload_binary(bytea) RETURNS void AS $$\n    INSERT INTO files(blob) VALUES ($1);\n  $$ LANGUAGE SQL;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/upload_binary\" \\\n    -X POST -H \"Content-Type: application/octet-stream\" \\\n    --data-binary \"@file_name.ext\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n\n  [ ... ]\n\nTo send raw text, the parameter type must be ``text`` and the header ``Content-Type: text/plain`` must be included in the request.\n\n.. _functions_array:\n\nFunctions with array parameters\n-------------------------------\n\nYou can call a function that takes an array parameter:\n\n.. code-block:: postgres\n\n   create function plus_one(arr int[]) returns int[] as $$\n      SELECT array_agg(n + 1) FROM unnest($1) AS n;\n   $$ language sql;\n\n.. code-block:: bash\n\n   curl \"http://localhost:3000/rpc/plus_one\" \\\n     -X POST -H \"Content-Type: application/json\" \\\n     -d '{\"arr\": [1,2,3,4]}'\n\n.. code-block:: json\n\n   [2,3,4,5]\n\nFor calling the function with GET, you can pass the array as an `array literal <https://www.postgresql.org/docs/current/arrays.html#ARRAYS-INPUT>`_,\nas in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``).\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/plus_one?arr=%7B1,2,3,4%7D'\"\n\n.. note::\n\n   For versions prior to PostgreSQL 10, to pass a PostgreSQL native array on a POST payload, you need to quote it and use an array literal:\n\n   .. code-block:: bash\n\n     curl \"http://localhost:3000/rpc/plus_one\" \\\n       -X POST -H \"Content-Type: application/json\" \\\n       -d '{ \"arr\": \"{1,2,3,4}\" }'\n\n   In these versions we recommend using function parameters of type JSON to accept arrays from the client.\n\n.. _functions_variadic:\n\nVariadic functions\n------------------\n\nYou can call a variadic function by passing a JSON array in a POST request:\n\n.. code-block:: postgres\n\n   create function plus_one(variadic v int[]) returns int[] as $$\n      SELECT array_agg(n + 1) FROM unnest($1) AS n;\n   $$ language sql;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/plus_one\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{\"v\": [1,2,3,4]}'\n\n.. code-block:: json\n\n   [2,3,4,5]\n\nIn a GET request, you can repeat the same parameter name:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/plus_one?v=1&v=2&v=3&v=4\"\n\nRepeating also works in POST requests with ``Content-Type: application/x-www-form-urlencoded``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/plus_one\" \\\n    -X POST -H \"Content-Type: application/x-www-form-urlencoded\" \\\n    -d 'v=1&v=2&v=3&v=4'\n\n.. _table_functions:\n\nTable-Valued Functions\n----------------------\n\nA function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <function_embed>`.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION best_films_2017() RETURNS SETOF films ..\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/best_films_2017?select=title,director:directors(*)\"\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/best_films_2017?rating=gt.8&order=title.desc\"\n\n.. _function_inlining:\n\nFunction Inlining\n~~~~~~~~~~~~~~~~~\n\nA function that follows the `rules for inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_ will also inline :ref:`filters <h_filter>`, :ref:`order <ordering>` and :ref:`limits <limits>`.\n\nFor example, for the following function:\n\n.. code-block:: postgres\n\n  create function getallprojects() returns setof projects\n  language sql stable\n  as $$\n    select * from projects;\n  $$;\n\nLet's get its :ref:`explain_plan` when calling it with filters applied:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/getallprojects?id=eq.1\" \\\n    -H \"Accept: application/vnd.pgrst.plan\"\n\n.. code-block:: postgres\n\n  Aggregate  (cost=8.18..8.20 rows=1 width=112)\n    ->  Index Scan using projects_pkey on projects  (cost=0.15..8.17 rows=1 width=40)\n          Index Cond: (id = 1)\n\nNotice there's no \"Function Scan\" node in the plan, which tells us it has been inlined.\n\nHorizontal Filtering\n~~~~~~~~~~~~~~~~~~~~\n\nTable-valued functions support horizontal filtering on selected and unselected columns.\n\nFor example, the following RPC with filter on unselected column returns:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 4, \"client_id\": 2 }\n  ]\n\n.. _scalar_functions:\n\nScalar functions\n----------------\n\nPostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/add_them?a=1&b=2\"\n\n.. code-block:: json\n\n  3\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/best_films_2017\"\n\n.. code-block:: json\n\n  [\n    { \"title\": \"Okja\", \"rating\": 7.4},\n    { \"title\": \"Call me by your name\", \"rating\": 8},\n    { \"title\": \"Blade Runner 2049\", \"rating\": 8.1}\n  ]\n\nTo manually choose a return format such as binary, see :ref:`custom_media`.\n\n.. _untyped_functions:\n\nUntyped functions\n-----------------\n\nFunctions that return ``record`` or ``SETOF record`` are supported:\n\n.. code-block:: postgres\n\n  create function projects_setof_record() returns setof record as $$\n    select * from projects;\n  $$ language sql;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/projects_setof_record\"\n\n.. code-block:: json\n\n  [{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1},\n   {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1},\n   {\"id\":3,\"name\":\"IOS\",\"client_id\":2}]\n\nHowever note that they will fail when trying to use :ref:`v_filter` and :ref:`h_filter` on them.\n\nSo while they can be used for quick tests, it's recommended to always choose a strict return type for the function.\n\nOverloaded functions\n--------------------\n\nYou can call overloaded functions with different number of arguments.\n\n.. code-block:: postgres\n\n  CREATE FUNCTION rental_duration(customer_id integer) ..\n\n  CREATE FUNCTION rental_duration(customer_id integer, from_date date) ..\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/rental_duration?customer_id=232\"\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/rental_duration?customer_id=232&from_date=2018-07-01\"\n\n.. important::\n\n  Overloaded functions with the same argument names but different types are not supported.\n"
  },
  {
    "path": "docs/references/api/media_type_handlers.rst",
    "content": ".. _custom_media:\n\nMedia Type Handlers\n###################\n\nMedia Type Handlers allow PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.\n\nMedia types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.\n\n.. code-block:: postgres\n\n   CREATE DOMAIN \"application/json\" AS json;\n\nUsing these domains, :ref:`functions <functions>` can become handlers and `user-defined aggregates <https://www.postgresql.org/docs/current/xaggr.html>`_ can serve as handlers for :ref:`tables_views` and :ref:`table_functions`.\n\n.. important::\n\n  - PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overridden.\n  - Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.\n    For these you can use the :ref:`any_handler`.\n\nHandler Function\n================\n\nAs an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.\n\n.. code-block:: postgres\n\n  create extension postgis;\n\n  create table lines (\n    id   int primary key\n  , name text\n  , geom geometry(LINESTRING, 4326)\n  );\n\n  insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::geometry);\n\nFor this you can create a vendor media type.\n\n.. code-block:: postgres\n\n  create domain \"application/vnd.twkb\" as bytea;\n\nAnd use it as a return type on a function, to make it a handler.\n\n.. code-block:: postgres\n\n  create or replace function get_line (id int)\n  returns \"application/vnd.twkb\" as $$\n    select st_astwkb(geom) from lines where id = get_line.id;\n  $$ language sql;\n\n.. note::\n\n   For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::\"application/vnd.twkb\"`.\n\nNow you can request the ``TWKB`` output like so:\n\n.. code-block:: bash\n\n  curl 'localhost:3000/rpc/get_line?id=1' -i \\\n    -H \"Accept: application/vnd.twkb\"\n\n  HTTP/1.1 200 OK\n  Content-Type: application/vnd.twkb\n\n  # binary output\n\nNote that PostgREST will automatically set the  ``Content-Type`` to ``application/vnd.twkb``.\n\nHandlers for Tables/Views\n=========================\n\nTo benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table.\n\nUser-defined aggregates can be turned into handlers by using domain media types as the return type of their transition or final functions.\n\nLet's create a transition function for this example.\n\n.. code-block:: postgres\n\n  create or replace function twkb_handler_transition (state bytea, next lines)\n  returns \"application/vnd.twkb\" as $$\n    select state || st_astwkb(next.geom);\n  $$ language sql;\n\nNow we'll use it on a new aggregate defined for the ``lines`` table.\n\n.. code-block:: postgres\n\n  create or replace aggregate twkb_agg (lines) (\n    initcond = ''\n  , stype = \"application/vnd.twkb\"\n  , sfunc = twkb_handler_transition\n  );\n\n.. note::\n\n  You can test see this aggregate working with:\n\n  .. code-block:: psql\n\n    SELECT twkb_agg(l) from lines l;\n\n                               twkb_agg\n    ---------------------------------------------------------------\n    \\xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30\n    (1 row)\n\nNow you can request the table endpoint with the ``twkb`` media type:\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines' -i \\\n    -H \"Accept: application/vnd.twkb\"\n\n  HTTP/1.1 200 OK\n  Content-Type: application/vnd.twkb\n\n  # binary output\n\nIf you have a table-valued function returning the same table type, the handler can also act upon on it.\n\n.. code-block:: postgres\n\n  create or replace function get_lines ()\n  returns setof lines as $$\n    select * from lines;\n  $$ language sql;\n\n.. code-block:: bash\n\n  curl 'localhost:3000/get_lines' -i \\\n    -H \"Accept: application/vnd.twkb\"\n\n  HTTP/1.1 200 OK\n  Content-Type: application/vnd.twkb\n\n  # binary output\n\nOverriding a Builtin Handler\n============================\n\nLet's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.\nIt'll include a `Byte order mark (BOM) <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.\n\nCreate a domain for the standard ``text/csv`` media type.\n\n.. code-block:: postgres\n\n  create domain \"text/csv\" as text;\n\nAnd a transition function that returns the domain.\n\n.. code-block:: postgres\n\n  create or replace function bom_csv_trans (state text, next lines)\n  returns \"text/csv\" as $$\n    select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\\n';\n  $$ language sql;\n\nThis time we'll add a final function. This will add the CSV header, the BOM and the ``Content-Disposition`` header.\n\n.. code-block:: postgres\n\n  create or replace function bom_csv_final (data \"text/csv\")\n  returns \"text/csv\" as $$\n    -- set the Content-Disposition header\n    select set_config('response.headers', '[{\"Content-Disposition\": \"attachment; filename=\\\"lines.csv\\\"\"}]', true);\n    select\n      -- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8\n      convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||\n      -- the header for the CSV\n      (E'id,name,geom\\n' || data);\n  $$ language sql;\n\nNow use the transition and final function as part of the new aggregate.\n\n.. code-block:: postgres\n\n  create or replace aggregate bom_csv_agg (lines) (\n    initcond = ''\n  , stype = \"text/csv\"\n  , sfunc = bom_csv_trans\n  , finalfunc = bom_csv_final\n  );\n\n.. note::\n\n  You can test this with:\n\n  .. code-block:: psql\n\n    select bom_csv_agg(l) from lines l;\n                                                 bom_csv_agg\n    -----------------------------------------------------------------------------------------------------\n     ﻿id,name,geom                                                                                      +\n     1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440+\n     2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840+\n\n    (1 row)\n\nAnd request it like:\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines' -i \\\n    -H \"Accept: text/csv\"\n\n  HTTP/1.1 200 OK\n  Content-Type: text/csv\n  Content-Disposition: attachment; filename=\"lines.csv\"\n\n  id,name,geom\n  1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440\n  2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840\n\n.. _any_handler:\n\nThe \"Any\" Handler\n=================\n\nFor more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This handler obeys the following rules:\n\n- It responds to all media types and even to requests that don't include an ``Accept`` header.\n- It sets the ``Content-Type`` header to ``application/octet-stream`` by default, but this can be overridden inside the function with :ref:`guc_resp_hdrs`.\n- It overrides all other handlers (:ref:`builtin <builtin_media>` or custom), so it's better to do it for an isolated function or view.\n\nLet's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.\n\n.. code-block:: postgres\n\n  create domain \"*/*\" as bytea;\n\n  -- we'll use an .xml suffix for the view to be clear its output is always XML\n  create view \"lines.xml\" as\n  select * from lines;\n\n  -- transition function\n  create or replace function lines_xml_trans (state \"*/*\", next \"lines.xml\")\n  returns \"*/*\" as $$\n    select state || xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom)::text::bytea || E'\\n' ;\n  $$ language sql;\n\n  -- final function\n  create or replace function lines_xml_final (data \"*/*\")\n  returns \"*/*\" as $$\n  declare\n    -- get the Accept header\n    req_accept text := current_setting('request.headers', true)::json->>'accept';\n  begin\n    -- when we need to override the default Content-Type (application/octet-stream) set by PostgREST\n    if req_accept = '*/*' then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'text/xml'))::text, true);\n    elsif req_accept IN ('application/xml', 'text/xml') then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);\n    else\n      -- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types\n      raise sqlstate 'PT415' using message = 'Unsupported Media Type';\n    end if;\n\n    return data;\n  end; $$ language plpgsql;\n\n  -- new aggregate\n  create or replace aggregate lines_xml_agg (\"lines.xml\") (\n    stype = \"*/*\"\n  , sfunc = lines_xml_trans\n  , finalfunc = lines_xml_final\n  );\n\nTest it on SQL:\n\n.. code-block:: psql\n\n  select (encode(lines_xml_agg(x), 'escape'))::xml from \"lines.xml\" x;\n                                                              encode\n  ------------------------------------------------------------------------------------------------------------------------------\n   <line id=\"1\" name=\"line-1\">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>+\n   <line id=\"2\" name=\"line-2\">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>+\n\nNow we can omit the ``Accept`` header and it will respond with XML.\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines.xml' -i\n\n  HTTP/1.1 200 OK\n  Content-Type: text/xml\n\n  <line id=\"1\" name=\"line-1\">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>\n  <line id=\"2\" name=\"line-2\">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>\n\nAnd it will accept only XML media types.\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines.xml' -i \\\n    -H \"Accept: text/xml\"\n\n  HTTP/1.1 200 OK\n  Content-Type: text/xml\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines.xml' -i  \\\n    -H \"Accept: application/xml\"\n\n  HTTP/1.1 200 OK\n  Content-Type: text/xml\n\n.. code-block:: bash\n\n  curl 'localhost:3000/lines.xml' -i \\\n    -H \"Accept: unknown/media\"\n\n  HTTP/1.1 415 Unsupported Media Type\n"
  },
  {
    "path": "docs/references/api/openapi.rst",
    "content": ".. _open-api:\n\nOpenAPI\n=======\n\nPostgREST automatically serves a full `OpenAPI <https://www.openapis.org/>`_ description on the root path. This provides a list of all endpoints (tables, foreign tables, views, functions), along with supported HTTP verbs and example payloads.\n\n.. note::\n\n  By default, this output depends on the permissions of the role that is contained in the JWT role claim (or the :ref:`db-anon-role` if no JWT is sent). If you need to show all the endpoints disregarding the role's permissions, set the :ref:`openapi-mode` config to :code:`ignore-privileges`.\n\nFor extra customization, the OpenAPI output contains a \"description\" field for every `SQL comment <https://www.postgresql.org/docs/current/sql-comment.html>`_ on any database object. For instance,\n\n.. code-block:: postgres\n\n  COMMENT ON SCHEMA mammals IS\n    'A warm-blooded vertebrate animal of a class that is distinguished by the secretion of milk by females for the nourishment of the young';\n\n  COMMENT ON TABLE monotremes IS\n    'Freakish mammals lay the best eggs for breakfast';\n\n  COMMENT ON VIEW monotremes_v IS\n    'Only the platypus is publicly visible';\n\n  COMMENT ON COLUMN monotremes.has_venomous_claw IS\n    'Sometimes breakfast is not worth it';\n\nThese unsavory comments will appear in the generated JSON as the fields, ``info.description``, ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``.\n\nAlso if you wish to generate a ``summary`` field you can do it by having a multiple line comment, the ``summary`` will be the first line and the ``description`` the lines that follow it:\n\n.. code-block:: postgres\n\n  COMMENT ON TABLE entities IS\n    $$Entities summary\n\n    Entities description that\n    spans\n    multiple lines$$;\n\nSimilarly, you can override the API title by commenting the schema.\n\n.. code-block:: postgres\n\n  COMMENT ON SCHEMA api IS\n  $$FooBar API\n\n  A RESTful API that serves FooBar data.$$;\n\nIf you need to include the ``security`` and ``securityDefinitions`` options, set the :ref:`openapi-security-active` configuration to ``true``.\n\nYou can use a tool like `Swagger UI <https://swagger.io/tools/swagger-ui/>`_ to create beautiful documentation from the description and to host an interactive web-based dashboard. The dashboard allows developers to make requests against a live PostgREST server, and provides guidance with request headers and example request bodies.\n\n.. important::\n\n  The OpenAPI information can go out of date as the schema changes under a running server. See :ref:`schema_reloading`.\n\n.. _override_openapi:\n\nOverriding Full OpenAPI Response\n--------------------------------\n\nYou can override the whole default response with a function result. To do this, set the function on :ref:`db-root-spec`.\n\n.. code:: bash\n\n   db-root-spec = \"root\"\n\n.. code:: postgres\n\n  create or replace function root() returns json as $_$\n  declare\n  openapi json = $$\n    {\n      \"swagger\": \"2.0\",\n      \"info\":{\n        \"title\":\"Overridden\",\n        \"description\":\"This is a my own API\"\n      }\n    }\n  $$;\n  begin\n    return openapi;\n  end\n  $_$ language plpgsql;\n\n.. code-block:: bash\n\n  curl http://localhost:3000\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n\n  {\n    \"swagger\": \"2.0\",\n    \"info\":{\n      \"title\":\"Overridden\",\n      \"description\":\"This is a my own API\"\n    }\n  }\n"
  },
  {
    "path": "docs/references/api/options.rst",
    "content": ".. _options_requests:\n\nOPTIONS method\n==============\n\nYou can verify which HTTP methods are allowed on endpoints for tables and views by using an OPTIONS request. These methods are allowed depending on what operations *can* be done on the table or view, not on the database permissions assigned to them.\n\nFor a table named ``people``, OPTIONS would show:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" -X OPTIONS -i\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Allow: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE\n\nFor a view, the methods are determined by the presence of INSTEAD OF TRIGGERS.\n\n.. table::\n   :widths: auto\n\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | Method allowed     | View's requirements                                                                             |\n   +====================+=================================================================================================+\n   | OPTIONS, GET, HEAD | None (Always allowed)                                                                           |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | POST               | INSTEAD OF INSERT TRIGGER                                                                       |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | PUT                | INSTEAD OF INSERT TRIGGER, INSTEAD OF UPDATE TRIGGER, also requires the presence of a           |\n   |                    | primary key                                                                                     |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | PATCH              | INSTEAD OF UPDATE TRIGGER                                                                       |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | DELETE             | INSTEAD OF DELETE TRIGGER                                                                       |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n   | All the above methods are allowed for                                                                                |\n   | `auto-updatable views <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ |\n   +--------------------+-------------------------------------------------------------------------------------------------+\n\nFor functions, the methods depend on their volatility. ``VOLATILE`` functions allow only ``OPTIONS,POST``, whereas the rest also permit ``GET,HEAD``.\n\n.. important::\n\n  Whenever you add or remove tables or views, or modify a view's INSTEAD OF TRIGGERS on the database, you must refresh PostgREST's schema cache for OPTIONS requests to work properly. See the section :ref:`schema_reloading`.\n"
  },
  {
    "path": "docs/references/api/pagination_count.rst",
    "content": "Pagination and Count\n####################\n\nPagination controls the number of rows returned for an :doc:`API resource <../api>` response. Combined with the count, you can traverse all the rows of a response.\n\n.. _limits:\n\nLimits and Pagination\n---------------------\n\nPostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results:\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Range-Unit: items\n  Content-Range: 0-14/*\n\nHere items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner.\n\nQuery Parameters\n~~~~~~~~~~~~~~~~\n\nOne way to request limits and offsets is by using query parameters. For example:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?limit=15&offset=30\"\n\nThis method is also useful for embedded resources, which we will cover in another section. The server always responds with range headers even if you use query parameters to limit the query.\n\nRange Header\n~~~~~~~~~~~~\n\nYou can use headers to specify the range of rows desired.\nThis request gets the first twenty people:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" -i \\\n    -H \"Range-Unit: items\" \\\n    -H \"Range: 0-19\"\n\nNote that the server may respond with fewer if unable to meet your request:\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Range-Unit: items\n  Content-Range: 0-17/*\n\nYou may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`.\n\n.. _prefer_count:\n\nCounting\n--------\n\nIn order to obtain the total size of the table (such as when rendering the last page link in a pagination control), you can specify a ``Prefer: count=<value>`` header. The values can be ``exact``, ``planned`` and ``estimated``.\n\nThis also works on views and :ref:`table_functions`.\n\n\n.. _exact_count:\n\nExact Count\n~~~~~~~~~~~\n\nTo get the exact count, use ``Prefer: count=exact``.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/bigtable\" -I \\\n    -H \"Range-Unit: items\" \\\n    -H \"Range: 0-24\" \\\n    -H \"Prefer: count=exact\"\n\nNote that the larger the table the slower this query runs in the database. The server will respond with the selected range and total\n\n.. code-block:: http\n\n  HTTP/1.1 206 Partial Content\n  Range-Unit: items\n  Content-Range: 0-24/3573458\n\n.. _planned_count:\n\nPlanned Count\n~~~~~~~~~~~~~\n\nTo avoid the shortcomings of :ref:`exact count <exact_count>`, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count.\nTo do this, specify the ``Prefer: count=planned`` header.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/bigtable?limit=25\" -I \\\n    -H \"Prefer: count=planned\"\n\n.. code-block:: http\n\n  HTTP/1.1 206 Partial Content\n  Content-Range: 0-24/3572000\n\nNote that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables.\nFor example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``.\nSee `ANALYZE <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.\n\n.. _estimated_count:\n\nEstimated Count\n~~~~~~~~~~~~~~~\n\nWhen you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is\n1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.\n\nIn general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.\n\nTo help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when\nthat threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is\ndefined by :ref:`db-max-rows`.\n\nHere's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/smalltable?limit=25\" -I \\\n    -H \"Prefer: count=estimated\"\n\n.. code-block:: http\n\n  HTTP/1.1 206 Partial Content\n  Content-Range: 0-24/321\n\nIf we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/bigtable?limit=25\" -I \\\n    -H \"Prefer: count=estimated\"\n\n.. code-block:: http\n\n  HTTP/1.1 206 Partial Content\n  Content-Range: 0-24/3572000\n"
  },
  {
    "path": "docs/references/api/preferences.rst",
    "content": ".. _preferences:\n\nPrefer Header\n#############\n\nPostgREST honors the Prefer HTTP header specified on `RFC 7240 <https://www.rfc-editor.org/rfc/rfc7240.html>`_. It allows clients to specify required and optional behaviors for their requests.\n\nThe following preferences are supported.\n\n- ``Prefer: handling``. See :ref:`prefer_handling`.\n- ``Prefer: timezone``. See :ref:`prefer_timezone`.\n- ``Prefer: return``. See :ref:`prefer_return`.\n- ``Prefer: count``. See :ref:`prefer_count`.\n- ``Prefer: resolution``. See :ref:`prefer_resolution`.\n- ``Prefer: missing``. See :ref:`prefer_missing`.\n- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.\n- ``Prefer: tx``. See :ref:`prefer_tx`.\n\n.. _prefer_handling:\n\nStrict or Lenient Handling\n==========================\n\nThe server ignores unrecognized or unfulfillable preferences by default. You can control this behavior with the ``handling`` preference. It can take two values: ``lenient`` (the default) or ``strict``.\n\n``handling=strict`` will throw an error if you specify invalid preferences. For instance:\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/projects\" \\\n    -H \"Prefer: handling=strict, foo, bar\"\n\n.. code-block:: http\n\n  HTTP/1.1 400 Bad Request\n  Content-Type: application/json; charset=utf-8\n\n.. code-block:: json\n\n  {\n      \"code\": \"PGRST122\",\n      \"message\": \"Invalid preferences given with handling=strict\",\n      \"details\": \"Invalid preferences: foo, bar\",\n      \"hint\": null\n  }\n\n\n``handling=lenient`` ignores invalid preferences.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/projects\" \\\n    -H \"Prefer: handling=lenient, foo, bar\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Type: application/json; charset=utf-8\n\n.. _prefer_timezone:\n\nTimezone\n========\n\nThe ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all time zones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.\n\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/timestamps\" \\\n    -H \"Prefer: timezone=America/Los_Angeles\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Type: application/json; charset=utf-8\n  Preference-Applied: timezone=America/Los_Angeles\n\n.. code-block:: json\n\n  [\n    {\"t\":\"2023-10-18T05:37:59.611-07:00\"},\n    {\"t\":\"2023-10-18T07:37:59.611-07:00\"},\n    {\"t\":\"2023-10-18T09:37:59.611-07:00\"}\n  ]\n\nFor an invalid time zone, PostgREST returns values with the default time zone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/timestamps\" \\\n    -H \"Prefer: timezone=Jupiter/Red_Spot\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Type: application/json; charset=utf-8\n\n.. code-block:: json\n\n  [\n    {\"t\":\"2023-10-18T12:37:59.611+00:00\"},\n    {\"t\":\"2023-10-18T14:37:59.611+00:00\"},\n    {\"t\":\"2023-10-18T16:37:59.611+00:00\"}\n  ]\n\nNote that there's no ``Preference-Applied`` in the response.\n\nHowever, with ``handling=strict``, an invalid time zone preference will throw an :ref:`error <pgrst122>`.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/timestamps\" \\\n    -H \"Prefer: handling=strict, timezone=Jupiter/Red_Spot\"\n\n.. code-block:: http\n\n  HTTP/1.1 400 Bad Request\n\n.. _prefer_return:\n\nReturn Representation\n=====================\n\nThe ``return`` preference can be used to obtain information about affected resource when it's :ref:`inserted <insert>`, :ref:`updated <update>` or :ref:`deleted <delete>`.\nThis helps avoid a subsequent GET request.\n\nMinimal\n-------\n\nWith ``Prefer: return=minimal``, no response body will be returned. This is the default mode for all write requests.\n\nHeaders Only\n------------\n\nIf the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/projects\" -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: return=headers-only\" \\\n    -d '{\"id\":33, \"name\": \"x\"}'\n\n.. code-block:: http\n\n  HTTP/1.1 201 Created\n  Location: /projects?id=eq.34\n  Preference-Applied: return=headers-only\n\nFull\n----\n\nOn the other end of the spectrum you can get the full created object back in the response to your request by including the header :code:`Prefer: return=representation`. That way you won't have to make another HTTP call to discover properties that may have been filled in on the server side. You can also apply the standard :ref:`v_filter` to these results.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/projects\" -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: return=representation\" \\\n    -d '{\"id\":33, \"name\": \"x\"}'\n\n.. code-block:: http\n\n  HTTP/1.1 201 Created\n  Preference-Applied: return=representation\n\n.. code-block:: json\n\n  [\n      {\n          \"id\": 33,\n          \"name\": \"x\"\n      }\n  ]\n\n.. _prefer_tx:\n\nTransaction End Preference\n==========================\n\nThe ``tx`` preference can be set to specify if the :ref:`transaction <transactions>` will end in a COMMIT or ROLLBACK. This preference is not enabled by default but can be activated with :ref:`db-tx-end`.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/projects\" -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: tx=rollback, return=representation\" \\\n    -d '{\"name\": \"Project X\"}'\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Preference-Applied: tx=rollback, return=representation\n\n  {\"id\": 35, \"name\": \"Project X\"}\n\n\n.. _prefer_missing:\n\nMissing\n=======\n\nWhen doing ``POST`` and ``PATCH`` requests, any missing columns in the payload will be inserted as ``null`` value by default. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.\n\nHaving:\n\n.. code-block:: postgres\n\n  create table foo (\n    id bigint generated by default as identity primary key\n  , bar text\n  , baz int default 100\n  );\n\nA request:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/foo?columns=id,bar,baz\" \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: missing=default, return=representation\" \\\n    -d @- << EOF\n      [\n        { \"bar\": \"val1\" },\n        { \"bar\": \"val2\", \"baz\": 15 }\n      ]\n  EOF\n\nWill result in:\n\n.. code-block:: json\n\n  [\n    { \"id\":  1, \"bar\": \"val1\", \"baz\": 100 },\n    { \"id\":  2, \"bar\": \"val2\", \"baz\": 15 }\n  ]\n\n\n.. _prefer_max_affected:\n\nMax Affected\n============\n\nYou can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The \"affected resources\" are the number of rows returned by ``DELETE`` and ``PATCH`` requests.\n\nTo illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/items?id=lt.15 -X DELETE \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: handling=strict, max-affected=10\"\n\n.. code-block:: http\n\n  HTTP/1.1 400 Bad Request\n\n.. code-block:: json\n\n  {\n      \"code\": \"PGRST124\",\n      \"message\": \"Query result exceeds max-affected preference constraint\",\n      \"details\": \"The query affects 14 rows\",\n      \"hint\": null\n  }\n\nWith :ref:`RPC <functions>`, the preference is honored completely on the basis of the number of rows returned in the result set of the function. This can be useful for complex mutation queries using `data-modifying statements <https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING>`_. A simple example:\n\n.. code-block:: postgres\n\n  CREATE FUNCTION test.delete_items()\n  RETURNS SETOF items AS $$\n    DELETE FROM items WHERE id < 15 RETURNING *;\n  $$ LANGUAGE SQL;\n\n.. code-block:: bash\n\n  curl -i \"http://localhost:3000/rpc/delete_items\" \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Prefer: handling=strict, max-affected=10\"\n\n.. code-block:: http\n\n  HTTP/1.1 400 Bad Request\n\n.. code-block:: json\n\n  {\n      \"code\": \"PGRST124\",\n      \"message\": \"Query result exceeds max-affected preference constraint\",\n      \"details\": \"The query affects 14 rows\",\n      \"hint\": null\n  }\n\n.. note::\n\n  It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.\n"
  },
  {
    "path": "docs/references/api/resource_embedding.rst",
    "content": ".. _resource_embedding:\n\nResource Embedding\n##################\n\nPostgREST allows including related resources in a single API call. This reduces the need for many API requests.\n\n.. _fk_join:\n\nForeign Key Joins\n=================\n\nThe server uses **Foreign Keys** to determine which database objects can be joined together. It supports joining tables, views and\ntable-valued functions.\n\n- For tables, it generates a join condition using the foreign keys columns (respecting composite keys).\n- For views, it generates a join condition using the views' base tables foreign key columns.\n- For table-valued functions, it generates a join condition based on the foreign key columns of the returned table type.\n\n.. important::\n\n  - Whenever foreign keys change you must do :ref:`schema_reloading` for this feature to work.\n\nRelationships\n=============\n\nFor example, consider a database of films and their awards:\n\n.. _erd_film:\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/film.png\n\n  .. code-tab:: postgresql SQL\n\n    create table actors(\n      id int primary key generated always as identity,\n      first_name text,\n      last_name text\n    );\n\n    create table directors(\n      id int primary key generated always as identity,\n      first_name text,\n      last_name text\n    );\n\n    create table films(\n      id int primary key generated always as identity,\n      director_id int references directors(id),\n      title text,\n      year int,\n      rating numeric(3,1),\n      language text\n    );\n\n    create table technical_specs(\n      film_id int references films(id) primary key,\n      runtime time,\n      camera text,\n      sound text\n    );\n\n    create table roles(\n      film_id int references films(id),\n      actor_id int references actors(id),\n      character text,\n      primary key(film_id, actor_id)\n    );\n\n    create table competitions(\n      id int primary key generated always as identity,\n      name text,\n      year int\n    );\n\n    create table nominations(\n      competition_id int references competitions(id),\n      film_id int references films(id),\n      rank int,\n      primary key (competition_id, film_id)\n    );\n\n.. _many-to-one:\n\nMany-to-one relationships\n-------------------------\n\nSince ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. This enables us to request all the films and the director for each film.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/films?select=title,directors(id,last_name)\"\n\n.. code-block:: json\n\n  [\n    { \"title\": \"Workers Leaving The Lumière Factory In Lyon\",\n      \"directors\": {\n        \"id\": 2,\n        \"last_name\": \"Lumière\"\n      }\n    },\n    { \"title\": \"The Dickson Experimental Sound Film\",\n      \"directors\": {\n        \"id\": 1,\n        \"last_name\": \"Dickson\"\n      }\n    },\n    { \"title\": \"The Haunted Castle\",\n      \"directors\": {\n        \"id\": 3,\n        \"last_name\": \"Méliès\"\n      }\n    }\n  ]\n\nNote that the embedded ``directors`` is returned as a JSON object because of the \"to-one\" end.\n\nSince the table name is plural, we can be more accurate by making it singular with an alias.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/films?select=title,director:directors(id,last_name)\"\n\n.. code-block:: json\n\n  [\n    { \"title\": \"Workers Leaving The Lumière Factory In Lyon\",\n      \"director\": {\n        \"id\": 2,\n        \"last_name\": \"Lumière\"\n      }\n    },\n    \"..\"\n  ]\n\n.. _one-to-many:\n\nOne-to-many relationships\n-------------------------\n\nThe **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the \"to-many\" end.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/directors?select=last_name,films(title)\"\n\n.. code-block:: json\n\n  [\n    { \"last_name\": \"Lumière\",\n      \"films\": [\n        {\"title\": \"Workers Leaving The Lumière Factory In Lyon\"}\n      ]\n    },\n    { \"last_name\": \"Dickson\",\n      \"films\": [\n        {\"title\": \"The Dickson Experimental Sound Film\"}\n      ]\n    },\n    { \"last_name\": \"Méliès\",\n      \"films\": [\n        {\"title\": \"The Haunted Castle\"}\n      ]\n    }\n  ]\n\n.. _many-to-many:\n\nMany-to-many relationships\n--------------------------\n\nThe join table determines many-to-many relationships. It must contain foreign keys to other two tables and they must be part of its composite key. In the :ref:`sample film database <erd_film>`, ``roles`` is taken as a join table.\n\nThe join table is also detected if the composite key has additional columns.\n\n.. code-block:: postgres\n\n  create table roles(\n    id int generated always as identity,\n  , film_id int references films(id)\n  , actor_id int references actors(id)\n  , character text,\n  , primary key(id, film_id, actor_id)\n  );\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/actors?select=first_name,last_name,films(title)\"\n\n.. code-block:: json\n\n  [\n    { \"first_name\": \"Willem\",\n      \"last_name\": \"Dafoe\",\n      \"films\": [\n        {\"title\": \"The Lighthouse\"}\n      ]\n    },\n    \"..\"\n  ]\n\n.. _one-to-one:\n\nOne-to-one relationships\n------------------------\n\nOne-to-one relationships are detected in two ways. (We'll use the ``films`` and ``technical_specs`` tables from the :ref:`sample film database <erd_film>` as an example).\n\n- When the foreign key is also a primary key.\n\n  .. code-block:: postgres\n\n    create table technical_specs(\n      film_id int references films(id) primary key\n    -- ...\n    );\n\n- Or when the foreign key has a unique constraint.\n\n  .. code-block:: postgres\n\n    create table technical_specs(\n      id int primary key\n    , film_id int references films(id) unique\n    -- ...\n    );\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/films?select=title,technical_specs(camera)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"Pulp Fiction\",\n      \"technical_specs\": {\"camera\": \"Arriflex 35-III\"}\n    },\n    \"..\"\n  ]\n\n.. _computed_relationships:\n\nComputed Relationships\n======================\n\nYou can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers <https://wiki.postgresql.org/wiki/Foreign_data_wrappers>`_.\n\nComputed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.\n\n.. important::\n\n  - Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them. e.g. ``RETURNS <table_name>`` is not inlinable.\n\nAssuming there's a foreign table ``premieres`` that we want to relate to ``films``.\n\n.. code-block:: postgres\n\n  create foreign table premieres (\n    id integer,\n    location text,\n    \"date\" date,\n    film_id integer\n  ) server import_csv options ( filename '/tmp/directors.csv', format 'csv');\n\n  create function film(premieres) returns setof films rows 1 as $$\n    select * from films where id = $1.film_id\n  $$ stable language sql;\n\nThe above function defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type). Since there's a ``rows 1``, this defines a many-to-one relationship.\nThe name of the function ``film`` is arbitrary and can be used to do the embedding:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/premieres?select=location,film(name)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"location\": \"Cannes Film Festival\",\n      \"film\": {\"name\": \"Pulp Fiction\"}\n    },\n    \"..\"\n  ]\n\n.. warning::\n\n  - Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, it will unnest the embedding and return repeated values for the top level resource.\n\nNow let's define the opposite one-to-many relationship.\n\n.. code-block:: postgres\n\n  create function premieres(films) returns setof premieres as $$\n    select * from premieres where film_id = $1.id\n  $$ stable language sql;\n\nIn this case there's an implicit ``ROWS 1000`` defined by PostgreSQL(`search \"result_rows\" on this PostgreSQL doc <https://www.postgresql.org/docs/current/sql-createfunction.html>`_).\nWe consider any value greater than 1 as \"many\" so this defines a one-to-many relationship.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/films?select=name,premieres(name)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"name\": \"Pulp Ficiton\",\n      \"premieres\": [{\"location\": \"Cannes Festival\"}]\n    },\n    \"..\"\n  ]\n\nOverriding Relationships\n------------------------\n\nComputed relationships also allow you to override the ones that PostgREST auto-detects.\n\nFor example, to override the :ref:`many-to-one relationship <many-to-one>` between ``films`` and ``directors``.\n\n.. code-block:: postgres\n\n  create function directors(films) returns setof directors rows 1 as $$\n    select * from directors where id = $1.director_id\n  $$ stable language sql;\n\nThanks to overloaded functions, you can use the same function name for different parameters. Thus define relationships from other tables/views to directors.\n\n.. code-block:: postgres\n\n  create function directors(film_schools) returns setof directors as $$\n    select * from directors where film_school_id = $1.id\n  $$ stable language sql;\n\nComputed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.\n\n.. _embed_disamb:\n.. _target_disamb:\n.. _hint_disamb:\n.. _complex_rels:\n\nForeign Key Joins on Multiple Foreign Key Relationships\n=======================================================\n\nWhen there are multiple foreign keys between tables, :ref:`fk_join` need disambiguation to resolve which foreign key columns to use for the join.\nTo do this, you can specify a foreign key by using the ``!<fk>`` syntax.\n\n.. _multiple_m2o:\n\nMultiple Many-To-One\n--------------------\n\nFor example, suppose you have the following ``orders`` and ``addresses`` tables:\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/orders.png\n\n  .. code-tab:: postgresql SQL\n\n    create table addresses (\n      id int primary key generated always as identity,\n      name text,\n      city text,\n      state text,\n      postal_code char(5)\n    );\n\n    create table orders (\n      id int primary key generated always as identity,\n      name text,\n      billing_address_id int,\n      shipping_address_id int,\n      constraint billing  foreign key(billing_address_id) references addresses(id),\n      constraint shipping foreign key(shipping_address_id) references addresses(id)\n    );\n\nSince the ``orders`` table has two foreign keys to the ``addresses`` table, a foreign key join is ambiguous and PostgREST will respond with an error:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/orders?select=*,addresses(*)\" -i\n\n\n.. code-block:: http\n\n   HTTP/1.1 300 Multiple Choices\n\n.. code-block:: json\n\n   {\n     \"code\": \"PGRST201\",\n     \"details\": [\n       {\n         \"cardinality\": \"many-to-one\",\n         \"embedding\": \"orders with addresses\",\n         \"relationship\": \"billing using orders(billing_address_id) and addresses(id)\"\n       },\n       {\n         \"cardinality\": \"many-to-one\",\n         \"embedding\": \"orders with addresses\",\n         \"relationship\": \"shipping using orders(shipping_address_id) and addresses(id)\"\n       }\n     ],\n     \"hint\": \"Try changing 'addresses' to one of the following: 'addresses!billing', 'addresses!shipping'. Find the desired relationship in the 'details' key.\",\n     \"message\": \"Could not embed because more than one relationship was found for 'orders' and 'addresses'\"\n   }\n\nTo successfully join ``orders`` with ``addresses``, we can follow the error ``hint`` which tells us to add the foreign key name as ``!billing`` or ``!shipping``.\nNote that the foreign keys have been named explicitly in the :ref:`SQL definition above <multiple_m2o>`. To make the result clearer we'll also alias the tables:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/orders?select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name)\"\n\n  curl --get \"http://localhost:3000/orders\" \\\n    -d \"select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name)\"\n\n.. code-block:: json\n\n   [\n     {\n       \"name\": \"Personal Water Filter\",\n       \"billing_address\": {\n         \"name\": \"32 Glenlake Dr.Dearborn, MI 48124\"\n       },\n       \"shipping_address\": {\n         \"name\": \"30 Glenlake Dr.Dearborn, MI 48124\"\n       }\n     }\n   ]\n\n.. _multiple_o2m:\n\nMultiple One-To-Many\n--------------------\n\nLet's take the tables from :ref:`multiple_m2o`. To get the opposite one-to-many relationship, we can also specify the foreign key name:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/addresses?select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)&id=eq.1\"\n\n  curl --get \"http://localhost:3000/addresses\" \\\n    -d \"select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)\" \\\n    -d \"id=eq.1\"\n\n.. code-block:: json\n\n   [\n     {\n       \"name\": \"32 Glenlake Dr.Dearborn, MI 48124\",\n       \"billing_orders\": [\n         { \"name\": \"Personal Water Filter\" },\n         { \"name\": \"Coffee Machine\" }\n       ],\n       \"shipping_orders\": [\n         { \"name\": \"Coffee Machine\" }\n       ]\n     }\n   ]\n\nRecursive Relationships\n-----------------------\n\nTo disambiguate recursive relationships, PostgREST requires :ref:`computed_relationships`.\n\n.. _recursive_o2o_embed:\n\nRecursive One-To-One\n~~~~~~~~~~~~~~~~~~~~\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/presidents.png\n\n  .. code-tab:: postgresql SQL\n\n    create table presidents (\n      id int primary key generated always as identity,\n      first_name text,\n      last_name text,\n      predecessor_id int references presidents(id) unique\n    );\n\nTo get either side of the Recursive One-To-One relationship, create the functions:\n\n.. code-block:: postgres\n\n  create or replace function predecessor(presidents) returns setof presidents rows 1 as $$\n    select * from presidents where id = $1.predecessor_id\n  $$ stable language sql;\n\n  create or replace function successor(presidents) returns setof presidents rows 1 as $$\n    select * from presidents where predecessor_id = $1.id\n  $$ stable language sql;\n\nNow, to query a president with their predecessor and successor:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/presidents?select=last_name,predecessor(last_name),successor(last_name)&id=eq.2\"\n\n  curl --get \"http://localhost:3000/presidents\" \\\n    -d \"select=last_name,predecessor(last_name),successor(last_name)\" \\\n    -d \"id=eq.2\"\n\n.. code-block:: json\n\n  [\n    {\n      \"last_name\": \"Adams\",\n      \"predecessor\": {\n        \"last_name\": \"Washington\"\n      },\n      \"successor\": {\n        \"last_name\": \"Jefferson\"\n      }\n    }\n  ]\n\n.. _recursive_o2m_embed:\n\nRecursive One-To-Many\n~~~~~~~~~~~~~~~~~~~~~\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/employees.png\n\n  .. code-tab:: postgresql SQL\n\n    create table employees (\n      id int primary key generated always as identity,\n      first_name text,\n      last_name text,\n      supervisor_id int references employees(id)\n    );\n\nTo get the One-To-Many embedding, that is, the supervisors with their supervisees, create a function like this one:\n\n.. code-block:: postgres\n\n  create or replace function supervisees(employees) returns setof employees as $$\n    select * from employees where supervisor_id = $1.id\n  $$ stable language sql;\n\nNow, the query would be:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/employees?select=last_name,supervisees(last_name)&id=eq.1\"\n\n  curl --get \"http://localhost:3000/employees\" \\\n    -d \"select=last_name,supervisees(last_name)\" \\\n    -d \"id=eq.1\"\n\n.. code-block:: json\n\n  [\n    {\n      \"name\": \"Taylor\",\n      \"supervisees\": [\n        { \"name\": \"Johnson\" },\n        { \"name\": \"Miller\" }\n      ]\n    }\n  ]\n\n.. _recursive_m2o_embed:\n\nRecursive Many-To-One\n~~~~~~~~~~~~~~~~~~~~~~\n\nLet's take the same ``employees`` table from :ref:`recursive_o2m_embed`.\nTo get the Many-To-One relationship, that is, the employees with their respective supervisor, you need to create a function like this one:\n\n.. code-block:: postgres\n\n  create or replace function supervisor(employees) returns setof employees rows 1 as $$\n    select * from employees where id = $1.supervisor_id\n  $$ stable language sql;\n\nThen, the query would be:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/employees?select=last_name,supervisor(last_name)&id=eq.3\"\n\n  curl --get \"http://localhost:3000/employees\" \\\n    -d \"select=last_name,supervisor(last_name)\" \\\n    -d \"id=eq.3\"\n\n.. code-block:: json\n\n  [\n    {\n      \"last_name\": \"Miller\",\n      \"supervisor\": {\n        \"last_name\": \"Taylor\"\n      }\n    }\n  ]\n\n.. _recursive_m2m_embed:\n\nRecursive Many-To-Many\n~~~~~~~~~~~~~~~~~~~~~~\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/users.png\n\n  .. code-tab:: postgresql SQL\n\n    create table users (\n      id int primary key generated always as identity,\n      first_name text,\n      last_name text,\n      username text unique\n    );\n\n    create table subscriptions (\n      subscriber_id int references users(id),\n      subscribed_id int references users(id),\n      type text,\n      primary key (subscriber_id, subscribed_id)\n    );\n\nTo get all the subscribers of a user as well as the ones they're following, define these functions:\n\n.. code-block:: postgres\n\n  create or replace function subscribers(users) returns setof users as $$\n    select u.*\n    from users u,\n         subscriptions s\n    where s.subscriber_id = u.id and\n          s.subscribed_id = $1.id\n  $$ stable language sql;\n\n  create or replace function following(users) returns setof users as $$\n    select u.*\n    from users u,\n         subscriptions s\n    where s.subscribed_id = u.id and\n          s.subscriber_id = $1.id\n  $$ stable language sql;\n\nThen, the request would be:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/users?select=username,subscribers(username),following(username)&id=eq.4\"\n\n  curl --get \"http://localhost:3000/users\" \\\n    -d \"select=username,subscribers(username),following(username)\" \\\n    -d \"id=eq.4\"\n\n.. code-block:: json\n\n   [\n     {\n       \"username\": \"the_top_artist\",\n       \"subscribers\": [\n         { \"username\": \"patrick109\" },\n         { \"username\": \"alicia_smith\" }\n       ],\n       \"following\": [\n         { \"username\": \"top_streamer\" }\n       ]\n     }\n   ]\n\n.. _embedding_partitioned_tables:\n\nForeign Key Joins on Partitioned Tables\n=======================================\n\nForeign Key joins can also be done between `partitioned tables <https://www.postgresql.org/docs/current/ddl-partitioning.html>`_ and other tables.\n\nFor example, let's create the ``box_office`` partitioned table that has the gross daily revenue of a film:\n\n.. tabs::\n\n  .. group-tab:: ERD\n\n    .. image:: ../../_static/boxoffice.png\n\n  .. code-tab:: postgresql SQL\n\n    CREATE TABLE box_office (\n      bo_date DATE NOT NULL,\n      film_id INT REFERENCES films NOT NULL,\n      gross_revenue DECIMAL(12,2) NOT NULL,\n      PRIMARY KEY (bo_date, film_id)\n    ) PARTITION BY RANGE (bo_date);\n\n    -- Let's also create partitions for each month of 2021\n\n    CREATE TABLE box_office_2021_01 PARTITION OF box_office\n    FOR VALUES FROM ('2021-01-01') TO ('2021-01-31');\n\n    CREATE TABLE box_office_2021_02 PARTITION OF box_office\n    FOR VALUES FROM ('2021-02-01') TO ('2021-02-28');\n\n    -- and so until december 2021\n\nSince it contains the ``films_id`` foreign key, it is possible to join ``box_office`` and ``films``:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000\"\n\n  curl --get \"http://localhost:3000/box_office\" \\\n    -d \"select=bo_date,gross_revenue,films(title)\" \\\n    -d \"gross_revenue=gte.1000000\"\n\n.. note::\n\n  * Foreign key joins on partitions is not allowed because it leads to ambiguity errors (see :ref:`embed_disamb`) between them and their parent partitioned table. More details at `#1783(comment) <https://github.com/PostgREST/postgrest/issues/1783#issuecomment-959823827>`_). :ref:`computed_relationships` can be used if this is needed.\n\n  * Partitioned tables can reference other tables since PostgreSQL 11 but can only be referenced from any other table since PostgreSQL 12.\n\n.. _embedding_views:\n\nForeign Key Joins on Views\n==========================\n\nPostgREST will infer the foreign keys of a view using its base tables. Base tables are the ones referenced in the ``FROM`` and ``JOIN`` clauses of the view definition.\nThe foreign keys' columns must be present in the top ``SELECT`` clause of the view for this to work.\n\nFor instance, the following view has ``nominations``, ``films`` and ``competitions`` as base tables:\n\n.. code-block:: postgres\n\n  CREATE VIEW nominations_view AS\n    SELECT\n       films.title as film_title\n     , competitions.name as competition_name\n     , nominations.rank\n     , nominations.film_id as nominations_film_id\n     , films.id as film_id\n    FROM nominations\n    JOIN films ON films.id = nominations.film_id\n    JOIN competitions ON competitions.id = nominations.competition_id;\n\nSince this view contains ``nominations.film_id``, which has a **foreign key** relationship to ``films``, then we can join the ``films`` table. Similarly, because the view contains ``films.id``, then we can also join the ``roles`` and the ``actors`` tables (the last one in a many-to-many relationship):\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/nominations_view?select=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5\"\n\n  curl --get \"http://localhost:3000/nominations_view\" \\\n    -d \"select=film_title,films(language),roles(character),actors(last_name,first_name)\" \\\n    -d \"rank=eq.5\"\n\nIt's also possible to foreign key join `Materialized Views <https://www.postgresql.org/docs/current/rules-materializedviews.html>`_.\n\n.. important::\n\n  - It's not guaranteed that foreign key joins will work on all kinds of views. In particular, foreign key joins won't work on views that contain UNIONs.\n\n    + Why? PostgREST detects base table foreign keys in the view by querying and parsing `pg_rewrite <https://www.postgresql.org/docs/current/catalog-pg-rewrite.html>`_.\n      This may fail depending on the complexity of the view.\n    + As a workaround, you can use :ref:`computed_relationships` to define manual relationships for views.\n\n  - If view definitions change you must refresh PostgREST's schema cache for this to work properly. See the section :ref:`schema_reloading`.\n\n.. _embedding_view_chains:\n\nForeign Key Joins on Chains of Views\n------------------------------------\n\nViews can also depend on other views, which in turn depend on the actual base table. For PostgREST to pick up those chains recursively to any depth, all the views must be in the search path, so either in the exposed schema (:ref:`db-schemas`) or in one of the schemas set in :ref:`db-extra-search-path`. This does not apply to the base table, which could be in a private schema as well. See :ref:`schema_isolation` for more details.\n\n.. _function_embed:\n\nForeign Key Joins on Table-Valued Functions\n===========================================\n\nIf you have a :ref:`Function <functions>` that returns a table type, you can do a Foreign Key join on the result.\n\nHere's a sample function (notice the ``RETURNS SETOF films``).\n\n.. code-block:: postgres\n\n  CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$\n    SELECT * FROM films;\n  $$ LANGUAGE SQL STABLE;\n\nA request with ``directors`` embedded:\n\n.. code-block:: bash\n\n   # curl \"http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*\"\n\n   curl --get \"http://localhost:3000/rpc/getallfilms\" \\\n     -d \"select=title,directors(id,last_name)\" \\\n     -d \"title=like.*Workers*\"\n\n.. code-block:: json\n\n   [\n     { \"title\": \"Workers Leaving The Lumière Factory In Lyon\",\n       \"directors\": {\n         \"id\": 2,\n         \"last_name\": \"Lumière\"\n       }\n     }\n   ]\n\n.. _mutation_embed:\n\nForeign Key Joins on Writes\n===========================\n\nYou can join related database objects after doing :ref:`insert`, :ref:`update` or :ref:`delete`.\n\nSay you want to insert a **film** and then get some of its attributes plus join its **director**.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/films?select=title,year,director:directors(first_name,last_name)\" \\\n    -H \"Prefer: return=representation\" \\\n    -d @- << EOF\n    {\n      \"id\": 100,\n      \"director_id\": 40,\n      \"title\": \"127 hours\",\n      \"year\": 2010,\n      \"rating\": 7.6,\n      \"language\": \"english\"\n    }\n  EOF\n\nResponse:\n\n.. code-block:: json\n\n   {\n    \"title\": \"127 hours\",\n    \"year\": 2010,\n    \"director\": {\n      \"first_name\": \"Danny\",\n      \"last_name\": \"Boyle\"\n    }\n   }\n\n.. _nested_embedding:\n\nNested Embedding\n================\n\nIf you want to embed through join tables but need more control on the intermediate resources, you can do nested embedding. For instance, you can request the Actors, their Roles and the Films for those Roles:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/actors?select=roles(character,films(title,year))\"\n\n.. _embed_filters:\n\nEmbedded Filters\n================\n\nEmbedded resources can be shaped similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded resource. For instance, to order the actors in each film:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,actors(*)&actors.order=last_name,first_name\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=*,actors(*)\" \\\n    -d \"actors.order=last_name,first_name\"\n\nThis sorts the list of actors in each film but does *not* change the order of the films themselves. To filter the roles returned with each film:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho)\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=*,roles(*)\" \\\n    -d \"roles.character=in.(Chico,Harpo,Groucho)\"\n\nOnce again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists.\n\nAn ``or`` filter can be used for a similar operation:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo)\"\n\n  curl --get \"http://localhost:3000/films\" \\\n   -d \"select=*,roles(*)\" \\\n   -d \"roles.or=(character.eq.Gummo,character.eq.Zeppo)\"\n\nHowever, this only works for columns inside ``roles``. See :ref:`how to use \"or\" across multiple resources <or_embed_rels>`.\n\nLimit and offset operations are possible:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=*,actors(*)\" \\\n    -d \"actors.limit=10\" \\\n    -d \"actors.offset=2\"\n\nEmbedded resources can be aliased and filters can be applied on these aliases:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=*,90_comps:competitions(name),91_comps:competitions(name)\" \\\n    -d \"90_comps.year=eq.1990\" \\\n    -d \"91_comps.year=eq.1991\"\n\nFilters can also be applied on nested embedded resources:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=*,roles(*,actors(*))\" \\\n    -d \"roles.actors.order=last_name\" \\\n    -d \"roles.actors.first_name=like.*Tom*\"\n\nThe result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables.\n\n.. _embedding_top_level_filter:\n\nTop-level Filtering\n===================\n\nBy default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors(first_name,last_name)\" \\\n    -d \"actors.first_name=eq.Jehanne\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"Workers Leaving The Lumière Factory In Lyon\",\n      \"actors\": []\n    },\n    {\n      \"title\": \"The Dickson Experimental Sound Film\",\n      \"actors\": []\n    },\n    {\n      \"title\": \"The Haunted Castle\",\n      \"actors\": [\n        {\n          \"first_name\": \"Jehanne\",\n          \"last_name\": \"d'Alcy\"\n        }\n      ]\n    }\n  ]\n\nIn order to filter the top level rows you need to add ``!inner`` to the embedded resource. For instance, to get **only** the films that have an actor named ``Jehanne``:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors!inner(first_name,last_name)\" \\\n    -d \"actors.first_name=eq.Jehanne\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"The Haunted Castle\",\n      \"actors\": [\n        {\n          \"first_name\": \"Jehanne\",\n          \"last_name\": \"d'Alcy\"\n        }\n      ]\n    }\n  ]\n\n.. _null_embed:\n\nNull filtering on Embedded Resources\n------------------------------------\n\nNull filtering on the embedded resources can behave the same as ``!inner``. While providing more flexibility.\n\nFor example, doing ``actors=not.is.null`` returns the same result as ``actors!inner(*)``:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,actors(*)&actors=not.is.null\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors(*)\" \\\n    -d \"actors=not.is.null\"\n\nThe ``is.null`` filter can be used in embedded resources to perform an anti-join. To get all the films that do not have any nominations:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,nominations()&nominations=is.null\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,nominations()\" \\\n    -d \"nominations=is.null\"\n\n\nBoth ``is.null`` and ``not.is.null`` can be included inside the `or` operator. For instance, to get the films that have no actors **or** directors registered yet:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,nominations()&nominations=is.null\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d select=title,actors(*),directors(*)\" \\\n    -d \"or=(actors.is.null,directors.is.null)\"\n\n.. _or_embed_rels:\n\nOR filtering across Embedded Resources\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can also use ``not.is.null`` to make an ``or`` filter across multiple resources.\nFor instance, to show the films with actors **or** directors named John:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,actors(),directors()&directors.first_name=eq.John&actors.first_name=eq.John&or=(directors.not.is.null,actors.not.is.null)\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors(),directors()\" \\\n    -d \"directors.first_name=eq.John\" \\\n    -d \"actors.first_name=eq.John\" \\\n    -d \"or=(directors.not.is.null,actors.not.is.null)\"\n\n.. code-block:: json\n\n  [\n    { \"title\": \"Pulp Fiction\" },\n    { \"title\": \"The Thing\" },\n    \"..\"\n  ]\n\nHere, we use :ref:`empty embeds <empty_embed>` because retrieving their info would be restricted by the filters.\nFor example, the ``directors`` embedding would return ``null`` if its ``first_name`` is not John.\nTo solve this, you need to add extra embedded resources and use the empty ones for filtering.\nFrom the above example:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,act:actors(),dir:directors(),actors(first_name),directors(first_name)&dir.first_name=eq.John&act.first_name=eq.John&or=(dir.not.is.null,act.not.is.null)\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    # We need to use aliases like \"act\" and \"dir\" to filter the empty embeds\n    -d \"select=title,act:actors(),dir:directors(),actors(first_name),directors(first_name)\" \\\n    -d \"dir.first_name=eq.John\" \\\n    -d \"act.first_name=eq.John\" \\\n    -d \"or=(dir.not.is.null,act.not.is.null)\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"Pulp Fiction\",\n      \"actors\": [\n        { \"first_name\": \"John\" },\n        { \"first_name\": \"Samuel\" },\n        { \"first_name\": \"Uma\" },\n        \"..\"\n      ]\n      \"directors\": {\n        \"first_name\": \"Quentin\"\n      }\n    },\n    \"..\"\n  ]\n\n.. _empty_embed:\n\nEmpty Embed\n-----------\n\nYou can leave an embedded resource empty, this helps with filtering in some cases.\n\nTo filter the films by actors but not include them:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,actors()&actors.first_name=eq.Jehanne&actors=not.is.null\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors()\" \\\n    -d \"actors.first_name=eq.Jehanne\" \\\n    -d \"actors=not.is.null\"\n\n.. code-block:: json\n\n  [\n    {\n      \"title\": \"The Haunted Castle\",\n    }\n  ]\n\n.. _top_level_order:\n\nTop-level Ordering\n==================\n\nOn :ref:`Many-to-One <many-to-one>` and :ref:`One-to-One <one-to-one>` relationships, you can use a column of the \"to-one\" end to sort the top-level.\n\nFor example, to arrange the films in descending order using the director's last name.\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/films?select=title,directors(last_name)&order=directors(last_name).desc\"\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,directors(last_name)\" \\\n    -d \"order=directors(last_name).desc\"\n\n.. _spread_embed:\n\nSpread embedded resource\n========================\n\nYou can modify the shape of the embedded resources by using the spread syntax (``...``).\n\n.. _spread_to_one_embed:\n\nSpread To-One relationships\n---------------------------\n\nSpread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships, will lift the embedded columns to the top object.\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,...directors(director_first_name:first_name, director_last_name:last_name)\" \\\n    -d \"title=like.*Workers*\"\n\n.. code-block:: json\n\n   [\n     {\n       \"title\": \"Workers Leaving The Lumière Factory In Lyon\",\n       \"director_first_name\": \"Louis\",\n       \"director_last_name\": \"Lumière\"\n     }\n   ]\n\nNote that there is no wrapping ``\"directors\"`` object, unlike regularly embedding :ref:`many-to-one <many-to-one>` relationships. Also note that embedded columns can be aliased normally.\n\n.. _spread_to_many_embed:\n\nSpread To-Many relationships\n----------------------------\n\nSpread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-many <many-to-many>` relationships, will convert the embedded columns into correlated arrays.\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/directors\" \\\n    -d \"select=first_name,...films(film_titles:title,film_years:year)\" \\\n    -d \"first_name=like.Quentin*\"\n\n.. code-block:: json\n\n   [\n     {\n       \"first_name\": \"Quentin\",\n       \"film_titles\": [\n         \"Pulp Fiction\",\n         \"Reservoir Dogs\"\n       ],\n       \"film_years\": [\n         1994,\n         1992\n       ]\n     }\n   ]\n\nNote that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlated-in the above result, we can say that \"Pulp Fiction\" premiered in 1994 and \"Reservoir Dogs\" in 1992.\n\nOrder in spread to-many\n~~~~~~~~~~~~~~~~~~~~~~~\n\nIn the above example, the order of the values inside the correlated arrays is unspecified, but all the values are guaranteed to be in the same unspecified order.\n\nYou can order the correlated arrays explicitly. For example, to order by the film year:\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/directors\" \\\n    -d \"select=first_name,...films(film_titles:title,film_years:year)\" \\\n    -d \"first_name=like.Quentin*\" \\\n    -d \"films.order=year\"\n\n.. code-block:: json\n\n   [\n     {\n       \"first_name\": \"Quentin\",\n       \"film_titles\": [\n         \"Reservoir Dogs\",\n         \"Pulp Fiction\"\n       ],\n       \"film_years\": [\n         1992,\n         1994\n       ]\n     }\n   ]\n\n.. warning::\n\n   Aliasing spread columns is recommended since JSON allows duplicate keys. Example:\n\n   .. code-block:: bash\n\n     curl --get \"localhost:3000/projects\" \\\n       -d \"select=id,name,...clients(id,name)\"\n\n   .. code-block:: json\n\n     [{\"id\":1,\"name\":\"Windows 7\",\"id\":1,\"name\":\"Microsoft\"},\n      {\"id\":2,\"name\":\"Windows 10\",\"id\":1,\"name\":\"Microsoft\"},\n      {\"id\":3,\"name\":\"IOS\",\"id\":2,\"name\":\"Apple\"},\n      {\"id\":4,\"name\":\"OSX\",\"id\":2,\"name\":\"Apple\"},\n      {\"id\":5,\"name\":\"Orphan\",\"id\":null,\"name\":null}]\n\n   This can be a problem in Javascript objects, since only the last duplicated key will be considered. To solve it do:\n\n   .. code-block:: bash\n\n     curl --get \"localhost:3000/projects\" \\\n       -d \"select=id,name,...clients(client_id:id,client_name:name)\"\n\n\nMultiple Spreads\n----------------\n\nYou can use multiple spreads at any level. For example, let's spread ``technical_specs`` and ``roles`` into ``films`` and then spread ``films`` into ``directors``:\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/directors\" \\\n    -d \"select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))\" \\\n    -d \"first_name=like.Quentin*\" \\\n    -d \"films.order=year\" \\\n    -d \"films.roles.order=character\"\n\n.. code-block:: json\n\n   [\n     {\n       \"first_name\": \"Quentin\",\n       \"film_titles\": [\n         \"Reservoir Dogs\",\n         \"Pulp Fiction\"\n       ],\n       \"film_years\": [\n         1992,\n         1994\n       ],\n       \"film_runtimes\": [\n         \"01:39:00\",\n         \"02:29:00\"\n       ]\n       \"film_characters\": [\n         [ \"Mr. Pink\", \"Mr. White\" ],\n         [ \"Mia Wallace\", \"Vincent Vega\" ]\n       ]\n     }\n   ]\n\nNote that:\n\n- All the ``film_*`` arrays are correlated-\"Reservoir Dogs\" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ \"Mr. Pink\", \"Mr. White\" ]``.\n- The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``).\n- The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``).\n\nSpread a join table\n-------------------\n\nSpread can be used to move the columns of a join table in a :ref:`many-to-many <many-to-many>` to the top object. For instance, to get the ``character`` column of the ``roles`` join table into ``actors``:\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/films\" \\\n    -d \"select=title,actors:roles(character,...actors(first_name,last_name))\" \\\n    -d \"title=like.*Lighthouse*\"\n\n.. code-block:: json\n\n   [\n     {\n       \"title\": \"The Lighthouse\",\n       \"actors\": [\n          {\n            \"character\": \"Thomas Wake\",\n            \"first_name\": \"Willem\",\n            \"last_name\": \"Dafoe\"\n          }\n       ]\n     }\n   ]\n\n\n"
  },
  {
    "path": "docs/references/api/resource_representation.rst",
    "content": "Resource Representation\n#######################\n\nPostgREST uses proper HTTP content negotiation (`RFC7231 <https://datatracker.ietf.org/doc/html/rfc7231#section-5.3>`_) to deliver a resource representation.\nThat is to say the same API endpoint can respond in different formats like JSON or CSV depending on the request.\n\n.. _res_format:\n\nResponse Format\n===============\n\nUse the Accept request header to specify the acceptable format (or formats) for the response:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" \\\n    -H \"Accept: application/json\"\n\n.. note::\n  \n  The ordering of columns in the response isn't guaranteed to align with the order specified in the ``select`` clause. For example, with resource embedding:\n\n  .. code-block:: bash\n    \n    http://localhost:3000/films?select=directors(last_name,id),title\n  \n  We may get:\n\n  .. code-block:: bash\n\n    [\n      {\n        \"title\": \"title\",\n        \"directors\": {\n          \"id\": 5,\n          \"last_name\": \"name\"\n        }\n      }\n    ]\n\n  This is in line with the `JSON schema spec <https://json-schema.org/draft/2020-12/json-schema-core#name-instance-data-model>`_:\n\n    *\"object: An unordered set of properties mapping a string to an instance\"*\n\n.. _builtin_media:\n\nBuiltin Media Type Handlers\n===========================\n\nBuiltin handlers are offered for common standard media types.\n\n* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`functions`.\n* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.\n* ``application/geo+json``, see :ref:`ww_postgis`.\n* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.\n\nThe following vendor media types handlers are also supported.\n\n* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.\n* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.\n\nAny unrecognized media type will throw an error.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" \\\n    -H \"Accept: unknown/unknown\"\n\n.. code-block:: http\n\n  HTTP/1.1 415 Unsupported Media Type\n\n  {\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: unknown/unknown\"}\n\nTo extend the accepted media types, you can use :ref:`custom_media`.\n\n.. _singular_plural:\n\nSingular or Plural\n------------------\n\nBy default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting :code:`/items?id=eq.1` returns\n\n.. code:: json\n\n  [\n    { \"id\": 1 }\n  ]\n\nThis can be inconvenient for client code. To return the first result as an object unenclosed by an array, specify :code:`vnd.pgrst.object` as part of the :code:`Accept` header\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/items?id=eq.1\" \\\n    -H \"Accept: application/vnd.pgrst.object+json\"\n\nThis returns\n\n.. code:: json\n\n  { \"id\": 1 }\n\nWhen a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:\n\n.. code-block:: json\n\n  {\n    \"code\": \"PGRST116\",\n    \"message\": \"Cannot coerce the result to a single JSON object\",\n    \"details\": \"The result contains 0 rows\",\n    \"hint\": null\n  }\n\n.. note::\n\n  Many APIs distinguish plural and singular resources using a special nested URL convention e.g. `/stories` vs `/stories/1`. Why do we use `/stories?id=eq.1`? The answer is because a singular resource is (for us) a row determined by a primary key, and primary keys can be compound (meaning defined across more than one column). The more familiar nested urls consider only a degenerate case of simple and overwhelmingly numeric primary keys. These so-called artificial keys are often introduced automatically by Object Relational Mapping libraries.\n\n  Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format.\n\n.. _stripped_nulls:\n\nStripped Nulls\n--------------\n\nBy default PostgREST returns all JSON null values. For example, requesting ``/projects?id=gt.10`` returns\n\n.. code:: json\n\n  [\n    { \"id\": 11, \"name\": \"OSX\",      \"client_id\": 1,    \"another_col\": \"val\" },\n    { \"id\": 12, \"name\": \"ProjectX\", \"client_id\": null, \"another_col\": null },\n    { \"id\": 13, \"name\": \"Y\",        \"client_id\": null, \"another_col\": null }\n  ]\n\nOn large result sets, the unused keys with ``null`` values can waste bandwidth unnecessarily. To remove them, specify ``nulls=stripped`` as a parameter of ``application/vnd.pgrst.array``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/projects?id=gt.10\" \\\n    -H \"Accept: application/vnd.pgrst.array+json;nulls=stripped\"\n\nThis returns\n\n.. code:: json\n\n  [\n    { \"id\": 11, \"name\": \"OSX\", \"client_id\": 1, \"another_col\": \"val\" },\n    { \"id\": 12, \"name\": \"ProjectX\" },\n    { \"id\": 13, \"name\": \"Y\"}\n  ]\n\n.. _req_body:\n\nRequest Body\n============\n\nThe server handles the following request body media types:\n\n* ``application/json``\n* ``application/x-www-form-urlencoded``\n* ``text/csv``\n\nFor :ref:`tables_views` this works on ``POST``, ``PATCH`` and ``PUT`` methods. For :ref:`functions`, it works on ``POST`` methods.\n\nFor functions there are three additional types:\n\n* ``application/octet-stream``\n* ``text/plain``\n* ``text/xml``\n\nSee :ref:`function_single_unnamed`.\n"
  },
  {
    "path": "docs/references/api/schemas.rst",
    "content": ".. _schemas:\n\nSchemas\n=======\n\nPostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.\n\n.. important::\n  \n  ``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects.\n\nSingle schema\n-------------\n\nTo expose a single schema, specify a single value in :ref:`db-schemas`.\n\n.. code:: bash\n\n   db-schemas = \"api\"\n\nThis schema is added to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request using :ref:`tx_settings`.\n\n.. _multiple-schemas:\n\nMultiple schemas\n----------------\n\nTo expose multiple schemas, specify a comma-separated list on :ref:`db-schemas`:\n\n.. code:: bash\n\n   db-schemas = \"tenant1, tenant2\"\n\nTo switch schemas, use the ``Accept-Profile`` and ``Content-Profile`` headers.\n\nIf you don't specify a Profile header, the first schema in the list(``tenant1`` here) is selected as the default schema.\n\nOnly the selected schema gets added to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request.\n\n.. note::\n\n   These headers are based on the \"Content Negotiation by Profile\" spec: https://www.w3.org/TR/dx-prof-conneg\n\nGET/HEAD\n~~~~~~~~\n\nFor GET or HEAD, select the schema with ``Accept-Profile``.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/items\" \\\n    -H \"Accept-Profile: tenant2\"\n\nOther methods\n~~~~~~~~~~~~~\n\nFor POST, PATCH, PUT and DELETE, select the schema with ``Content-Profile``.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/items\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -H \"Content-Profile: tenant2\" \\\n    -d '{...}'\n\nYou can also select the schema for :ref:`functions` and :ref:`open-api`.\n\nRestricted schemas\n~~~~~~~~~~~~~~~~~~\n\nYou can only switch to a schema included in :ref:`db-schemas`. Using another schema will result in an error:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/items\" \\\n    -H \"Accept-Profile: tenant3\"\n\n.. code-block::\n\n  {\n    \"code\":\"PGRST106\",\n    \"details\":null,\n    \"hint\":null,\n    \"message\":\"The schema must be one of the following: tenant1, tenant2\"\n  }\n\n\nDynamic schemas\n~~~~~~~~~~~~~~~\n\nTo add schemas dynamically, you can use :ref:`in_db_config` plus :ref:`config reloading <config_reloading_notify>` and :ref:`schema cache reloading <schema_reloading_notify>`. Here are some options for how to do this:\n\n- If the schemas' names have a pattern, like a ``tenant_`` prefix, do:\n\n.. code-block:: postgres\n\n  create or replace function postgrest.pre_config()\n  returns void as $$\n    select\n      set_config('pgrst.db_schemas', string_agg(nspname, ','), true)\n    from pg_namespace\n    where nspname like 'tenant_%';\n  $$ language sql;\n\n- If there's no name pattern but they're created with a particular role (``CREATE SCHEMA mine AUTHORIZATION joe``), do:\n\n.. code-block:: postgres\n\n  create or replace function postgrest.pre_config()\n  returns void as $$\n    select\n      set_config('pgrst.db_schemas', string_agg(nspname, ','), true)\n    from pg_namespace\n    where nspowner = 'joe'::regrole;\n  $$ language sql;\n\n- Otherwise, you might need to create a table that stores the allowed schemas.\n\n.. code-block:: postgres\n\n  create table postgrest.config (schemas text);\n\n  create or replace function postgrest.pre_config()\n  returns void as $$\n    select\n      set_config('pgrst.db_schemas', schemas, true)\n    from postgrest.config;\n  $$ language sql;\n\nThen each time you add an schema, do:\n\n.. code-block:: postgres\n\n   NOTIFY pgrst, 'reload config';\n   NOTIFY pgrst, 'reload schema';\n"
  },
  {
    "path": "docs/references/api/tables_views.rst",
    "content": ".. _tables_views:\n\nTables and Views\n################\n\nAll tables and views of the :ref:`exposed schema <schemas>` and accessible by the :ref:`active database role <roles>` are available for querying. They are exposed in one-level deep routes.\n\nFor instance the full contents of a table `people` is returned at\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\"\n\nThere are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, PATCH, and DELETE verbs depending entirely on database permissions.\n\n.. note::\n\n  Why not provide nested routes? Many APIs allow nesting to retrieve related information, such as :code:`/films/1/director`. We offer a more flexible mechanism (inspired by GraphQL) to embed related resources. This is covered on :ref:`resource_embedding`.\n\n.. _read:\n\nRead\n====\n\n.. _head_req:\n\nGET and HEAD\n------------\n\nUsing the GET method, you can retrieve tables and views rows. The default :ref:`res_format` is JSON.\n\nA HEAD method will behave identically to GET except that no response body will be returned (`RFC 2616 <https://datatracker.ietf.org/doc/html/rfc2616#section-9.4>`_).\nAs an optimization, the generated query won't execute an aggregate (to avoid unnecessary data transfer).\n\n.. _h_filter:\n\nHorizontal Filtering\n--------------------\n\nYou can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?age=lt.13\"\n\nYou can evaluate multiple conditions on columns by adding more query string parameters. For instance, to return people who are 18 or older **and** are students:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?age=gte.18&student=is.true\"\n\n.. _operators:\n\nOperators\n~~~~~~~~~\n\nThese operators are available:\n\n============  ========================  ==================================================================================\nAbbreviation  In PostgreSQL             Meaning\n============  ========================  ==================================================================================\neq            :code:`=`                 equals\ngt            :code:`>`                 greater than\ngte           :code:`>=`                greater than or equal\nlt            :code:`<`                 less than\nlte           :code:`<=`                less than or equal\nneq           :code:`<>` or :code:`!=`  not equal\nlike          :code:`LIKE`              LIKE operator (to avoid `URL encoding <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can use ``*`` as an alias of the percent sign ``%`` for the pattern)\nilike         :code:`ILIKE`             ILIKE operator (to avoid `URL encoding <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can use ``*`` as an alias of the percent sign ``%`` for the pattern)\nmatch         :code:`~`                 ~ operator, see :ref:`pattern_matching`\nimatch        :code:`~*`                ~* operator, see :ref:`pattern_matching`\nin            :code:`IN`                one of a list of values, e.g. :code:`?a=in.(1,2,3)`\n                                        – also supports commas in quoted strings like\n                                        :code:`?a=in.(\"hi,there\",\"yes,you\")`\nis            :code:`IS`                checking for exact equality (null,not_null,true,false,unknown)\nisdistinct    :code:`IS DISTINCT FROM`  not equal, treating :code:`NULL` as a comparable value\nfts           :code:`@@`                :ref:`fts` using to_tsquery\nplfts         :code:`@@`                :ref:`fts` using plainto_tsquery\nphfts         :code:`@@`                :ref:`fts` using phraseto_tsquery\nwfts          :code:`@@`                :ref:`fts` using websearch_to_tsquery\ncs            :code:`@>`                contains e.g. :code:`?tags=cs.{example, new}`\ncd            :code:`<@`                contained in e.g. :code:`?values=cd.{1,2,3}`\nov            :code:`&&`                overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` –\n                                        also supports array types, use curly braces instead of square brackets e.g.\n                                        :code:`?arr=ov.{1,3}`\nsl            :code:`<<`                strictly left of, e.g. :code:`?range=sl.(1,10)`\nsr            :code:`>>`                strictly right of\nnxr           :code:`&<`                does not extend to the right of, e.g. :code:`?range=nxr.(1,10)`\nnxl           :code:`&>`                does not extend to the left of\nadj           :code:`-|-`               is adjacent to, e.g. :code:`?range=adj.(1,10)`\nnot           :code:`NOT`               negates another operator, see :ref:`logical_operators`\nor            :code:`OR`                logical :code:`OR`, see :ref:`logical_operators`\nand           :code:`AND`               logical :code:`AND`, see :ref:`logical_operators`\nall           :code:`ALL`               comparison matches all the values in the list, see :ref:`modifiers`\nany           :code:`ANY`               comparison matches any value in the list, see :ref:`modifiers`\n============  ========================  ==================================================================================\n\nFor more complicated filters you will have to create a new view in the database, or use a function. For instance, here's a view to show \"today's stories\" including possibly older pinned stories:\n\n.. code-block:: postgres\n\n  CREATE VIEW fresh_stories AS\n  SELECT *\n    FROM stories\n   WHERE pinned = true\n      OR published > now() - interval '1 day'\n  ORDER BY pinned DESC, published DESC;\n\nThe view will provide a new endpoint:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/fresh_stories\"\n\n.. _logical_operators:\n\nLogical operators\n~~~~~~~~~~~~~~~~~\n\nMultiple conditions on columns are evaluated using ``AND`` by default, but you can combine them using ``OR`` with the ``or`` operator. For example, to return people under 18 **or** over 21:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?or=(age.lt.18,age.gt.21)\"\n\nTo **negate** any operator, you can prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` .\n\nYou can also apply complex logic to the conditions:\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/people?grade=gte.90&student=is.true&or=(age.eq.14,not.and(age.gte.11,age.lte.17))\"\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"grade=gte.90\" \\\n    -d \"student=is.true\" \\\n    -d \"or=(age.eq.14,not.and(age.gte.11,age.lte.17))\"\n\nIf the filter value has a :ref:`reserved character <reserved-chars>`, then you need to wrap it in double quotes:\n\n.. code-block:: bash\n\n  curl -g 'http://localhost:3000/survey?or=(age_range.adj.\"[18,21)\",age_range.cs.\"[30,35]\")'\n\n.. _modifiers:\n\nOperator Modifiers\n~~~~~~~~~~~~~~~~~~\n\nYou may further simplify the logic using the ``any/all`` modifiers of ``eq,like,ilike,gt,gte,lt,lte,match,imatch``.\n\nFor instance, to avoid repeating the same column for ``or``, use ``any`` to get people with last names that start with O or P:\n\n.. code-block:: bash\n\n  curl -g \"http://localhost:3000/people?last_name=like(any).{O*,P*}\"\n\nIn a similar way, you can use ``all`` to avoid repeating the same column for ``and``. To get the people with last names that start with O and end with n:\n\n.. code-block:: bash\n\n  curl -g \"http://localhost:3000/people?last_name=like(all).{O*,*n}\"\n\n.. _pattern_matching:\n\nPattern Matching\n~~~~~~~~~~~~~~~~\n\nThe pattern-matching operators (:code:`like`, :code:`ilike`, :code:`match`, :code:`imatch`) exist to support filtering data using patterns instead of concrete strings, as described in the `PostgreSQL docs <https://www.postgresql.org/docs/current/functions-matching.html>`__.\n\nTo ensure best performance on larger data sets, an `appropriate index <https://www.postgresql.org/docs/current/pgtrgm.html#PGTRGM-INDEX>`__ should be used and even then, it depends on the pattern value and actual data statistics whether an existing index will be used by the query planner or not.\n\n.. _fts:\n\nFull-Text Search\n~~~~~~~~~~~~~~~~\n\nThe :code:`fts` operator has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming.\n\nThe following examples illustrate the possibilities, assuming column :code:`my_tsv` is of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_.\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_tsv=fts(french).amusant\"\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_tsv=plfts.The%20Fat%20Cats\"\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_tsv=not.phfts(english).The%20Fat%20Cats\"\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_tsv=not.wfts(french).amusant\"\n\n.. _fts_to_tsvector:\n\nAutomatic ``tsvector`` conversion\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIf the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.\nThis allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_text_column=fts(french).amusant\"\n\n.. code-block:: bash\n\n  curl --get \"http://localhost:3000/people\" \\\n    -d \"my_json_column=not.phfts(english).The%20Fat%20Cats\"\n\n.. important::\n\n  To ensure this operation is fast, you need to create an index on the expression:\n\n  .. code-block:: postgres\n\n    CREATE INDEX idx_people_col ON people\n    USING GIN (to_tsvector('french', my_text_column));\n\n.. _v_filter:\n\nVertical Filtering\n------------------\n\nWhen certain columns are wide (such as those holding binary data), it is more efficient for the server to withhold them in a response. The client can specify which columns are required using the :code:`select` parameter.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=first_name,age\"\n\n.. code-block:: json\n\n  [\n    {\"first_name\": \"John\", \"age\": 30},\n    {\"first_name\": \"Jane\", \"age\": 20}\n  ]\n\nThe default is ``*``, meaning all columns. This value will become more important below in :ref:`resource_embedding`.\n\n.. _renaming_columns:\n\nRenaming Columns\n~~~~~~~~~~~~~~~~\n\nYou can rename the columns by prefixing them with an alias followed by the colon ``:`` operator.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=fullName:full_name,birthDate:birth_date\"\n\n.. code-block:: json\n\n  [\n    {\"fullName\": \"John Doe\", \"birthDate\": \"04/25/1988\"},\n    {\"fullName\": \"Jane Doe\", \"birthDate\": \"01/12/1998\"}\n  ]\n\n.. _json_columns:\n\nJSON Columns\n~~~~~~~~~~~~\n\nTo further reduce the data transferred, you can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs <https://www.postgresql.org/docs/current/functions-json.html>`__.\n\n.. code-block:: postgres\n\n  CREATE TABLE people (\n    id int,\n    json_data json\n  );\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=id,json_data->>blood_type,json_data->phones\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 1, \"blood_type\": \"A-\", \"phones\": [{\"country_code\": \"61\", \"number\": \"917-929-5745\"}] },\n    { \"id\": 2, \"blood_type\": \"O+\", \"phones\": [{\"country_code\": \"43\", \"number\": \"512-446-4988\"}, {\"country_code\": \"43\", \"number\": \"213-891-5979\"}] }\n  ]\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=id,json_data->phones->0->>number\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 1, \"number\": \"917-929-5745\"},\n    { \"id\": 2, \"number\": \"512-446-4988\"}\n  ]\n\nThis also works with filters:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=id,json_data->blood_type&json_data->>blood_type=eq.A-\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 1, \"blood_type\": \"A-\" },\n    { \"id\": 3, \"blood_type\": \"A-\" },\n    { \"id\": 7, \"blood_type\": \"A-\" }\n  ]\n\nNote that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=id,json_data->age&json_data->age=gt.20\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 11, \"age\": 25 },\n    { \"id\": 12, \"age\": 30 },\n    { \"id\": 15, \"age\": 35 }\n  ]\n\nOrdering is also supported:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=id,json_data->age&order=json_data->>age.desc\"\n\n.. code-block:: json\n\n  [\n    { \"id\": 15, \"age\": 35 },\n    { \"id\": 12, \"age\": 30 },\n    { \"id\": 11, \"age\": 25 }\n  ]\n\n.. _composite_array_columns:\n\nComposite / Array Columns\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe arrow operators(``->``, ``->>``) can also be used for accessing composite fields and array elements.\n\n.. code-block:: postgres\n\n  CREATE TYPE coordinates (\n    lat decimal(8,6),\n    long decimal(9,6)\n  );\n\n  CREATE TABLE countries (\n    id int,\n    location coordinates,\n    languages text[]\n  );\n\n.. code-block:: bash\n\n  # curl \"http://localhost:3000/countries?select=id,location->>lat,location->>long,primary_language:languages->0&location->lat=gte.19\"\n\n  curl --get \"http://localhost:3000/countries\" \\\n    -d \"select=id,location->>lat,location->>long,primary_language:languages->0\" \\\n    -d \"location->lat=gte.19\"\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 5,\n      \"lat\": \"19.741755\",\n      \"long\": \"-155.844437\",\n      \"primary_language\": \"en\"\n    }\n  ]\n\n.. important::\n\n  When using the ``->`` and ``->>`` operators on composite and array columns, PostgREST uses a query like ``to_jsonb(<col>)->'field'``. To make filtering and ordering on those nested fields use an index, the index needs to be created on the same expression, including the ``to_jsonb(...)`` call:\n\n  .. code-block:: postgres\n\n    CREATE INDEX ON mytable ((to_jsonb(data) -> 'identification' ->> 'registration_number'));\n\n.. _casting_columns:\n\nCasting Columns\n~~~~~~~~~~~~~~~\n\nCasting the columns is possible by suffixing them with the double colon ``::`` plus the desired type.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?select=full_name,salary::text\"\n\n.. code-block:: json\n\n  [\n    {\"full_name\": \"John Doe\", \"salary\": \"90000.00\"},\n    {\"full_name\": \"Jane Doe\", \"salary\": \"120000.00\"}\n  ]\n\n.. note::\n\n  To prevent invalidating :ref:`index_usage`, casting on horizontal filtering is not allowed. To do this, you can use :ref:`computed_cols`.\n\n.. _ordering:\n\nOrdering\n--------\n\nThe reserved word ``order`` reorders the response rows. It uses a comma-separated list of columns and directions:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?order=age.desc,height.asc\"\n\nIf no direction is specified it defaults to ascending order:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?order=age\"\n\nIf you care where nulls are sorted, add ``nullsfirst`` or ``nullslast``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?order=age.nullsfirst\"\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?order=age.desc.nullslast\"\n\nYou can also sort on fields of :ref:`composite_array_columns` or :ref:`json_columns`.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/countries?order=location->>lat\"\n\n.. _index_usage:\n\nIndex Usage\n-----------\n\nIndexes work transparently when using horizontal filtering, vertical filtering and ordering. For example, when having:\n\n.. code-block:: postgresql\n\n  create index salary_idx on employees (salary);\n\nWe can confirm that a filter on employees uses the index by getting the :ref:`explain_plan`.\n\n.. code-block:: bash\n\n  curl 'localhost:3000/employees?salary=eq.36000' -H \"Accept: application/vnd.pgrst.plan\"\n\n  Aggregate  (cost=9.52..9.54 rows=1 width=144)\n    ->  Bitmap Heap Scan on employees  (cost=4.16..9.50 rows=2 width=136)\n          Recheck Cond: (salary = '$36,000.00'::money)\n          ->  Bitmap Index Scan on salary_idx  (cost=0.00..4.16 rows=2 width=0)\n                Index Cond: (salary = '$36,000.00'::money)\n\nThere we can see `\"Index Cond\" <https://www.pgmustard.com/docs/explain/index-cond>`_, which confirms the index is being used by the query planner.\n\n.. _insert:\n\nInsert\n======\n\nAll tables and `auto-updatable views <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ can be modified through the API, subject to permissions of the requester's database role.\n\nTo create a row in a database table post a JSON object whose keys are the names of the columns you would like to create. Missing properties will be set to default values when applicable.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/table_name\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d '{ \"col1\": \"value1\", \"col2\": \"value2\" }'\n\n.. code::\n\n  HTTP/1.1 201 Created\n\nNo response body will be returned by default but you can use :ref:`prefer_return` to get the affected resource and :ref:`resource_embedding` to add related resources.\n\nx-www-form-urlencoded\n---------------------\n\nURL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" \\\n    -X POST -H \"Content-Type: application/x-www-form-urlencoded\" \\\n    -d \"name=John+Doe&age=50&weight=80\"\n\n.. note::\n\n  When inserting a row you must post a JSON object, not quoted JSON.\n\n  .. code::\n\n    Yes\n    { \"a\": 1, \"b\": 2 }\n\n    No\n    \"{ \\\"a\\\": 1, \\\"b\\\": 2 }\"\n\n  Some JavaScript libraries will post the data incorrectly if you're not careful. For best results try one of the :ref:`clientside_libraries` built for PostgREST.\n\n.. important::\n\n  It's recommended that you `use triggers instead of rules <https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_rules>`_.\n  Insertion on views with complex `rules <https://www.postgresql.org/docs/current/sql-createrule.html>`_ might not work out of the box with PostgREST due to its usage of CTEs.\n  If you want to keep using rules, a workaround is to wrap the view insertion in a function and call it through the :ref:`functions` interface.\n  For more details, see this `github issue <https://github.com/PostgREST/postgrest/issues/1283>`_.\n\n.. _bulk_insert:\n\nBulk Insert\n-----------\n\nBulk insert works exactly like single row insert except that you provide either a JSON array of objects having uniform keys, or lines in CSV format. This not only minimizes the HTTP requests required but uses a single INSERT statement on the back-end for efficiency.\n\nTo bulk insert CSV simply post to a table route with :code:`Content-Type: text/csv` and include the names of the columns as the first row. For instance\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" \\\n    -X POST -H \"Content-Type: text/csv\" \\\n    --data-binary @- << EOF\n  name,age,height\n  J Doe,62,70\n  Jonas,10,55\n  EOF\n\nAn empty field (:code:`,,`) is coerced to an empty string and the reserved word :code:`NULL` is mapped to the SQL null value. Note that there should be no spaces between the column names and commas.\n\nTo bulk insert JSON post an array of objects having all-matching keys\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    [\n      { \"name\": \"J Doe\", \"age\": 62, \"height\": 70 },\n      { \"name\": \"Janus\", \"age\": 10, \"height\": 55 }\n    ]\n  EOF\n\n\n.. _specify_columns:\n\nSpecifying Columns\n------------------\n\nBy using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted and ignore the rest of the payload.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/datasets?columns=source,publication_date,figure\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -d @- << EOF\n    {\n      \"source\": \"Natural Disaster Prevention and Control\",\n      \"publication_date\": \"2015-09-11\",\n      \"figure\": 1100,\n      \"location\": \"...\",\n      \"comment\": \"...\",\n      \"extra\": \"...\",\n      \"stuff\": \"...\"\n    }\n  EOF\n\nIn this case, only **source**, **publication_date** and **figure** will be inserted. The rest of the JSON keys will be ignored.\n\nUsing this also has the side-effect of being more efficient for :ref:`bulk_insert` since PostgREST will not process the JSON and\nit'll send it directly to PostgreSQL.\n\n.. _update:\n\nUpdate\n======\n\nTo update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to specify which record(s) to update. Here is an example query setting the :code:`category` column to child for all people below a certain age.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people?age=lt.13\" \\\n    -X PATCH -H \"Content-Type: application/json\" \\\n    -d '{ \"category\": \"child\" }'\n\nUpdates also support:\n\n- :ref:`prefer_return`\n- :ref:`resource_embedding`\n- :ref:`v_filter`\n- :ref:`Missing Preference <prefer_missing>`\n- :ref:`specify_columns`\n\n.. warning::\n\n  Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`.\n\n.. _prefer_resolution:\n\n.. _upsert:\n\nUpsert\n======\n\nYou can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/products\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -H \"Prefer: resolution=merge-duplicates\" \\\n    -d @- << EOF\n    [\n      { \"sku\": \"CL2031\", \"name\": \"Existing T-shirt\", \"price\": 35 },\n      { \"sku\": \"CL2040\", \"name\": \"Existing Hoodie\", \"price\": 60 },\n      { \"sku\": \"AC1022\", \"name\": \"New Cap\", \"price\": 30 }\n    ]\n  EOF\n\nBy default, upsert operates based on the primary key columns, so you must specify all of them.\nYou can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`.\nUpsert works best when the primary key is natural (e.g. ``sku``).\nHowever, it can work with surrogate primary keys (e.g. ``id serial primary key``), if you also do a :ref:`bulk_insert` with :ref:`prefer_missing`:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/employees?columns=id,name,salary\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -H \"Prefer: resolution=merge-duplicates, missing=default\" \\\n    -d @- << EOF\n    [\n      { \"id\": 1, \"name\": \"Existing employee 1\", \"salary\": 30000 },\n      { \"id\": 2, \"name\": \"Existing employee 2\", \"salary\": 42000 },\n      { \"name\": \"New employee 3\", \"salary\": 50000 }\n    ]\n  EOF\n\n.. important::\n  After creating a table or changing its primary key, you must refresh PostgREST schema cache for upsert to work properly. To learn how to refresh the cache see :ref:`schema_reloading`.\n\n.. _on_conflict:\n\nOn Conflict\n-----------\n\nBy specifying the ``on_conflict`` query parameter, you can make upsert work on a column(s) that has a UNIQUE constraint.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/employees?on_conflict=name\" \\\n    -X POST -H \"Content-Type: application/json\" \\\n    -H \"Prefer: resolution=merge-duplicates\" \\\n    -d @- << EOF\n    [\n      { \"name\": \"Old employee 1\", \"salary\": 40000 },\n      { \"name\": \"Old employee 2\", \"salary\": 52000 },\n      { \"name\": \"New employee 3\", \"salary\": 60000 }\n    ]\n  EOF\n\n.. _upsert_put:\n\nPUT\n---\n\nA single row upsert can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`:\n\n.. code-block:: bash\n\n  curl \"http://localhost/employees?id=eq.4\" \\\n    -X PUT -H \"Content-Type: application/json\" \\\n    -d '{ \"id\": 4, \"name\": \"Sara B.\", \"salary\": 60000 }'\n\nAll the columns must be specified in the request body, including the primary key columns.\n\n.. _delete:\n\nDelete\n======\n\nTo delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/user?active=is.false\" -X DELETE\n\nDeletions also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:`v_filter`.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/user?id=eq.1\" -X DELETE \\\n    -H \"Prefer: return=representation\"\n\n.. code-block:: json\n\n  {\"id\": 1, \"email\": \"johndoe@email.com\"}\n\n.. warning::\n\n  Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.\n\n.. raw:: html\n\n  <script type=\"text/javascript\">\n    let hash = window.location.hash;\n\n    const redirects = {\n      // Tables and Views\n      '#computed-virtual-columns': 'computed_fields.html#computed-fields',\n      '#limits-and-pagination': 'pagination_count.html#limits-and-pagination',\n      '#exact-count': 'pagination_count.html#exact-count',\n      '#planned-count': 'pagination_count.html#planned-count',\n      '#estimated-count': 'pagination_count.html#estimated-count',\n      '#prefer-return-headers-only': 'preferences.html#headers-only',\n      '#prefer-return-representation': 'preferences.html#full',\n      '#bulk-insert-default': 'preferences.html#prefer-missing',\n    };\n\n    let willRedirectTo = redirects[hash];\n\n    if (willRedirectTo) {\n      window.location.href = willRedirectTo;\n    }\n  </script>\n"
  },
  {
    "path": "docs/references/api/url_grammar.rst",
    "content": ".. note::\n\n  This page is a work in progress.\n\n.. _url_grammar:\n\nURL Grammar\n===========\n\n.. _custom_queries:\n\nCustom Queries\n--------------\n\nThe PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are\n\n* Table unions\n* More complicated joins than those provided by :ref:`resource_embedding`.\n* Geo-spatial queries that require an argument, like \"points near (lat,lon)\"\n\nUnicode support\n---------------\n\nPostgREST supports unicode in schemas, tables, columns and values. To access a table with unicode name, use percent encoding.\n\nTo request this:\n\n.. code-block:: http\n\n  GET /موارد HTTP/1.1\n\nDo this:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/%D9%85%D9%88%D8%A7%D8%B1%D8%AF\"\n\n.. _tabs-cols-w-spaces:\n\nTable / Columns with spaces\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can request table/columns with spaces in them by percent encoding the spaces with ``%20``:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/Order%20Items?Unit%20Price=lt.200\"\n\n.. _reserved-chars:\n\nReserved characters\n~~~~~~~~~~~~~~~~~~~\n\nIf filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``*``, ``(``, ``)``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.\n\nHere ``Hebdon,John`` and ``Williams,Mary`` are values.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22)\"\n\nHere ``information.cpe`` is a column name.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/vulnerabilities?%22information.cpe%22=like.*MS*\"\n\nIf the value filtered by the ``in`` operator has a double quote (``\"``), you can escape it using a backslash ``\"\\\"\"``. A backslash itself can be used with a double backslash ``\"\\\\\"``.\n\nHere ``Quote:\"`` and ``Backslash:\\`` are percent-encoded values. Note that ``%5C`` is the percent-encoded backslash.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22)\"\n\n.. note::\n\n   Some HTTP libraries might encode URLs automatically(e.g. :code:`axios`). In these cases you should use double quotes\n   :code:`\"\"` directly instead of :code:`%22`.\n"
  },
  {
    "path": "docs/references/api/vary_header.rst",
    "content": ".. _vary_header:\n\nVary Header\n===========\n\nIn order to assist caching proxies and CDNs, PostgREST includes a ``Vary`` header of value\n``Accept, Prefer, Range`` in its responses which should fit most of the bills. As any other\nresponse header, it's available for override\nby updating ``response.headers`` GUC variable accordingly, for example:\n\n.. code-block:: postgres\n\n   -- Override the Vary header to include Accept, Prefer and X-Test-Vary headers\n   perform set_config('response.headers', '[{\"Vary\": \"Accept, Prefer, X-Test-Vary\"}]', true);\n\nIn this case PostgREST will use provided value verbatim.\n"
  },
  {
    "path": "docs/references/api.rst",
    "content": ".. _api:\n\nAPI\n###\n\nPostgREST exposes three database objects of a schema as resources: tables, views and functions.\n\n.. toctree::\n   :glob:\n   :maxdepth: 1\n\n   api/tables_views.rst\n   api/functions.rst\n   api/schemas.rst\n   api/computed_fields.rst\n   api/domain_representations.rst\n   api/pagination_count.rst\n   api/resource_embedding.rst\n   api/resource_representation.rst\n   api/media_type_handlers.rst\n   api/aggregate_functions.rst\n   api/openapi.rst\n   api/preferences.rst\n   api/vary_header.rst\n   api/*\n\n.. raw:: html\n\n  <script type=\"text/javascript\">\n    let hash = window.location.hash;\n\n    const redirects = {\n      // Tables and Views\n      '#horizontal-filtering-rows': 'api/tables_views.html#horizontal-filtering',\n      '#operators': 'api/tables_views.html#operators',\n      '#logical-operators': 'api/tables_views.html#logical-operators',\n      '#pattern-matching': 'api/tables_views.html#pattern-matching',\n      '#full-text-search': 'api/tables_views.html#full-text-search',\n      '#vertical-filtering-columns': 'api/tables_views.html#vertical-filtering',\n      '#renaming-columns': 'api/tables_views.html#renaming-columns',\n      '#casting-columns': 'api/tables_views.html#casting-columns',\n      '#json-columns': 'api/tables_views.html#json-columns',\n      '#composite-array-columns': 'api/tables_views.html#composite-array-columns',\n      '#computed-virtual-columns': 'api/computed_fields.html',\n      '#ordering': 'api/tables_views.html#ordering',\n      '#limits-and-pagination': 'api/pagination_count.html',\n      '#exact-count': 'api/pagination_count.html#exact-count',\n      '#planned-count': 'api/pagination_count.html#planned-count',\n      '#estimated-count': 'api/pagination_count.html#estimated-count',\n      '#updates': 'api/tables_views.html#update',\n      '#insertions': 'api/tables_views.html#insert',\n      '#bulk-insert': 'api/tables_views.html#bulk-insert',\n      '#specifying-columns': 'api/tables_views.html#specifying-columns',\n      '#upsert': 'api/tables_views.html#upsert',\n      '#on-conflict': 'api/tables_views.html#on-conflict',\n      '#put': 'api/tables_views.html#put',\n      '#deletions': 'api/tables_views.html#delete',\n      '#limited-updates-deletions': 'api/tables_views.html#limited-update-delete',\n      // Functions\n      '#stored-procedures': 'api/functions.html',\n      '#calling-functions-with-a-single-json-parameter': 'api/functions.html#functions-with-a-single-json-parameter',\n      '#calling-functions-with-a-single-unnamed-parameter': 'api/functions.html#functions-with-a-single-unnamed-parameter',\n      '#calling-functions-with-array-parameters': 'api/functions.html#functions-with-array-parameters',\n      '#calling-variadic-functions': 'api/functions.html#variadic-functions',\n      '#scalar-functions': 'api/functions.html#scalar-functions',\n      '#function-filters': 'api/functions.html#table-valued-functions',\n      '#overloaded-functions': 'api/functions.html#overloaded-functions',\n      // Schemas\n      '#switching-schemas': 'api/schemas.html',\n      // Resource Embedding\n      '#resource-embedding': 'api/resource_embedding.html#resource-embedding',\n      '#many-to-one-relationships': 'api/resource_embedding.html#many-to-one-relationships',\n      '#one-to-many-relationships': 'api/resource_embedding.html#one-to-many-relationships',\n      '#many-to-many-relationships': 'api/resource_embedding.html#many-to-many-relationships',\n      '#one-to-one-relationships': 'api/resource_embedding.html#one-to-one-relationships',\n      '#computed-relationships': 'api/resource_embedding.html#computed-relationships',\n      '#nested-embedding': 'api/resource_embedding.html#nested-embedding',\n      '#embedded-filters': 'api/resource_embedding.html#embedded-filters',\n      '#embedding-with-top-level-filtering': 'api/resource_embedding.html#top-level-filtering',\n      '#embedding-partitioned-tables': 'api/resource_embedding.html#foreign-key-joins-on-partitioned-tables',\n      '#embedding-views': 'api/resource_embedding.html#foreign-key-joins-on-views',\n      '#embedding-chains-of-views': 'api/resource_embedding.html#foreign-key-joins-on-chains-of-views',\n      '#embedding-on-stored-procedures': 'api/resource_embedding.html#foreign-key-joins-on-table-valued-functions',\n      '#embedding-after-insertions-updates-deletions': 'api/resource_embedding.html#foreign-key-joins-on-writes',\n      '#embedding-disambiguation': 'api/resource_embedding.html#foreign-key-joins-on-multiple-foreign-key-relationships',\n      '#target-disambiguation': 'api/resource_embedding.html#foreign-key-joins-on-multiple-foreign-key-relationships',\n      '#hint-disambiguation': 'api/resource_embedding.html#foreign-key-joins-on-multiple-foreign-key-relationships',\n      \"#embedding-through-join-tables\": \"api/resource_embedding.html#many-to-many-relationships\",\n      // OpenAPI\n      '#openapi-support': 'api/openapi.html',\n      // Resource Representation\n      '#response-format': 'api/resource_representation.html#response-format',\n      '#singular-or-plural': 'api/resource_representation.html#singular-or-plural',\n      '#response-formats-for-scalar-responses': 'api/functions.html#scalar-functions',\n      // CORS\n      '#cors': 'api/cors.html',\n      // OPTIONS\n      '#options': 'api/options.html',\n      // URL Grammar\n      '#custom-queries': 'api/url_grammar.html#custom-queries',\n      '#unicode-support': 'api/url_grammar.html#unicode-support',\n      '#table-columns-with-spaces': 'api/url_grammar.html#table-columns-with-spaces',\n      '#reserved-characters': 'api/url_grammar.html#reserved-characters',\n      // Transactions\n      '#immutable-and-stable-functions': 'transactions.html#access-mode',\n      '#http-context': 'transactions.html#transaction-scoped-settings',\n      '#accessing-request-headers-cookies-and-jwt-claims': 'transactions.html#request-headers-cookies-and-jwt-claims',\n      '#legacy-guc-variable-names': 'transactions.html#transaction-scoped-settings',\n      '#accessing-request-path-and-method': 'transactions.html#request-path-and-method',\n      '#setting-response-headers': 'transactions.html#response-headers',\n      '#setting-headers-via-pre-request': 'transactions.html#setting-headers-via-pre-request',\n      '#setting-response-status-code': 'transactions.html#response-status-code',\n      '#raise-errors-with-http-status-codes': 'errors.html#raise-errors-with-http-status-codes',\n      // Admin\n      '#execution-plan': 'observability.html#execution-plan',\n      // Deprecated\n      '#bulk-call': '../releases/v11.0.1.html#breaking-changes',\n    };\n\n    let willRedirectTo = redirects[hash];\n\n    if (willRedirectTo) {\n      window.location.href = willRedirectTo;\n    }\n  </script>\n"
  },
  {
    "path": "docs/references/auth.rst",
    "content": ".. _authn:\n\nAuthentication\n==============\n\nPostgREST is designed to keep the database at the center of API security. All :ref:`authorization happens in the database <db_authz>` . It is PostgREST's job to **authenticate** requests -- i.e. verify that a client is who they say they are -- and then let the database **authorize** client actions.\n\n.. _roles:\n\nOverview of role system\n-----------------------\n\nThere are three types of roles used by PostgREST, the **authenticator**, **anonymous** and **user** roles. The database administrator creates these roles and configures PostgREST to use them.\n\n.. image:: ../_static/security-roles.png\n\nThe authenticator role is used for connecting to the database and should be configured to have very limited access. It is a chameleon whose job is to \"become\" other users to service authenticated HTTP requests.\n\n\n.. code:: sql\n\n\n  CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;\n  CREATE ROLE anonymous NOLOGIN;\n  CREATE ROLE webuser NOLOGIN;\n\n.. note::\n\n  The names \"authenticator\" and \"anon\" names are configurable and not sacred, we simply choose them for clarity. See :ref:`db-uri` and :ref:`db-anon-role`.\n\n.. _user_impersonation:\n\nUser Impersonation\n~~~~~~~~~~~~~~~~~~\n\nThe picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`).\n\n.. image:: ../_static/security-anon-choice.png\n\nThis role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement.\n\n.. note::\n\n  The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.\n\n.. _jwt_auth:\n\nJWT Authentication\n------------------\n\nWe use `JSON Web Tokens <https://datatracker.ietf.org/doc/html/rfc7519/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification.\nAs you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role (configurable with :ref:`jwt_role_extract`).\n\n.. code:: json\n\n  {\n    \"role\": \"user123\"\n  }\n\nWhen a request contains a valid JWT with a role claim PostgREST will switch to the database role with that name for the duration of the HTTP request.\n\n.. code:: sql\n\n  SET LOCAL ROLE user123;\n\nNote that the database administrator must allow the authenticator role to switch into this user by previously executing\n\n.. code:: sql\n\n  GRANT user123 TO authenticator;\n  -- similarly for the anonymous role\n  -- GRANT anonymous TO authenticator;\n\nIf the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't.\n\n.. _bearer_auth:\n\nBearer Authentication\n~~~~~~~~~~~~~~~~~~~~~\n\nTo make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/foo\" \\\n    -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4\"\n\nThe ``Bearer`` header value can be used with or without capitalization(``bearer``).\n\n.. _jwt_generation:\n\nJWT Generation\n~~~~~~~~~~~~~~\n\nYou can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`).\n\n.. _jwt_signature:\n\nJWT Signature Verification\n--------------------------\n\nPostgREST supports both symmetric and asymmetric keys for verifying the signature of the token.\n\nSymmetric Keys\n~~~~~~~~~~~~~~\n\nIn the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`.\nIf it is set to a simple string then PostgREST interprets it as an HMAC-SHA256 passphrase.\n\n.. code-block:: ini\n\n  jwt-secret = \"reallyreallyreallyreallyverysafe\"\n\n.. _asym_keys:\n\nAsymmetric Keys\n~~~~~~~~~~~~~~~\n\nIn asymmetric cryptography the signer uses the private key and the verifier the public key.\n\nAs described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. However you can also specify a literal JSON Web Key (JWK) or set. For example, you can use an RSA-256 public key encoded as a JWK:\n\n.. code-block:: json\n\n  {\n    \"alg\":\"RS256\",\n    \"e\":\"AQAB\",\n    \"key_ops\":[\"verify\"],\n    \"kty\":\"RSA\",\n    \"n\":\"9zKNYTaYGfGm1tBMpRT6FxOYrM720GhXdettc02uyakYSEHU2IJz90G_MLlEl4-WWWYoS_QKFupw3s7aPYlaAjamG22rAnvWu-rRkP5sSSkKvud_IgKL4iE6Y2WJx2Bkl1XUFkdZ8wlEUR6O1ft3TS4uA-qKifSZ43CahzAJyUezOH9shI--tirC028lNg767ldEki3WnVr3zokSujC9YJ_9XXjw2hFBfmJUrNb0-wldvxQbFU8RPXip-GQ_JPTrCTZhrzGFeWPvhA6Rqmc3b1PhM9jY7Dur1sjYWYVyXlFNCK3c-6feo5WlRfe1aCWmwZQh6O18eTmLeT4nWYkDzQ\"\n  }\n\n.. note::\n\n  This could also be a JSON Web Key Set (JWKS) if it was contained within an array assigned to a `keys` member, e.g. ``{ keys: [jwk1, jwk2] }``.\n\nJust pass it in as a single line string, escaping the quotes:\n\n.. code-block:: ini\n\n  jwt-secret = \"{ \\\"alg\\\":\\\"RS256\\\", … }\"\n\nTo generate such a public/private key pair use a utility like `latchset/jose <https://github.com/latchset/jose>`_.\n\n.. code-block:: bash\n\n  jose jwk gen -i '{\"alg\": \"RS256\"}' -o rsa.jwk\n  jose jwk pub -i rsa.jwk -o rsa.jwk.pub\n\n  # now rsa.jwk.pub contains the desired JSON object\n\nYou can specify the literal value as we saw earlier, or reference a filename to load the JWK from a file:\n\n.. code-block:: ini\n\n  jwt-secret = \"@rsa.jwk.pub\"\n\n``kid`` verification\n^^^^^^^^^^^^^^^^^^^^\n\nPostgREST has built-in verification of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JSON Web Key Set.\nIt goes as follows:\n\n- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the :ref:`jwt-secret`.\n\n  + If no key has a matching ``kid`` (or if they don't have one defined), the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.\n  + If a key matches the ``kid`` value then it will validate the token against that key accordingly.\n\n- If the JWT doesn't have a ``kid``, PostgREST  will try each key in the :ref:`jwt-secret` one by one until it finds one that works.\n\n.. _jwt_claims_validation:\n\nJWT Claims Validation\n---------------------\n\nTime-Based claims validation\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe time-based JWT claims specified in `RFC 7519 <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_ are validated:\n\n- ``exp`` Expiration Time\n- ``iat`` Issued At\n- ``nbf`` Not Before\n\nWe allow a 30-second clock skew when validating the above claims. In other words, we give an extra 30 seconds before the JWT is rejected if there is a slight discrepancy in the timestamps.\n\n.. _jwt_aud:\n\n``aud`` validation\n~~~~~~~~~~~~~~~~~~\n\nPostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_.\nIt works this way:\n\n- If :ref:`jwt-aud` is not set (the default), PostgREST identifies with all audiences and allows the JWT for any ``aud`` claim.\n- If :ref:`jwt-aud` is set to a specific audience, PostgREST will check if this audience is present in the ``aud`` claim:\n\n  + If the ``aud`` value is a JSON string, it will match it to the :ref:`jwt-aud`.\n  + If the ``aud`` value is a JSON array of strings, it will search every element for a match.\n  + If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error.\n  + If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request.\n\n.. _jwt_caching:\n\nJWT Cache\n---------\n\nJWT signature validation (specially :ref:`asym_keys` such as RSA) is slow, we can cache ``JWT`` validation results to avoid this performance overhead.\n\nThe JWT cache is bounded and uses the `SIEVE algorithm <https://cachemon.github.io/SIEVE-website>`_ for efficient eviction. The cache is enabled by default and can be configured with :ref:`jwt-cache-max-entries`.\n\nIt's recommended to leave the JWT cache enabled as our load tests indicate ~20% more throughput for simple GET requests when using it. This while reducing CPU utilization in exchange for a bit more memory.\n\n:ref:`jwt_cache_metrics` are available.\n\n.. note::\n\n  - If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.\n  - JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).\n  - You can use the :ref:`server-timing_header` to see the performance benefit of JWT caching.\n\n.. _jwt_role_extract:\n\nJWT Role Extraction\n-------------------\n\nA JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.\n\nThe DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:\n\n- ``==`` selects the first array element that exactly matches the right operand\n- ``!=`` selects the first array element that does not match the right operand\n- ``^==`` selects the first array element that starts with the right operand\n- ``==^`` selects the first array element that ends with the right operand\n- ``*==`` selects the first array element that contains the right operand\n\nThe selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:\n\n- ``[a:b]`` take slice from index ``a`` up to ``b``\n- ``[a:]`` take slice from index ``a`` to end\n- ``[:b]`` take slice from start to index ``b``\n- ``[:]`` select everything, no slicing\n\n.. important::\n\n  Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.\n\nUsage examples:\n\n  .. code:: bash\n\n    # {\"postgrest\":{\"roles\": [\"other\", \"author\"]}}\n    # the DSL accepts characters that are alphanumerical or one of \"_$@\" as keys\n    jwt-role-claim-key = \".postgrest.roles[1]\"\n\n    # {\"https://www.example.com/role\": { \"key\": \"author\" }}\n    # non-alphanumerical characters can go inside quotes(escaped in the config value)\n    jwt-role-claim-key = \".\\\"https://www.example.com/role\\\".key\"\n\n    # {\"postgrest\":{\"roles\": [\"other\", \"author\"]}}\n    # `@` represents the current element in the array\n    # all the these match the string \"author\"\n    jwt-role-claim-key = \".postgrest.roles[?(@ == \\\"author\\\")]\"\n    jwt-role-claim-key = \".postgrest.roles[?(@ != \\\"other\\\")]\"\n    jwt-role-claim-key = \".postgrest.roles[?(@ ^== \\\"aut\\\")]\"\n    jwt-role-claim-key = \".postgrest.roles[?(@ ==^ \\\"hor\\\")]\"\n    jwt-role-claim-key = \".postgrest.roles[?(@ *== \\\"utho\\\")]\"\n\n    # {\"postgrest\":{\"wlcg\": [\"/groupa\", \"/groupb/\"]}}\n    # skip the \"/\" character using slice operator\n    jwt-role-claim-key = \".postgrest.wlcg[0][1:]\"\n    jwt-role-claim-key = \".postgrest.wlcg[1][1:-1]\"\n\n.. note::\n\n  The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.\n\nJWT Security\n------------\n\nThere are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action.\n\nThe critique against the `JWT standard <https://datatracker.ietf.org/doc/html/rfc7519>`_ is voiced in detail `elsewhere on the web <https://web.archive.org/web/20230123041631/https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid>`_. The most relevant part for PostgREST is the so-called :code:`alg=none` issue. Some servers implementing JWT allow clients to choose the algorithm used to sign the JWT. In this case, an attacker could set the algorithm to :code:`none`, remove the need for any signature at all and gain unauthorized access. The current implementation of PostgREST, however, does not allow clients to set the signature algorithm in the HTTP request, making this attack irrelevant. The critique against the standard is that it requires the implementation of the :code:`alg=none` at all.\n\nAnother type of critique focuses on the misuse of JWT for maintaining web sessions. The basic recommendation is to `stop using JWT for sessions <http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/>`_ because most, if not all, solutions to the problems that arise when you do, `do not work <http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/>`_. The linked articles discuss the problems in depth but the essence of the problem is that JWT is not designed to be secure and stateful units for client-side storage and therefore not suited to session management.\n\nPostgREST uses JWT mainly for authentication and authorization purposes and encourages users to do the same. For web sessions, using cookies over HTTPS is good enough and well catered for by standard web frameworks.\n\n.. _custom_validation:\n\nCustom Validation\n-----------------\n\nPostgREST does not enforce any extra constraints besides JWT validation. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a function to call immediately after :ref:`user_impersonation` and before the main query itself runs.\n\n.. code:: ini\n\n  db-pre-request = \"public.check_user\"\n\nIn the function you can run arbitrary code to check the request and raise an exception(see :ref:`raise_error`) to block it if desired. Here you can take advantage of :ref:`guc_req_headers_cookies_claims` for\ndoing custom logic based on the web user info.\n\n.. code-block:: postgres\n\n  CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$\n  DECLARE\n    email text := current_setting('request.jwt.claims', true)::json->>'email';\n  BEGIN\n    IF email = 'evil.user@malicious.com' THEN\n      RAISE EXCEPTION 'No, you are evil'\n        USING HINT = 'Stop being so evil and maybe you can log in';\n    END IF;\n  END\n  $$ LANGUAGE plpgsql;\n\n.. raw:: html\n\n  <script type=\"text/javascript\">\n    let hash = window.location.hash;\n\n    const redirects = {\n      '#jwt-based-user-impersonation': '#jwt-authentication',\n      '#client-auth': '#bearer-authentication',\n      '#jwt-caching': '#jwt-cache',\n      '#jwk-kid-validation': '#kid-verification',\n      '#jwt-aud-claim-validation': '#aud-validation',\n    };\n\n    let willRedirectTo = redirects[hash];\n\n    if (willRedirectTo) {\n      window.location.href = willRedirectTo;\n    }\n  </script>\n"
  },
  {
    "path": "docs/references/cli.rst",
    "content": ".. _cli:\n\nCLI\n===\n\nPostgREST provides a CLI with the options listed below:\n\n.. code:: text\n\n  Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]\n                 [FILENAME]\n\n    PostgREST / create a REST API to an existing Postgres\n    database\n\n  Available options:\n    -h,--help                Show this help text\n    -v,--version             Show the version information\n    -e,--example             Show an example configuration file\n    --dump-config            Dump loaded configuration and exit\n    --dump-schema            Dump loaded schema as JSON and exit (for debugging,\n                             output structure is unstable)\n    --ready                  Checks the health of PostgREST by doing a request on\n                             the admin server /ready endpoint\n    FILENAME                 Path to configuration file\n\nFILENAME\n--------\n\nRuns PostgREST with the given :ref:`file_config`.\n\nHelp\n----\n\n.. code:: bash\n\n  $ postgrest --help\n\nShows all the options available.\n\nVersion\n-------\n\n.. code:: bash\n\n  $ postgrest --version\n\nPrints the PostgREST version.\n\nExample\n-------\n\n.. code:: bash\n\n  $ postgrest --example\n\nShows example configuration settings.\n\nDump Config\n-----------\n\n.. code:: bash\n\n  $ postgrest --dump-config\n\nDumps the loaded :ref:`configuration` values, considering the configuration file, environment variables and :ref:`in_db_config`.\n\nDump Schema\n-----------\n\n.. code:: bash\n\n  $ postgrest --dump-schema\n\nDumps the schema cache in JSON format.\n\nReady Flag\n----------\n\nMakes a request to the ``/ready`` endpoint of the :ref:`admin_server`. It exits with a return code of ``0`` on success and ``1`` on failure.\n\n.. code-block:: bash\n\n  $ postgrest --ready\n  OK: http://localhost:3001/ready\n\n.. note::\n\n  The ``--ready`` flag cannot be used when :ref:`server-host` is configured with special hostnames. We suggest to change it to ``localhost``.\n"
  },
  {
    "path": "docs/references/configuration.rst",
    "content": ".. _configuration:\n\nConfiguration\n#############\n\nConfiguration parameters can be provided via:\n\n- :ref:`file_config`.\n- :ref:`env_variables_config`, overriding values from the config file.\n- :ref:`in_db_config`, overriding values from both the config file and environment variables.\n\nUsing :ref:`config_reloading` you can modify the parameters without restarting the server.\n\n\nMinimum parameters\n==================\n\nThe server is able to start without any config parameters, but it won't be able to serve requests unless it has :ref:`a role to serve anonymous requests with <db-anon-role>` - or :ref:`a secret to use for JWT authentication <jwt-secret>`.\n\n.. _file_config:\n\nConfig File\n===========\n\nThere is no predefined location for the config file, you must specify the file path as the one and only argument to the server:\n\n.. code:: bash\n\n  ./postgrest /path/to/postgrest.conf\n\nThe configuration file must contain a set of key value pairs:\n\n.. code::\n\n  # postgrest.conf\n\n  # The standard connection URI format, documented at\n  # https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING\n  db-uri       = \"postgres://user:pass@host:5432/dbname\"\n\n  # The database role to use when no client authentication is provided.\n  # Should differ from authenticator\n  db-anon-role = \"anon\"\n\n  # The secret to verify the JWT for authenticated requests with.\n  # Needs to be 32 characters minimum.\n  jwt-secret           = \"reallyreallyreallyreallyverysafe\"\n  jwt-secret-is-base64 = false\n\n  # Port the postgrest process is listening on for http requests\n  server-port = 3000\n\nYou can run ``postgrest --example`` to display all possible configuration parameters and how to use them in a configuration file.\n\n.. _env_variables_config:\n\nEnvironment Variables\n=====================\n\nEnvironment variables are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri`` and ``PGRST_APP_SETTINGS_*`` to ``app.settings.*``.\n\n`libpq environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`_ are also supported for constructing the connection string, see :ref:`db-uri`.\n\nSee the full list of environment variable names on :ref:`config_full_list`.\n\n.. _in_db_config:\n\nIn-Database Configuration\n=========================\n\nYou can also configure the server with database settings by using a :ref:`pre-config <db-pre-config>` function. For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this:\n\n.. code-block::\n\n  # postgrest.conf\n\n  db-pre-config  = \"postgrest.pre_config\"\n\n  # or env vars\n\n  PGRST_DB_PRE_CONFIG = \"postgrest.pre_config\"\n\n.. code-block:: postgres\n\n  -- create a dedicated schema, hidden from the API\n  create schema postgrest;\n  -- grant usage on this schema to the authenticator\n  grant usage on schema postgrest to authenticator;\n\n  -- the function can configure postgREST by using set_config\n  create or replace function postgrest.pre_config()\n  returns void as $$\n    select\n        set_config('pgrst.db_schemas', 'schema1, schema2', true)\n      , set_config('pgrst.jwt_secret', 'REALLYREALLYREALLYREALLYVERYSAFE', true);\n  $$ language sql;\n\nNote that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. See the full list of in-database names on :ref:`config_full_list`.\n\nYou can disable the in-database configuration by setting :ref:`db-config` to ``false``.\n\n.. note::\n  For backwards compatibility, you can do in-db config by modifying the :ref:`authenticator role <roles>`. This is no longer recommended as it requires SUPERUSER.\n\n  .. code:: postgresql\n\n     ALTER ROLE authenticator SET pgrst.db_schemas = \"tenant1, tenant2, tenant3\"\n     ALTER ROLE authenticator IN DATABASE <your_database_name> SET pgrst.db_schemas = \"tenant4, tenant5\" -- database-specific setting, overrides the previous setting\n\n.. _config_reloading:\n\nConfiguration Reloading\n=======================\n\nIt's possible to reload PostgREST's configuration without restarting the server. You can do this :ref:`via signal <config_reloading_signal>` or :ref:`via notification <config_reloading_notify>`.\n\n- Any modification to the :ref:`file_config` will be applied during reload.\n- Any modification to the :ref:`in_db_config` will be applied during reload.\n- Not all settings are reloadable, see the reloadable list on :ref:`config_full_list`.\n- It's not possible to change :ref:`env_variables_config` for a running process, hence reloading a Docker container configuration will not work. In these cases, you can restart the process or use :ref:`in_db_config`.\n\n.. _config_reloading_signal:\n\nConfiguration Reload with signal\n--------------------------------\n\nTo reload the configuration via signal, send a SIGUSR2 signal to the server process.\n\n.. code:: bash\n\n  killall -SIGUSR2 postgrest\n\n.. _config_reloading_notify:\n\nConfiguration Reload with NOTIFY\n--------------------------------\n\nTo reload the configuration from within the database, you can use the ``NOTIFY`` command. See :ref:`listener`.\n\n.. code:: postgresql\n\n   NOTIFY pgrst, 'reload config'\n\n.. _config_full_list:\n\nList of parameters\n==================\n\n.. _admin-server-host:\n\nadmin-server-host\n-----------------\n\n  =============== =======================\n  **Type**        String\n  **Default**     `server-host` value\n  **Reloadable**  N\n  **Environment** PGRST_ADMIN_SERVER_HOST\n  **In-Database** `n/a`\n  =============== =======================\n\n  Specifies the host for the :ref:`admin_server`. Defaults to :ref:`server-host` value.\n\n.. _admin-server-port:\n\nadmin-server-port\n-----------------\n\n  =============== =======================\n  **Type**        Int\n  **Default**     `n/a`\n  **Reloadable**  N\n  **Environment** PGRST_ADMIN_SERVER_PORT\n  **In-Database** `n/a`\n  =============== =======================\n\n  Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.\n\n.. _app.settings.*:\n\napp.settings.*\n--------------\n\n  =============== =======================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  &\n  **Environment** PGRST_APP_SETTINGS_*\n  **In-Database** `n/a`\n  =============== =======================\n\n  Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = \"$(MYAPP_JWT_SECRET)\"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to PostgreSQL functions as :code:`current_setting('app.settings.jwt_secret')`.\n\n  When using the environment variable `PGRST_APP_SETTINGS_*` form, the remainder of the variable is used as the new name. Case is not important : :code:`PGRST_APP_SETTINGS_MY_ENV_VARIABLE=some_value` can be accessed in postgres as :code:`current_setting('app.settings.my_env_variable')`.\n\n  The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not \"rolled back\" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.\n\n.. _client-error-verbosity:\n\nclient-error-verbosity\n----------------------\n\n  =============== =======================\n  **Type**        String\n  **Default**     verbose\n  **Reloadable**  Y\n  **Environment** PGRST_CLIENT_ERROR_VERBOSITY\n  **In-Database** pgrst.client_error_verbosity\n  =============== =======================\n\n  Specifies the verbosity of PostgREST errors. See :ref:`client_error_verbosity`.\n\n  .. code:: bash\n\n    # Return error \"code\", \"message\", \"details\" and \"hint\"\n    client-error-verbosity = \"verbose\"\n\n    # Return only \"code\" and \"message\"\n    client-error-verbosity = \"minimal\"\n\n  .. note::\n\n    This setting only affects client side error messages. Server side logs are not affected by this setting.\n\n.. _db-aggregates-enabled:\n\ndb-aggregates-enabled\n---------------------\n\n  =============== =======================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_DB_AGGREGATES_ENABLED\n  **In-Database** pgrst.db_aggregates_enabled\n  =============== =======================\n\n\n  When this is set to :code:`true`, the use of :ref:`aggregate_functions` is allowed.\n\n  It is recommended that this be set to ``false`` unless proper safeguards are in place to prevent potential performance problems from arising. For example, it is possible that a user may request the ``max()`` of an unindexed column in a table with millions of rows. At best, this would result in a slow query, and at worst, it could be abused to prevent other users from accessing your API (i.e. a form of denial-of-service attack.)\n\n  Proper safeguards could include:\n    - Use of a statement timeout. See :ref:`impersonated_settings`.\n    - Use of the `pg_plan_filter extension <https://github.com/pgexperts/pg_plan_filter>`_ to block excessively expensive queries.\n\n.. _db-anon-role:\n\ndb-anon-role\n------------\n\n  =============== =======================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_DB_ANON_ROLE\n  **In-Database** pgrst.db_anon_role\n  =============== =======================\n\n  The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`.\n\n  When unset anonymous access will be blocked.\n\n.. _db-channel:\n\ndb-channel\n----------\n\n  =============== =======================\n  **Type**        String\n  **Default**     pgrst\n  **Reloadable**  Y\n  **Environment** PGRST_DB_CHANNEL\n  **In-Database** `n/a`\n  =============== =======================\n\n  The name of the notification channel that PostgREST uses for :ref:`schema_reloading_notify` and :ref:`config_reloading_notify`.\n\n.. _db-channel-enabled:\n\ndb-channel-enabled\n------------------\n\n  =============== =======================\n  **Type**        Boolean\n  **Default**     True\n  **Reloadable**  Y\n  **Environment** PGRST_DB_CHANNEL_ENABLED\n  **In-Database** `n/a`\n  =============== =======================\n\n  When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.\n\n  You should set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.\n\n.. _db-config:\n\ndb-config\n---------\n\n  =============== =======================\n  **Type**        Boolean\n  **Default**     True\n  **Reloadable**  Y\n  **Environment** PGRST_DB_CONFIG\n  **In-Database** `n/a`\n  =============== =======================\n\n   Enables the in-database configuration.\n\n.. _db-pre-config:\n\ndb-pre-config\n-------------\n\n  =============== =======================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_DB_PRE_CONFIG\n  **In-Database** pgrst.db_pre_config\n  =============== =======================\n\n   Name of the function that does :ref:`in_db_config`.\n\n.. _db-extra-search-path:\n\ndb-extra-search-path\n--------------------\n\n  =============== ==========================\n  **Type**        String\n  **Default**     public\n  **Reloadable**  Y\n  **Environment** PGRST_DB_EXTRA_SEARCH_PATH\n  **In-Database** pgrst.db_extra_search_path\n  =============== ==========================\n\n  Extra schemas to add to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request. These schemas tables, views and functions **don't get API endpoints**, they can only be referred from the database objects inside your :ref:`db-schemas`.\n\n  This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schemas`.\n\n  Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.\n\n.. important::\n\n  We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``\"\"``.\n\n.. _db-hoisted-tx-settings:\n\ndb-hoisted-tx-settings\n----------------------\n\n  =============== ==================================================================================\n  **Type**        String\n  **Default**     statement_timeout, plan_filter.statement_cost_limit, default_transaction_isolation\n  **Reloadable**  Y\n  **Environment** PGRST_DB_HOISTED_TX_SETTINGS\n  **In-Database** pgrst.db_hoisted_tx_settings\n  =============== ==================================================================================\n\n  Hoisted settings are allowed to be applied as transaction-scoped function settings. Multiple settings can be added in a comma-separated string, e.g. ``work_mem, statement_timeout``.\n\n.. _db-max-rows:\n\ndb-max-rows\n-----------\n\n  =============== ==========================\n  **Type**        Int\n  **Default**     ∞\n  **Reloadable**  Y\n  **Environment** PGRST_DB_MAX_ROWS\n  **In-Database** pgrst.db_max_rows\n  =============== ==========================\n\n  *For backwards compatibility, this config parameter is also available without prefix as \"max-rows\".*\n\n  A hard limit to the number of rows PostgREST will fetch from a view, table, or function. Limits payload size for accidental or malicious requests.\n\n.. _db-plan-enabled:\n\ndb-plan-enabled\n---------------\n\n  =============== ==========================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_DB_PLAN_ENABLED\n  **In-Database** pgrst.db_plan_enabled\n  =============== ==========================\n\n  When this is set to :code:`true`, the execution plan of a request can be retrieved by using the :code:`Accept: application/vnd.pgrst.plan` header. See :ref:`explain_plan`.\n\n.. _db-pool:\n\ndb-pool\n-------\n\n  =============== ==========================\n  **Type**        Int\n  **Default**     10\n  **Reloadable**  N\n  **Environment** PGRST_DB_POOL\n  **In-Database** n/a\n  =============== ==========================\n\n  Number of maximum connections to keep open in PostgREST's database pool.\n\n.. _db-pool-acquisition-timeout:\n\ndb-pool-acquisition-timeout\n---------------------------\n\n  =============== =================================\n  **Type**        Int\n  **Default**     10\n  **Reloadable**  N\n  **Environment** PGRST_DB_POOL_ACQUISITION_TIMEOUT\n  **In-Database** `n/a`\n  =============== =================================\n\n  Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database.\n\n.. _db-pool-max-idletime:\n\ndb-pool-max-idletime\n--------------------\n\n  =============== =================================\n  **Type**        Int\n  **Default**     30\n  **Reloadable**  N\n  **Environment** PGRST_DB_POOL_MAX_IDLETIME\n  **In-Database** `n/a`\n  =============== =================================\n\n   *For backwards compatibility, this config parameter is also available as \"db-pool-timeout\".*\n\n   Time in seconds to close idle pool connections.\n\n.. _db-pool-max-lifetime:\n\ndb-pool-max-lifetime\n--------------------\n\n  =============== =================================\n  **Type**        Int\n  **Default**     1800\n  **Reloadable**  N\n  **Environment** PGRST_DB_POOL_MAX_LIFETIME\n  **In-Database** `n/a`\n  =============== =================================\n\n  Specifies the maximum time in seconds of an existing connection in the pool.\n\n.. _db-pool-automatic-recovery:\n\ndb-pool-automatic-recovery\n--------------------------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     True\n  **Reloadable**  Y\n  **Environment** PGRST_DB_POOL_AUTOMATIC_RECOVERY\n  **In-Database** `n/a`\n  =============== =================================\n\n  Enables or disables connection retrying.\n\n  When disabled, PostgREST would terminate immediately after connection loss instead of retrying indefinitely. See :ref:`this section <automatic_recovery>` for more information.\n\n.. _db-pre-request:\n\ndb-pre-request\n--------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_DB_PRE_REQUEST\n  **In-Database** pgrst.db_pre_request\n  =============== =================================\n\n  *For backwards compatibility, this config parameter is also available without prefix as \"pre-request\".*\n\n  A schema-qualified function name to call right after the :ref:`tx_settings` are set. See :ref:`pre-request`.\n\n.. _db-prepared-statements:\n\ndb-prepared-statements\n----------------------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     True\n  **Reloadable**  Y\n  **Environment** PGRST_DB_PREPARED_STATEMENTS\n  **In-Database** pgrst.db_prepared_statements\n  =============== =================================\n\n  Enables or disables prepared statements.\n\n  When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.\n\n  You should only set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.\n\n.. _db-root-spec:\n\ndb-root-spec\n------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_DB_ROOT_SPEC\n  **In-Database** pgrst.db_root_spec\n  =============== =================================\n\n  Function to override the OpenAPI response. See :ref:`override_openapi`.\n\n.. _db-schemas:\n\ndb-schemas\n----------\n\n  =============== =================================\n  **Type**        String\n  **Default**     public\n  **Reloadable**  Y\n  **Environment** PGRST_DB_SCHEMAS\n  **In-Database** pgrst.db_schemas\n  =============== =================================\n\n  *For backwards compatibility, this config parameter is also available in singular as \"db-schema\".*\n\n  The list of database schemas to expose to clients. See :ref:`schemas`.\n\n.. _db-tx-end:\n\ndb-tx-end\n---------\n\n  =============== =================================\n  **Type**        String\n  **Default**     commit\n  **Reloadable**  N\n  **Environment** PGRST_DB_TX_END\n  **In-Database** pgrst.db_tx_end\n  =============== =================================\n\n  Specifies how to terminate the database transactions. See :ref:`prefer_tx`.\n\n  .. code:: bash\n\n    # The transaction is always committed\n    db-tx-end = \"commit\"\n\n    # The transaction is committed unless a \"Prefer: tx=rollback\" header is sent\n    db-tx-end = \"commit-allow-override\"\n\n    # The transaction is always rolled back\n    db-tx-end = \"rollback\"\n\n    # The transaction is rolled back unless a \"Prefer: tx=commit\" header is sent\n    db-tx-end = \"rollback-allow-override\"\n\n.. _db-uri:\n\ndb-uri\n------\n\n  =============== =================================\n  **Type**        String\n  **Default**     postgresql://\n  **Reloadable**  N\n  **Environment** PGRST_DB_URI\n  **In-Database** `n/a`\n  =============== =================================\n\n  The standard `PostgreSQL connection string <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>`_, there are different ways to specify it:\n\nURI Format\n~~~~~~~~~~\n\n  .. code::\n\n    \"postgres://authenticator:mysecretpassword@localhost:5433/postgres?parameters=val\"\n\n  - Under this format symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error.\n  - If enforcing an SSL connection to the database is required you can use `sslmode <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``.\n  - The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`.\n  - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ and the `Peer Authentication method <https://www.postgresql.org/docs/current/auth-peer.html>`_ as an alternative to TCP/IP communication and authentication with a password, this also grants higher performance.  To do this you can omit the host and the password, e.g. ``postgres://user@/dbname``, see the `libpq connection string <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>`_ documentation for more details.\n\nKeyword/Value Format\n~~~~~~~~~~~~~~~~~~~~\n\n  .. code::\n\n    \"host=localhost port=5433 user=authenticator password=mysecretpassword dbname=postgres\"\n\nLIBPQ Environment Variables\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n  .. code::\n\n    PGHOST=localhost PGPORT=5433 PGUSER=authenticator PGDATABASE=postgres\n\n  Any parameter that is not set in the above formats is read from `libpq environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`_. The default connection string is ``postgresql://``, which reads **all** parameters from the environment.\n\nExternal config file\n~~~~~~~~~~~~~~~~~~~~\n\n  Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the connection string out of an external file.\n\n.. _jwt-aud:\n\njwt-aud\n-------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_JWT_AUD\n  **In-Database** pgrst.jwt_aud\n  =============== =================================\n\n    Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud`.\n\n.. _jwt-role-claim-key:\n\njwt-role-claim-key\n------------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     .role\n  **Reloadable**  Y\n  **Environment** PGRST_JWT_ROLE_CLAIM_KEY\n  **In-Database** pgrst.jwt_role_claim_key\n  =============== =================================\n\n  *For backwards compatibility, this config parameter is also available without prefix as \"role-claim-key\".*\n\n  See :ref:`jwt_role_extract` on how to specify key paths and usage examples.\n\n.. _jwt-secret:\n\njwt-secret\n----------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_JWT_SECRET\n  **In-Database** pgrst.jwt_secret\n  =============== =================================\n\n  The secret or `JSON Web Key (JWK) (or set) <https://datatracker.ietf.org/doc/html/rfc7517>`_ used to decode JWT tokens clients provide for authentication. For security the key must be **at least 32 characters long**. If this parameter is not specified then PostgREST refuses authentication requests. Choosing a value for this parameter beginning with the at sign such as :code:`@filename` loads the secret out of an external file. This is useful for automating deployments. Note that any binary secrets must be base64 encoded. Both symmetric and asymmetric cryptography are supported. For more info see :ref:`asym_keys`.\n\n  Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the secret out of an external file.\n\n  .. warning::\n\n     Only when using the :ref:`file_config`, if the ``jwt-secret`` contains a ``$`` character by itself it will give errors. In this case, use ``$$`` and PostgREST will interpret it as a single ``$`` character.\n\n.. _jwt-secret-is-base64:\n\njwt-secret-is-base64\n--------------------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_JWT_SECRET_IS_BASE64\n  **In-Database** pgrst.jwt_secret_is_base64\n  =============== =================================\n\n  When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.\n\n.. _jwt-cache-max-entries:\n\njwt-cache-max-entries\n----------------------\n\n  =============== =================================\n  **Type**        Int\n  **Default**     1000\n  **Reloadable**  Y\n  **Environment** PGRST_JWT_CACHE_MAX_ENTRIES\n  **In-Database** pgrst.jwt_cache_max_entries\n  =============== =================================\n\n  Maximum number of entries in JWT cache. The value :code:`0` disables JWT caching. See :ref:`jwt_caching`.\n\n.. _log-level:\n\nlog-level\n---------\n\n  =============== =================================\n  **Type**        String\n  **Default**     error\n  **Reloadable**  N\n  **Environment** PGRST_LOG_LEVEL\n  **In-Database** `n/a`\n  =============== =================================\n\n  Specifies the level of information to be logged while running PostgREST.\n\n  .. code:: bash\n\n      # Only startup and db connection recovery messages are logged\n      log-level = \"crit\"\n\n      # All the \"crit\" level events plus server errors (status 5xx) are logged\n      log-level = \"error\"\n\n      # All the \"error\" level events plus request errors (status 4xx) are logged\n      log-level = \"warn\"\n\n      # All the \"warn\" level events plus all requests (every status code) are logged\n      log-level = \"info\"\n\n      # All the above plus events for development purposes are logged\n      # Logs connection pool events and the schema cache parsing time\n      log-level = \"debug\"\n\n  Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.\n\n.. _log-query:\n\nlog-query\n---------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_LOG_QUERY\n  **In-Database** `n/a`\n  =============== =================================\n\n  Logs the SQL query for the corresponding request at the current :ref:`log-level`. See :ref:`sql_query_logs`.\n\n.. _openapi-mode:\n\nopenapi-mode\n------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     follow-privileges\n  **Reloadable**  Y\n  **Environment** PGRST_OPENAPI_MODE\n  **In-Database** pgrst.openapi_mode\n  =============== =================================\n\n  Specifies how the OpenAPI output should be displayed.\n\n  .. code:: bash\n\n    # Follows the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent)\n    # Shows information depending on the permissions that the role making the request has\n    openapi-mode = \"follow-privileges\"\n\n    # Ignores the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent)\n    # Shows all the exposed information, regardless of the permissions that the role making the request has\n    openapi-mode = \"ignore-privileges\"\n\n    # Disables the OpenApi output altogether.\n    # Throws a `404 Not Found` error when accessing the API root path\n    openapi-mode = \"disabled\"\n\n.. _openapi-security-active:\n\nopenapi-security-active\n-----------------------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_OPENAPI_SECURITY_ACTIVE\n  **In-Database** pgrst.openapi_security_active\n  =============== =================================\n\nWhen this is set to :code:`true`, security options are included in the :ref:`OpenAPI output <open-api>`.\n\n.. _openapi-server-proxy-uri:\n\nopenapi-server-proxy-uri\n------------------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  N\n  **Environment** PGRST_OPENAPI_SERVER_PROXY_URI\n  **In-Database** pgrst.openapi_server_proxy_uri\n  =============== =================================\n\n  Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. Use a complete URI syntax :code:`scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]`. Ex. :code:`https://postgrest.com`\n\n  .. code:: json\n\n    {\n      \"swagger\": \"2.0\",\n      \"info\": {\n        \"version\": \"0.4.3.0\",\n        \"title\": \"PostgREST API\",\n        \"description\": \"This is a dynamic API generated by PostgREST\"\n      },\n      \"host\": \"postgrest.com:443\",\n      \"basePath\": \"/\",\n      \"schemes\": [\n        \"https\"\n      ]\n    }\n\n.. _server_cors_allowed_origins:\n\nserver-cors-allowed-origins\n---------------------------\n\n  =============== ===================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  N\n  **Environment** PGRST_SERVER_CORS_ALLOWED_ORIGINS\n  **In-Database** `pgrst.server_cors_allowed_origins`\n  =============== ===================================\n\n  Specifies allowed CORS origins in this config. See :ref:`cors`.\n\n  When this is not set or set to :code:`\"\"`, PostgREST **accepts** CORS requests from any domain.\n\n.. _server-host:\n\nserver-host\n-----------\n\n  =============== =================================\n  **Type**        String\n  **Default**     !4\n  **Reloadable**  N\n  **Environment** PGRST_SERVER_HOST\n  **In-Database** `n/a`\n  =============== =================================\n\n  Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings:\n\n  * :code:`*` - any IPv4 or IPv6 hostname\n  * :code:`*4` - any IPv4 or IPv6 hostname, IPv4 preferred\n  * :code:`!4` - any IPv4 hostname\n  * :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred\n  * :code:`!6` - any IPv6 hostname\n\n  Examples:\n\n  .. code:: bash\n\n    server-host = \"127.0.0.1\"\n\n.. _server-port:\n\nserver-port\n-----------\n\n  =============== =================================\n  **Type**        Int\n  **Default**     3000\n  **Reloadable**  N\n  **Environment** PGRST_SERVER_PORT\n  **In-Database** `n/a`\n  =============== =================================\n\n  The TCP port to bind the web server. Use ``0`` to automatically assign a port.\n\n.. _server-trace-header:\n\nserver-trace-header\n-------------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  Y\n  **Environment** PGRST_SERVER_TRACE_HEADER\n  **In-Database** pgrst.server_trace_header\n  =============== =================================\n\n  The header name used to trace HTTP requests. See :ref:`trace_header`.\n\n.. _server-timing-enabled:\n\nserver-timing-enabled\n---------------------\n\n  =============== =================================\n  **Type**        Boolean\n  **Default**     False\n  **Reloadable**  Y\n  **Environment** PGRST_SERVER_TIMING_ENABLED\n  **In-Database** pgrst.server_timing_enabled\n  =============== =================================\n\n  Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header.\n  See :ref:`server-timing_header`.\n\n.. _server-unix-socket:\n\nserver-unix-socket\n------------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     `n/a`\n  **Reloadable**  N\n  **Environment** PGRST_SERVER_UNIX_SOCKET\n  **In-Database** `n/a`\n  =============== =================================\n\n  `Unix domain socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ where to bind the PostgREST web server.\n  If specified, this takes precedence over :ref:`server-port`. Example:\n\n  .. code:: bash\n\n    server-unix-socket = \"/tmp/pgrst.sock\"\n\n.. _server-unix-socket-mode:\n\nserver-unix-socket-mode\n-----------------------\n\n  =============== =================================\n  **Type**        String\n  **Default**     660\n  **Reloadable**  N\n  **Environment** PGRST_SERVER_UNIX_SOCKET_MODE\n  **In-Database** `n/a`\n  =============== =================================\n\n  `Unix file mode <https://en.wikipedia.org/wiki/File_system_permissions>`_ to be set for the socket specified in :ref:`server-unix-socket`\n  Needs to be a valid octal between 600 and 777.\n\n  .. code:: bash\n\n    server-unix-socket-mode = \"660\"\n"
  },
  {
    "path": "docs/references/connection_pool.rst",
    "content": ".. _connection_pool:\n\nConnection Pool\n===============\n\nA connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. Every request to an :doc:`API resource <api>` borrows a connection from the pool to start a :doc:`transaction <transactions>`.\n\nMinimizing connections is paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources.\n\n.. _pool_growth_limit:\n.. _dyn_conn_pool:\n\nDynamic Connection Pool\n-----------------------\n\nTo conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic.\n\n- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it's pointless to set this higher than the ``max_connections`` setting in your database.\n- If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released.\n- For connecting to the database, the :ref:`authenticator <roles>` role is used. You can configure this using :ref:`db-uri`.\n\nConnection Application Name\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPostgREST sets the connection `application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ for all of its used connections.\nThis is useful for PostgreSQL statistics and logs.\n\nFor example, you can query `pg_stat_activity <https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW>`_ to get the PostgREST version:\n\n.. code-block:: postgres\n\n  select distinct usename, application_name\n  from pg_stat_activity\n  where usename = 'authenticator';\n\n     usename     |     application_name\n  ---------------+--------------------------\n   authenticator | PostgREST 12.1\n\n\nConnection lifetime\n-------------------\n\nLong-lived PostgreSQL connections can consume considerable memory (see `here <https://www.postgresql.org/message-id/CAFj8pRCQN2B2vrVMH1-bd-8xtzjytWR%2BAjZ%2BMCj9J2wPxKPa9Q%40mail.gmail.com>`_ for more details).\nUnder a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can be full of long-lived connections.\n\nTo avoid this problem and save resources, a connection max lifetime (:ref:`db-pool-max-lifetime`) is enforced.\nAfter the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released.\n\nAcquisition Timeout\n-------------------\n\nIf all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout (:ref:`db-pool-acquisition-timeout`).\n\nIf the request reaches the timeout, it will be aborted with the following response:\n\n.. code-block:: http\n\n  HTTP/1.1 504 Gateway Timeout\n\n  {\"code\":\"PGRST003\",\n   \"details\":null,\n   \"hint\":null,\n   \"message\":\"Timed out acquiring connection from connection pool.\"}\n\n.. important::\n\n  Getting this error message is an indicator of a performance issue. To solve it, you can:\n\n  - Reduce your queries execution time.\n\n    - Check the request :ref:`explain_plan` to tune your query, this usually means adding indexes.\n\n  - Reduce the amount of requests.\n\n    - Reduce write requests. Do :ref:`bulk_insert` (or :ref:`upsert`) instead of inserting rows one by one.\n    - Reduce read requests. Use :ref:`resource_embedding`. Combine unrelated data into a single request using custom database views or functions.\n    - Use :ref:`functions` for combining read and write logic into a single request.\n\n  - Increase the :ref:`db-pool` size.\n\n    - Not a panacea since connections can't grow infinitely. Try the previous recommendations before this.\n\n.. _automatic_recovery:\n\nAutomatic Recovery\n------------------\n\nThe server will retry reconnecting to the database if connection loss happens.\n\n- It will retry forever with exponential backoff, with a maximum backoff time of 32 seconds between retries. Each of these attempts are :ref:`logged <pgrst_logging>`.\n- It will only stop retrying if the server deems the error to be fatal. This can be a password authentication failure or an internal error.\n- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true (the default). Otherwise they'll happen once a request arrives.\n- To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering.\n- To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry.\n- Automatic recovery can be disabled by setting :ref:`db-pool-automatic-recovery` to ``false``.\n\n.. _external_connection_poolers:\n\nUsing External Connection Poolers\n---------------------------------\n\nIt's possible to use external connection poolers, such as PgBouncer. Session pooling is compatible, while transaction pooling requires :ref:`db-prepared-statements` set to ``false``. Statement pooling is not compatible with PostgREST.\n\nAlso set :ref:`db-channel-enabled` to ``false`` since ``LISTEN`` is not compatible with transaction pooling. Although it should not give any errors if left enabled.\n\n.. note::\n\n  It's not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool.\n"
  },
  {
    "path": "docs/references/errors.rst",
    "content": ".. _error_source:\n\nErrors\n######\n\nPostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.\n\n.. _postgresql_errors:\n\nErrors from PostgreSQL\n======================\n\nPostgREST will forward errors coming from PostgreSQL. For instance, on a failed constraint:\n\n.. code-block:: http\n\n  POST /projects HTTP/1.1\n\n.. code-block:: http\n\n  HTTP/1.1 400 Bad Request\n  Content-Type: application/json; charset=utf-8\n\n.. code-block:: json\n\n\n  {\n      \"code\": \"23502\",\n      \"details\": \"Failing row contains (null, foo, null).\",\n      \"hint\": null,\n      \"message\": \"null value in column \\\"id\\\" of relation \\\"projects\\\" violates not-null constraint\"\n  }\n\n.. _status_codes:\n\nHTTP Status Codes\n-----------------\n\nPostgREST translates `PostgreSQL error codes <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_ into HTTP status as follows:\n\n+--------------------------+-------------------------+---------------------------------+\n| PostgreSQL error code(s) | HTTP status             | Error description               |\n+==========================+=========================+=================================+\n| 08*                      | 503                     | pg connection err               |\n+--------------------------+-------------------------+---------------------------------+\n| 09*                      | 500                     | triggered action exception      |\n+--------------------------+-------------------------+---------------------------------+\n| 0L*                      | 403                     | invalid grantor                 |\n+--------------------------+-------------------------+---------------------------------+\n| 0P*                      | 403                     | invalid role specification      |\n+--------------------------+-------------------------+---------------------------------+\n| 23503                    | 409                     | foreign key violation           |\n+--------------------------+-------------------------+---------------------------------+\n| 23505                    | 409                     | uniqueness violation            |\n+--------------------------+-------------------------+---------------------------------+\n| 25006                    | 405                     | read only sql transaction       |\n+--------------------------+-------------------------+---------------------------------+\n| 25*                      | 500                     | invalid transaction state       |\n+--------------------------+-------------------------+---------------------------------+\n| 28*                      | 403                     | invalid auth specification      |\n+--------------------------+-------------------------+---------------------------------+\n| 2D*                      | 500                     | invalid transaction termination |\n+--------------------------+-------------------------+---------------------------------+\n| 38*                      | 500                     | external routine exception      |\n+--------------------------+-------------------------+---------------------------------+\n| 39*                      | 500                     | external routine invocation     |\n+--------------------------+-------------------------+---------------------------------+\n| 3B*                      | 500                     | savepoint exception             |\n+--------------------------+-------------------------+---------------------------------+\n| 40*                      | 500                     | transaction rollback            |\n+--------------------------+-------------------------+---------------------------------+\n| 53400                    | 500                     | config limit exceeded           |\n+--------------------------+-------------------------+---------------------------------+\n| 53*                      | 503                     | insufficient resources          |\n+--------------------------+-------------------------+---------------------------------+\n| 54*                      | 500                     | too complex                     |\n+--------------------------+-------------------------+---------------------------------+\n| 55*                      | 500                     | obj not in prerequisite state   |\n+--------------------------+-------------------------+---------------------------------+\n| 57*                      | 500                     | operator intervention           |\n+--------------------------+-------------------------+---------------------------------+\n| 58*                      | 500                     | system error                    |\n+--------------------------+-------------------------+---------------------------------+\n| F0*                      | 500                     | config file error               |\n+--------------------------+-------------------------+---------------------------------+\n| HV*                      | 500                     | foreign data wrapper error      |\n+--------------------------+-------------------------+---------------------------------+\n| P0001                    | 400                     | default code for \"raise\"        |\n+--------------------------+-------------------------+---------------------------------+\n| P0*                      | 500                     | PL/pgSQL error                  |\n+--------------------------+-------------------------+---------------------------------+\n| XX*                      | 500                     | internal error                  |\n+--------------------------+-------------------------+---------------------------------+\n| 42883                    | 404                     | undefined function              |\n+--------------------------+-------------------------+---------------------------------+\n| 42P01                    | 404                     | undefined table                 |\n+--------------------------+-------------------------+---------------------------------+\n| 42P17                    | 500                     | infinite recursion              |\n+--------------------------+-------------------------+---------------------------------+\n| 42501                    | | if authenticated 403, | insufficient privileges         |\n|                          | | else 401              |                                 |\n+--------------------------+-------------------------+---------------------------------+\n| other                    | 400                     |                                 |\n+--------------------------+-------------------------+---------------------------------+\n\nErrors from PostgREST\n=====================\n\nErrors that come from PostgREST itself maintain the same structure but differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:\n\n.. code-block:: http\n\n  POST /rpc/nonexistent_function HTTP/1.1\n\n.. code-block:: http\n\n  HTTP/1.1 404 Not Found\n  Content-Type: application/json; charset=utf-8\n\n.. code-block:: json\n\n  {\n    \"hint\": \"...\",\n    \"details\": null\n    \"code\": \"PGRST202\",\n    \"message\": \"Could not find the api.nonexistent_function() function in the schema cache\"\n  }\n\n\n.. _pgrst_errors:\n\nPostgREST Error Codes\n---------------------\n\nPostgREST error codes have the form ``PGRSTgxx``.\n\n- ``PGRST`` is the prefix that differentiates the error from a PostgreSQL error.\n- ``g`` is the error group\n- ``xx`` is the error identifier in the group.\n\n.. _pgrst0**:\n\nGroup 0 - Connection\n~~~~~~~~~~~~~~~~~~~~\n\nRelated to the connection with the database.\n\n+---------------+-------------+-------------------------------------------------------------+\n| Code          | HTTP status | Description                                                 |\n+===============+=============+=============================================================+\n| .. _pgrst000: | 503         | Could not connect with the database due to an incorrect     |\n|               |             | :ref:`db-uri` or due to the PostgreSQL service not running. |\n| PGRST000      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst001: | 503         | Could not connect with the database due to an internal      |\n|               |             | error.                                                      |\n| PGRST001      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst002: | 503         | Could not connect with the database when building the       |\n|               |             | :doc:`Schema Cache <schema_cache>`                          |\n| PGRST002      |             | due to the PostgreSQL service not running.                  |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst003: | 504         | The request timed out waiting for a pool connection         |\n|               |             | to be available. See :ref:`db-pool-acquisition-timeout`.    |\n| PGRST003      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n\n.. _pgrst1**:\n\nGroup 1 - Api Request\n~~~~~~~~~~~~~~~~~~~~~\n\nRelated to the HTTP request elements.\n\n+---------------+-------------+-------------------------------------------------------------+\n| Code          | HTTP status | Description                                                 |\n+===============+=============+=============================================================+\n| .. _pgrst100: | 400         | Parsing error in the query string parameter.                |\n|               |             | See :ref:`h_filter`, :ref:`operators` and :ref:`ordering`.  |\n| PGRST100      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst101: | 405         | For :ref:`functions <functions>`, only ``GET`` and ``POST`` |\n|               |             | verbs are allowed. Any other verb will throw this error.    |\n| PGRST101      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst102: | 400         | An invalid request body was sent(e.g. an empty body or      |\n|               |             | malformed JSON).                                            |\n| PGRST102      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst103: | 416         | An invalid range was specified for :ref:`limits`.           |\n|               |             |                                                             |\n| PGRST103      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst105: | 405         | An invalid :ref:`PUT <upsert_put>` request was done         |\n|               |             |                                                             |\n| PGRST105      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst106: | 406         | The schema specified when                                   |\n|               |             | :ref:`switching schemas <multiple-schemas>` is not present  |\n| PGRST106      |             | in the :ref:`db-schemas` configuration variable.            |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst107: | 415         | The ``Content-Type`` sent in the request is invalid.        |\n|               |             |                                                             |\n| PGRST107      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst108: | 400         | The filter is applied to a embedded resource that is not    |\n|               |             | specified in the ``select`` part of the query string.       |\n| PGRST108      |             | See :ref:`embed_filters`.                                   |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst111: | 500         | An invalid ``response.headers`` was set.                    |\n|               |             | See :ref:`guc_resp_hdrs`.                                   |\n| PGRST111      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst112: | 500         | The status code must be a positive integer.                 |\n|               |             | See :ref:`guc_resp_status`.                                 |\n| PGRST112      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst114: | 400         | For an :ref:`UPSERT using PUT <upsert_put>`, when           |\n|               |             | :ref:`limits and offsets <limits>` are used.                |\n| PGRST114      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst115: | 400         | For an :ref:`UPSERT using PUT <upsert_put>`, when the       |\n|               |             | primary key in the query string and the body are different. |\n| PGRST115      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst116: | 406         | More than 1 or no items where returned when requesting      |\n|               |             | a singular response. See :ref:`singular_plural`.            |\n| PGRST116      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst117: | 405         | The HTTP verb used in the request in not supported.         |\n|               |             |                                                             |\n| PGRST117      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst118: | 400         | Could not order the result using the related table because  |\n|               |             | there is no many-to-one or one-to-one relationship between  |\n| PGRST118      |             | them.                                                       |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst120: | 400         | An embedded resource can only be filtered using the         |\n|               |             | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|\n| PGRST120      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst121: | 500         | PostgREST can't parse the JSON objects in RAISE             |\n|               |             | ``PGRST`` error. See :ref:`raise headers <raise_headers>`.  |\n| PGRST121      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst122: | 400         | Invalid preferences found in ``Prefer`` header with         |\n|               |             | ``Prefer: handling=strict``. See :ref:`prefer_handling`.    |\n| PGRST122      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst123: | 400         | Aggregate functions are disabled.                           |\n|               |             | See :ref:`db-aggregates-enabled`.                           |\n| PGRST123      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst124: | 400         | ``max-affected`` preference is violated.                    |\n|               |             | See :ref:`prefer_max_affected`.                             |\n| PGRST124      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst125: | 404         | Invalid path is specified in request URL.                   |\n|               |             |                                                             |\n| PGRST125      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst126: | 404         | Open API config is disabled but API root path is            |\n|               |             | accessed. See :ref:`openapi-mode`.                          |\n| PGRST126      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst127: | 400         | The feature specified in the ``details`` field is not       |\n|               |             | implemented.                                                |\n| PGRST127      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst128: | 400         | ``max-affected`` preference is violated with ``RPC`` call.  |\n|               |             | See :ref:`prefer_max_affected`.                             |\n| PGRST128      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n\n\n.. _pgrst2**:\n\nGroup 2 - Schema Cache\n~~~~~~~~~~~~~~~~~~~~~~\n\nRelated to a :ref:`schema_cache`. Most of the time, these errors are solved by :ref:`schema_reloading`.\n\n+---------------+-------------+-------------------------------------------------------------+\n| Code          | HTTP status | Description                                                 |\n+===============+=============+=============================================================+\n| .. _pgrst200: | 400         | Caused by stale foreign key relationships, otherwise any of |\n|               |             | the embedding resources or the relationship itself may not  |\n| PGRST200      |             | exist in the database.                                      |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst201: | 300         | An ambiguous embedding request was made.                    |\n|               |             | See :ref:`complex_rels`.                                    |\n| PGRST201      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst202: | 404         | Caused by a stale function signature, otherwise             |\n|               |             | the function may not exist in the database.                 |\n| PGRST202      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst203: | 300         | Caused by requesting overloaded functions with the same     |\n|               |             | argument names but different types, or by using a ``POST``  |\n| PGRST203      |             | verb to request overloaded functions with a ``JSON`` or     |\n|               |             | ``JSONB`` type unnamed parameter. The solution is to rename |\n|               |             | the function or add/modify the names of the arguments.      |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst204: | 400         | Caused when the :ref:`column specified <specify_columns>`   |\n|               |             | in the ``columns`` query parameter is not found.            |\n| PGRST204      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst205: | 404         | Caused when the :ref:`table specified <tables_views>` in    |\n|               |             | the URI is not found.                                       |\n| PGRST205      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n\n.. _pgrst3**:\n\nGroup 3 - JWT\n~~~~~~~~~~~~~\n\nRelated to the authentication process using JWT. You can follow the :ref:`tut1` for an example on how to implement authentication and the :doc:`Authentication page <auth>` for more information on this process.\n\n+---------------+-------------+-------------------------------------------------------------+\n| Code          | HTTP status | Description                                                 |\n+===============+=============+=============================================================+\n| .. _pgrst300: | 500         | A :ref:`JWT secret <jwt-secret>` is missing from the        |\n|               |             | configuration.                                              |\n| PGRST300      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst301: | 401         | Provided JWT couldn't be decoded or it is invalid.          |\n|               |             |                                                             |\n| PGRST301      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst302: | 401         | Attempted to do a request without                           |\n|               |             | :ref:`bearer_auth` when the anonymous role                  |\n| PGRST302      |             | is disabled by not setting it in :ref:`db-anon-role`.       |\n+---------------+-------------+-------------------------------------------------------------+\n| .. _pgrst303: | 401         | :ref:`JWT claims validation <jwt_claims_validation>`        |\n|               |             | or parsing failed.                                          |\n| PGRST303      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n\n.. The Internal Errors Group X** is always at the end\n\n.. _pgrst_X**:\n\nGroup X - Internal\n~~~~~~~~~~~~~~~~~~\n\nInternal errors. If you encounter any of these, you may have stumbled on a PostgREST bug, please `open an issue <https://github.com/PostgREST/postgrest/issues>`_ and we'll be glad to fix it.\n\n+---------------+-------------+-------------------------------------------------------------+\n| Code          | HTTP status | Description                                                 |\n+===============+=============+=============================================================+\n| .. _pgrstX00: | 500         | Internal errors related to the library used for connecting  |\n|               |             | to the database.                                            |\n| PGRSTX00      |             |                                                             |\n+---------------+-------------+-------------------------------------------------------------+\n\n.. _custom_errors:\n\nCustom Errors\n=============\n\nYou can customize the errors by using the `RAISE statement <https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html#PLPGSQL-STATEMENTS-RAISE>`_  on functions.\n\n.. _raise_error:\n\nRAISE errors with HTTP Status Codes\n-----------------------------------\n\nCustom status codes can be done by raising SQL exceptions inside :ref:`functions <functions>`. For instance, here's a saucy function that always responds with an error:\n\n.. code-block:: postgres\n\n  CREATE OR REPLACE FUNCTION just_fail() RETURNS void\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    RAISE EXCEPTION 'I refuse!'\n      USING DETAIL = 'Pretty simple',\n            HINT = 'There is nothing you can do.';\n  END\n  $$;\n\nCalling the function returns HTTP 400 with the body\n\n.. code-block:: json\n\n  {\n    \"message\":\"I refuse!\",\n    \"details\":\"Pretty simple\",\n    \"hint\":\"There is nothing you can do.\",\n    \"code\":\"P0001\"\n  }\n\nOne way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.\n\nFor even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:\n\n.. code-block:: postgres\n\n  RAISE sqlstate 'PT402' using\n    message = 'Payment Required',\n    detail = 'Quota exceeded',\n    hint = 'Upgrade your plan';\n\nReturns:\n\n.. code-block:: http\n\n  HTTP/1.1 402 Payment Required\n  Content-Type: application/json; charset=utf-8\n\n  {\n    \"message\": \"Payment Required\",\n    \"details\": \"Quota exceeded\",\n    \"hint\": \"Upgrade your plan\",\n    \"code\": \"PT402\"\n  }\n\n.. _raise_headers:\n\nAdd HTTP Headers with RAISE\n---------------------------\n\nFor full control over headers and status you can raise a ``PGRST`` SQLSTATE error. You can achieve this by adding the ``code``, ``message``, ``detail`` and ``hint`` in the PostgreSQL error message field as a JSON object. Here, the ``details`` and ``hint`` are optional. Similarly, the ``status`` and ``headers`` must be added to the SQL error detail field as a JSON object. For instance:\n\n.. code-block:: postgres\n\n  RAISE sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"Payment Required\",\"details\":\"Quota exceeded\",\"hint\":\"Upgrade your plan\"}',\n      detail = '{\"status\":402,\"headers\":{\"X-Powered-By\":\"Nerd Rage\"}}';\n\nReturns:\n\n.. code-block:: http\n\n  HTTP/1.1 402 Payment Required\n  Content-Type: application/json; charset=utf-8\n  X-Powered-By: Nerd Rage\n\n  {\n    \"message\": \"Payment Required\",\n    \"details\": \"Quota exceeded\",\n    \"hint\": \"Upgrade your plan\",\n    \"code\": \"123\"\n  }\n\n\nFor non standard HTTP status, you can optionally add ``status_text`` to describe the status code. For status code ``419`` the detail field may look like this:\n\n.. code-block:: postgres\n\n  detail = '{\"status\":419,\"status_text\":\"Page Expired\",\"headers\":{\"X-Powered-By\":\"Nerd Rage\"}}';\n\nIf PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.\n\n.. _proxy-status_header:\n\nProxy-Status Header\n===================\n\nFor error cases, the standard `Proxy-Status <https://www.rfc-editor.org/rfc/rfc9209.html#name-the-proxy-status-http-field>`_ header is returned with the error code. The error code comes from either :ref:`PostgREST <pgrst_errors>`, :ref:`PostgreSQL <postgresql_errors>` or :ref:`Custom <custom_errors>` errors. This is useful when doing ``HEAD`` requests where the HTTP status is not descriptive enough.\n\nFor example, doing a request on a table with high count (say 30_000_000), we get:\n\n.. code-block:: http\n\n  HEAD /table HTTP/1.1\n  Prefer: count=exact\n\n.. code-block:: http\n\n  HTTP/1.1 500 Internal Server Error\n  Proxy-Status: PostgREST; error=57014\n\nThe PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.\n\n.. _client_error_verbosity:\n\nClient Error Verbosity\n======================\n\nFor HTTP clients, the error verbosity can be set via :ref:`client-error-verbosity` config.\n\nWith ``verbose``, it returns ``code``, ``message``, ``details`` and ``hint``.\n\n.. code:: bash\n\n  curl \"localhost:3000/itemsxx\"\n\n.. code-block:: json\n\n  {\n      \"code\": \"PGRST205\",\n      \"message\": \"Could not find the table 'public.itemsxx' in the schema cache\",\n      \"details\": \"Perhaps you meant the table 'public.items'\",\n      \"hint\": null\n  }\n\nWith ``minimal``, just ``code`` and ``message`` is returned.\n\n.. code:: bash\n\n  curl \"localhost:3000/itemsxx\"\n\n.. code-block:: json\n\n  {\n      \"code\": \"PGRST205\",\n      \"message\": \"Could not find the table 'public.itemsxx' in the schema cache\"\n  }\n"
  },
  {
    "path": "docs/references/listener.rst",
    "content": ".. _listener:\n\nListener\n########\n\nPostgREST uses `LISTEN <https://www.postgresql.org/docs/current/sql-listen.html>`_ to reload its :ref:`Schema Cache <schema_reloading_notify>` and :ref:`Configuration <config_reloading_notify>` via `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_.\nThis is useful in environments where you can't send SIGUSR1 or SIGUSR2 Unix Signals.\nLike on cloud managed containers or on Windows systems.\n\n.. code:: postgresql\n\n  NOTIFY pgrst, 'reload schema'; -- reload schema cache\n  NOTIFY pgrst, 'reload config'; -- reload config\n  NOTIFY pgrst;                  -- reload both\n\nBy default, the LISTEN channel is enabled (:ref:`db-channel-enabled`) and named ``pgrst`` (:ref:`db-channel`).\n\nListener on Read Replicas\n=========================\n\n\nThe ``LISTEN`` and ``NOTIFY`` commands do not work on PostgreSQL read replicas.\nThus, if you connect PostgREST to a read replica the Listener will fail to start.\n\n.. code:: psql\n\n  -- check if the instance is a replica\n  postgres=# select pg_is_in_recovery();\n   pg_is_in_recovery\n  -------------------\n   t\n  (1 row)\n\n  postgres=# LISTEN pgrst;\n  ERROR:  cannot execute LISTEN during recovery\n\nTo work around this, you can connect the Listener to the primary while still using the :ref:`connection_pool` on the replica.\n\nThis can be done by using the standard `libpq multiple hosts <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS>`_ and `target_session_attrs <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ in your :ref:`connection string <db-uri>`.\n\n.. code:: bash\n\n  db-uri = \"postgres://read_replica.host,primary.host/mydb?target_session_attrs=read-only\"\n\nThis will cause the :ref:`connection_pool` to connect to the read replica host and ``LISTEN`` on the fallback primary host.\n\n.. note::\n\n  Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.\n\n.. _listener_automatic_recovery:\n\nAutomatic Recovery\n==================\n\nThe listener will retry reconnecting to the database if connection loss happens.\n\n- It will retry forever with exponential backoff, with a maximum backoff time of 32 seconds between retries. Each of these attempts are :ref:`logged <pgrst_logging>`.\n- Automatic recovery can be disabled by setting :ref:`db-pool-automatic-recovery` to ``false``.\n- To ensure a valid state, the listener reloads the :ref:`schema_cache` and :ref:`configuration` when recovering.\n"
  },
  {
    "path": "docs/references/observability.rst",
    "content": ".. _observability:\n\nObservability\n#############\n\nObservability allows measuring a system's current state based on the data it generates, such as logs, metrics, and traces.\n\n.. contents::\n   :depth: 1\n   :local:\n   :backlinks: none\n\n.. _pgrst_logging:\n\nLogs\n====\n\nPostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available.\n\nWith :ref:`log-level` set to ``info``, we get:\n\n.. code::\n\n   127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] \"GET /clients HTTP/1.1\" 200 56 \"\" \"curl/7.64.0\"\n   127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] \"GET /unexistent HTTP/1.1\" 404 162 \"\" \"curl/7.64.0\"\n\nFor diagnostic information about the server itself, PostgREST logs to ``stderr``:\n\n  - The full version of the connected PostgreSQL database.\n  - :ref:`schema_cache` statistics.\n  - The messages received by the :ref:`listener`.\n\n.. code::\n\n   06/May/2024:08:16:11 -0500: Starting PostgREST 12.1...\n   06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit\n   06/May/2024:08:16:11 -0500: Connection Pool initialized with a maximum size of 10 connections\n   06/May/2024:08:16:11 -0500: API server listening on port 3000\n   06/May/2024:08:16:11 -0500: Listening for database notifications on the \"pgrst\" channel\n   06/May/2024:08:16:11 -0500: Config reloaded\n   06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds\n   06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers\n   06/May/2024:14:11:27 -0500: Received a config reload message on the \"pgrst\" channel\n   06/May/2024:14:11:27 -0500: Config reloaded\n\n.. note::\n\n  Logs are based on the ``log-level`` setting. See :ref:`log-level`.\n\n.. _sql_query_logs:\n\nSQL Query Logs\n--------------\n\nTo log the SQL queries executed for a request, set the :ref:`log-query` to ``true``. It will be logged based on the current :ref:`log-level` setting.\n\n.. code-block:: bash\n\n  log-level = \"warn\"\n  log-query = \"true\"\n\nThe SQL queries will only be logged on ``400`` HTTP errors and up.\nSo, if the user requests a resource without sufficient privileges:\n\n.. code-block:: bash\n\n  curl \"localhost:3000/protected_table\"\n\nThis will be logged by PostgREST:\n\n.. code::\n\n  17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT \"public\".\"protected_table\".* FROM \"public\".\"protected_table\"  )  SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t\n  127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] \"GET /protected_table HTTP/1.1\" 401 99 \"\" \"curl/8.7.1\"\n\nDatabase Logs\n-------------\n\nAdditionally, to find all the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.\n\nFind :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.\n\n.. code:: sql\n\n  # send logs where the collector can access them\n  log_destination = \"stderr\"\n\n  # collect stderr output to log files\n  logging_collector = on\n\n  # save logs in pg_log/ under the pg data directory\n  log_directory = \"pg_log\"\n\n  # (optional) new log file per day\n  log_filename = \"postgresql-%Y-%m-%d.log\"\n\n  # log every kind of SQL statement\n  log_statement = \"all\"\n\nRestart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands.\n\n.. note::\n\n  On Docker you can enable the logs by using a custom ``init.sh``:\n\n  .. code:: bash\n\n    #!/bin/sh\n    echo \"log_statement = 'all'\" >> /var/lib/postgresql/data/postgresql.conf\n\n  After that you can start the container and check the logs with ``docker logs``.\n\n  .. code:: bash\n\n    docker run -v \"$(pwd)/init.sh\":\"/docker-entrypoint-initdb.d/init.sh\" -d postgres\n    docker logs -f <container-id>\n\n.. _metrics:\n\nMetrics\n=======\n\nThe ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3001/metrics\"\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Type: text/plain; charset=utf-8\n\n  # HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load\n  # TYPE pgrst_schema_cache_query_time_seconds gauge\n  pgrst_schema_cache_query_time_seconds 1.5937927e-2\n  # HELP pgrst_schema_cache_loads_total The total number of times the schema cache was loaded\n  # TYPE pgrst_schema_cache_loads_total counter\n  pgrst_schema_cache_loads_total 1.0\n  ...\n\nSchema Cache Metrics\n--------------------\n\nMetrics related to the :ref:`schema_cache`.\n\npgrst_schema_cache_query_time_seconds\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Gauge\n======== =======\n\nThe query time in seconds of the last schema cache load.\n\npgrst_schema_cache_loads_total\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n========== ==========================\n**Type**   Counter\n**Labels** ``status``: SUCCESS | FAIL\n========== ==========================\n\nThe total number of times the schema cache was loaded.\n\nConnection Pool Metrics\n-----------------------\n\nMetrics related to the :ref:`connection_pool`.\n\npgrst_db_pool_timeouts_total\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Counter\n======== =======\n\nThe total number of pool connection timeouts.\n\npgrst_db_pool_available\n~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Gauge\n======== =======\n\nAvailable connections in the pool.\n\npgrst_db_pool_waiting\n~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Gauge\n======== =======\n\nRequests waiting to acquire a pool connection\n\npgrst_db_pool_max\n~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Gauge\n======== =======\n\nMax pool connections.\n\n.. _jwt_cache_metrics:\n\nJWT Cache Metrics\n-----------------\n\nMetrics related to the :ref:`jwt_caching`.\n\npgrst_jwt_cache_requests_total\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Counter\n======== =======\n\nThe total number of JWT cache lookups.\n\npgrst_jwt_cache_hits_total\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Counter\n======== =======\n\nThe total number of JWT cache hits.\n\npgrst_jwt_cache_evictions_total\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n======== =======\n**Type** Counter\n======== =======\n\nThe total number of JWT cache evictions.\n\nTraces\n======\n\nServer Version Header\n---------------------\n\nWhen debugging a problem it's important to verify the running PostgREST version. For this you can look at the :code:`Server` HTTP response header that is returned on every request.\n\n.. code::\n\n  HEAD /users HTTP/1.1\n\n  Server: postgrest/11.0.1\n\n.. _trace_header:\n\nTrace Header\n------------\n\nYou can enable tracing HTTP requests by setting :ref:`server-trace-header`. Specify the set header in the request, and the server will include it in the response.\n\n.. code:: bash\n\n  server-trace-header = \"X-Request-Id\"\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/users\" \\\n    -H \"X-Request-Id: 123\"\n\n.. code::\n\n  HTTP/1.1 200 OK\n  X-Request-Id: 123\n\nProxy-Status Header\n-------------------\n\nSee :ref:`proxy-status_header`.\n\n.. _server-timing_header:\n\nServer-Timing Header\n--------------------\n\nYou can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.\nThis header communicates metrics of the different phases in the request-response cycle.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/users\" -i\n\n.. code::\n\n  HTTP/1.1 200 OK\n\n  Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4\n\n- All the durations (``dur``) are in milliseconds.\n- The ``jwt`` stage is when :ref:`jwt_auth` is done. This duration can be lowered with :ref:`jwt_caching`.\n- On the ``parse`` stage, the :ref:`url_grammar` is parsed.\n- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.\n- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.\n- The ``response`` stage is where the response status and headers are computed.\n\n.. note::\n\n  We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.\n\n.. _content-length_header:\n\nContent-Length Header\n---------------------\n\nYou can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.\n\n.. code-block:: bash\n\n  curl -i 'localhost:3000/users'\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 104\n\nNote that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).\nThis is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.\n\nThe body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.\n\n.. _explain_plan:\n\nExecution plan\n--------------\n\nYou can get the `EXPLAIN execution plan <https://www.postgresql.org/docs/current/sql-explain.html>`_ of a request by adding the ``Accept: application/vnd.pgrst.plan`` header.\nThis is enabled by :ref:`db-plan-enabled` (false by default).\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/users?select=name&order=id\" \\\n    -H \"Accept: application/vnd.pgrst.plan\"\n\n.. code-block:: postgres\n\n  Aggregate  (cost=73.65..73.68 rows=1 width=112)\n    ->  Index Scan using users_pkey on users  (cost=0.15..60.90 rows=850 width=36)\n\nThe output of the plan is generated in ``text`` format by default but you can change it to JSON by using the ``+json`` suffix.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/users?select=name&order=id\" \\\n    -H \"Accept: application/vnd.pgrst.plan+json\"\n\n.. code-block:: json\n\n  [\n    {\n      \"Plan\": {\n        \"Node Type\": \"Aggregate\",\n        \"Strategy\": \"Plain\",\n        \"Partial Mode\": \"Simple\",\n        \"Parallel Aware\": false,\n        \"Async Capable\": false,\n        \"Startup Cost\": 73.65,\n        \"Total Cost\": 73.68,\n        \"Plan Rows\": 1,\n        \"Plan Width\": 112,\n        \"Plans\": [\n          {\n            \"Node Type\": \"Index Scan\",\n            \"Parent Relationship\": \"Outer\",\n            \"Parallel Aware\": false,\n            \"Async Capable\": false,\n            \"Scan Direction\": \"Forward\",\n            \"Index Name\": \"users_pkey\",\n            \"Relation Name\": \"users\",\n            \"Alias\": \"users\",\n            \"Startup Cost\": 0.15,\n            \"Total Cost\": 60.90,\n            \"Plan Rows\": 850,\n            \"Plan Width\": 36\n          }\n        ]\n      }\n    }\n  ]\n\nBy default the plan is assumed to generate the JSON representation of a resource(``application/json``), but you can obtain the plan for the :ref:`different representations that PostgREST supports <res_format>` by adding them to the ``for`` parameter. For instance, to obtain the plan for a ``text/xml``, you would use ``Accept: application/vnd.pgrst.plan; for=\"text/xml``.\n\nThe other available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, which correspond to the `EXPLAIN command options <https://www.postgresql.org/docs/current/sql-explain.html>`_. To use the ``analyze`` and ``wal`` parameters for example, you would add them like ``Accept: application/vnd.pgrst.plan; options=analyze|wal``.\n\nNote that akin to the EXPLAIN command, the changes will be committed when using the ``analyze`` option. To avoid this, you can use the :ref:`db-tx-end` and the ``Prefer: tx=rollback`` header.\n\nSecuring the Execution Plan\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIt's recommended to only activate :ref:`db-plan-enabled` on testing environments since it reveals internal database details.\nHowever, if you choose to use it in production you can add a :ref:`db-pre-request` to filter the requests that can use this feature.\n\nFor example, to only allow requests from an IP address to get the execution plans:\n\n.. code-block:: postgres\n\n -- Assuming a proxy(Nginx, Cloudflare, etc) passes an \"X-Forwarded-For\" header(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)\n create or replace function filter_plan_requests()\n returns void as $$\n declare\n   headers   json := current_setting('request.headers', true)::json;\n   client_ip text := coalesce(headers->>'x-forwarded-for', '');\n   accept    text := coalesce(headers->>'accept', '');\n begin\n   if accept like 'application/vnd.pgrst.plan%' and client_ip != '144.96.121.73' then\n     raise insufficient_privilege using\n       message = 'Not allowed to use application/vnd.pgrst.plan';\n   end if;\n end; $$ language plpgsql;\n\n -- set this function on your postgrest.conf\n -- db-pre-request = filter_plan_requests\n\n.. raw:: html\n\n  <script type=\"text/javascript\">\n    let hash = window.location.hash;\n\n    const redirects = {\n      '#health_check': 'health_check.html',\n      '#server-version': '#server-version-header',\n    };\n\n    let willRedirectTo = redirects[hash];\n\n    if (willRedirectTo) {\n      window.location.href = willRedirectTo;\n    }\n  </script>\n"
  },
  {
    "path": "docs/references/schema_cache.rst",
    "content": ".. _schema_cache:\n\nSchema Cache\n============\n\nPostgREST requires metadata from the database schema to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.\n\nGetting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.\n\n.. _schema_reloading:\n\nSchema Cache Reloading\n----------------------\n\nTo not let the schema cache go stale (happens when you make changes to the database), you need to reload it.\n\nYou can do this with UNIX signals or with PostgreSQL notifications. It's also possible to do this automatically using `event triggers <https://www.postgresql.org/docs/current/event-trigger-definition.html>`_.\n\n.. note::\n\n  - Requests will wait until the schema cache reload is done. This to prevent client errors due to an stale schema cache.\n  - If you are using the :ref:`in_db_config`, a schema cache reload will :ref:`reload the configuration<config_reloading>` as well.\n\n.. _schema_reloading_signals:\n\nSchema Cache Reloading with Unix Signals\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo manually reload the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process.\n\n.. code:: bash\n\n  killall -SIGUSR1 postgrest\n\n\nFor docker you can do:\n\n.. code:: bash\n\n  docker kill -s SIGUSR1 <container>\n\n  # or in docker-compose\n  docker-compose kill -s SIGUSR1 <service>\n\n.. _schema_reloading_notify:\n\nSchema Cache Reloading with NOTIFY\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo reload the schema cache from within the database, you can use the ``NOTIFY`` command. See :ref:`listener`.\n\n.. code-block:: postgres\n\n  NOTIFY pgrst, 'reload schema'\n\n.. _auto_schema_reloading:\n\nAutomatic Schema Cache Reloading\n--------------------------------\n\nYou can do automatic reloading and forget there is a schema cache. For this use an `event trigger <https://www.postgresql.org/docs/current/event-trigger-definition.html>`_ and ``NOTIFY``.\n\n.. code-block:: postgres\n\n  -- Create an event trigger function\n  CREATE OR REPLACE FUNCTION pgrst_watch() RETURNS event_trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NOTIFY pgrst, 'reload schema';\n  END;\n  $$;\n\n  -- This event trigger will fire after every ddl_command_end event\n  CREATE EVENT TRIGGER pgrst_watch\n    ON ddl_command_end\n    EXECUTE PROCEDURE pgrst_watch();\n\nNow, whenever the ``pgrst_watch`` trigger fires, PostgREST will auto-reload the schema cache.\n\nTo disable auto reloading, drop the trigger.\n\n.. code-block:: postgres\n\n  DROP EVENT TRIGGER pgrst_watch\n\nFiner-Grained Event Trigger\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can refine the previous event trigger to only react to the events relevant to the schema cache. This also prevents unnecessary\nreloading when creating temporary tables inside functions.\n\n.. code-block:: postgres\n\n  -- watch CREATE and ALTER\n  CREATE OR REPLACE FUNCTION pgrst_ddl_watch() RETURNS event_trigger AS $$\n  DECLARE\n    cmd record;\n  BEGIN\n    FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands()\n    LOOP\n      IF cmd.command_tag IN (\n        'CREATE SCHEMA', 'ALTER SCHEMA'\n      , 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE'\n      , 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE'\n      , 'CREATE VIEW', 'ALTER VIEW'\n      , 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW'\n      , 'CREATE FUNCTION', 'ALTER FUNCTION'\n      , 'CREATE TRIGGER'\n      , 'CREATE TYPE', 'ALTER TYPE'\n      , 'CREATE RULE'\n      , 'COMMENT'\n      )\n      -- don't notify in case of CREATE TEMP table or other objects created on pg_temp\n      AND cmd.schema_name is distinct from 'pg_temp'\n      THEN\n        NOTIFY pgrst, 'reload schema';\n      END IF;\n    END LOOP;\n  END; $$ LANGUAGE plpgsql;\n\n  -- watch DROP\n  CREATE OR REPLACE FUNCTION pgrst_drop_watch() RETURNS event_trigger AS $$\n  DECLARE\n    obj record;\n  BEGIN\n    FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()\n    LOOP\n      IF obj.object_type IN (\n        'schema'\n      , 'table'\n      , 'foreign table'\n      , 'view'\n      , 'materialized view'\n      , 'function'\n      , 'trigger'\n      , 'type'\n      , 'rule'\n      )\n      AND obj.is_temporary IS false -- no pg_temp objects\n      THEN\n        NOTIFY pgrst, 'reload schema';\n      END IF;\n    END LOOP;\n  END; $$ LANGUAGE plpgsql;\n\n  CREATE EVENT TRIGGER pgrst_ddl_watch\n    ON ddl_command_end\n    EXECUTE PROCEDURE pgrst_ddl_watch();\n\n  CREATE EVENT TRIGGER pgrst_drop_watch\n    ON sql_drop\n    EXECUTE PROCEDURE pgrst_drop_watch();\n"
  },
  {
    "path": "docs/references/transactions.rst",
    "content": ".. _transactions:\n\nTransactions\n============\n\nAfter :ref:`user_impersonation`, every request to an :doc:`API resource <api>` runs inside a transaction. The sequence of the transaction is as follows:\n\n.. code-block:: postgres\n\n  START TRANSACTION; -- <Access Mode> <Isolation Level>\n  -- <Transaction-scoped settings>\n  -- <Main Query>\n  END; -- <Transaction End>\n\n.. _access_mode:\n\nAccess Mode\n-----------\n\nThe access mode determines whether the transaction can modify the database or not. There are 2 possible values: READ ONLY and READ WRITE.\n\nModifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests. Consider the following:\n\n.. code-block:: postgres\n\n  CREATE SEQUENCE callcounter_count START 1;\n\n  CREATE VIEW callcounter AS\n  SELECT nextval('callcounter_count');\n\nSince the ``callcounter`` view modifies the sequence, calling it with GET or HEAD will result in an error:\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/callcounter\"\n\n.. code-block:: http\n\n  HTTP/1.1 405 Method Not Allowed\n\n  {\"code\":\"25006\",\"details\":null,\"hint\":null,\"message\":\"cannot execute nextval() in a read-only transaction\"}\n\nAccess Mode on Tables and Views\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe access mode on :ref:`tables_views` is determined by the HTTP method.\n\n.. list-table::\n   :header-rows: 1\n\n   * - HTTP Method\n     - Access Mode\n   * - GET, HEAD\n     - READ ONLY\n   * - POST, PATCH, PUT, DELETE\n     - READ WRITE\n\nAccess Mode on Functions\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n:ref:`functions` additionally depend on the function `volatility <https://www.postgresql.org/docs/current/xfunc-volatility.html>`_.\n\n.. list-table::\n   :header-rows: 2\n\n   * -\n     - Access Mode\n     -\n     -\n   * - HTTP Method\n     - VOLATILE\n     - STABLE\n     - IMMUTABLE\n   * - GET, HEAD\n     - READ ONLY\n     - READ ONLY\n     - READ ONLY\n   * - POST\n     - READ WRITE\n     - READ ONLY\n     - READ ONLY\n\n.. note::\n\n  - The volatility marker is a promise about the behavior of the function.  PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure.  But, because of the READ ONLY transaction the function will fail under PostgREST.\n  - The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here.\n\n.. _isolation_lvl:\n\nIsolation Level\n---------------\n\nEvery transaction uses the PostgreSQL default isolation level: READ COMMITTED. Unless you modify `default_transaction_isolation <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-ISOLATION>`_  for an impersonated role or function.\n\n.. code-block:: postgres\n\n  ALTER ROLE webuser SET default_transaction_isolation TO 'repeatable read';\n\nEvery ``webuser`` gets its queries executed with ``default_transaction_isolation`` set to REPEATABLE READ.\n\nOr to change the isolation level per function call.\n\n.. code-block:: postgres\n\n  CREATE OR REPLACE FUNCTION myfunc()\n  RETURNS text as $$\n    SELECT 'hello';\n  $$\n  LANGUAGE SQL\n  SET default_transaction_isolation TO 'serializable';\n\n.. _tx_settings:\n\nTransaction-Scoped Settings\n---------------------------\n\nPostgREST uses settings tied to the transaction lifetime. These can be used to get data about the HTTP request. Or to modify the HTTP response.\n\nYou can get these with ``current_setting``\n\n.. code-block:: postgres\n\n  -- request settings use the ``request.`` prefix.\n  SELECT\n    current_setting('request.<setting>', true);\n\nAnd you can set them with ``set_config``\n\n.. code-block:: postgres\n\n  -- response settings use the ``response.`` prefix.\n  SELECT\n    set_config('response.<setting>', 'value1' ,true);\n\n.. _guc_req_headers_cookies_claims:\n\nRequest Headers, Cookies and JWT claims\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPostgREST stores the headers, cookies and headers as JSON. To get them:\n\n.. code-block:: postgres\n\n  -- To get all the headers sent in the request\n  SELECT current_setting('request.headers', true)::json;\n\n  -- To get a single header, you can use JSON arrow operators\n  SELECT current_setting('request.headers', true)::json->>'user-agent';\n\n  -- value of sessionId in a cookie\n  SELECT current_setting('request.cookies', true)::json->>'sessionId';\n\n  -- value of the email claim in a jwt\n  SELECT current_setting('request.jwt.claims', true)::json->>'email';\n\n.. important::\n\n  - The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``.\n  - The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.\n  - Settings don't become NULL after the transaction is committed, instead they're set to a an empty string ``''``.\n\n    + This is considered expected behavior by PostgreSQL. For more details, see `this discussion <https://www.postgresql.org/message-id/flat/CAB_pDVVa84w7hXhzvyuMTb8f5kKV3bee_p9QTZZ58Rg7zYM7sw%40mail.gmail.com>`_.\n    + To avoid this inconsistency, you can create a wrapper function like:\n\n    .. code-block:: postgres\n\n      CREATE FUNCTION my_current_setting(text) RETURNS text\n      LANGUAGE SQL AS $$\n        SELECT nullif(current_setting($1, true), '');\n      $$;\n\n.. _guc_req_path_method:\n\nRequest Path and Method\n~~~~~~~~~~~~~~~~~~~~~~~\n\nThe path and method are stored as ``text``.\n\n.. code-block:: postgres\n\n  SELECT current_setting('request.path', true);\n\n  SELECT current_setting('request.method', true);\n\nRequest Role and Search Path\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBecause of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways:\n\n.. code-block:: postgres\n\n  SELECT current_role;\n\n  SELECT current_user;\n\n  SELECT current_setting('role', true);\n\nAdditionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`.\n\n.. _guc_resp_hdrs:\n\nResponse Headers\n~~~~~~~~~~~~~~~~\n\nYou can set ``response.headers`` to add headers to the HTTP response. For instance, this statement would add caching headers to the response:\n\n.. code-block:: postgres\n\n  -- tell client to cache response for two days\n\n  SELECT set_config('response.headers',\n    '[{\"Cache-Control\": \"public\"}, {\"Cache-Control\": \"max-age=259200\"}]', true);\n\n.. code-block:: http\n\n  HTTP/1.1 200 OK\n  Content-Type: application/json; charset=utf-8\n  Cache-Control: no-cache, no-store, must-revalidate\n\nNotice that the ``response.headers`` should be set to an *array* of single-key objects rather than a single multiple-key object. This is because headers such as ``Cache-Control`` or ``Set-Cookie`` need repeating when setting many values. An object would not allow the repeated key.\n\n.. note::\n\n  PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overridden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.\n\n.. _guc_resp_status:\n\nResponse Status Code\n~~~~~~~~~~~~~~~~~~~~\n\nYou can set the ``response.status`` to override the default status code PostgREST provides. For instance, the following function would replace the default ``200`` status code.\n\n.. code-block:: postgres\n\n   create or replace function teapot() returns json as $$\n   begin\n     perform set_config('response.status', '418', true);\n     return json_build_object('message', 'The requested entity body is short and stout.',\n                              'hint', 'Tip it over and pour it out.');\n   end;\n   $$ language plpgsql;\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/rpc/teapot\" -i\n\n.. code-block:: http\n\n  HTTP/1.1 418 I'm a teapot\n\n  {\n    \"message\" : \"The requested entity body is short and stout.\",\n    \"hint\" : \"Tip it over and pour it out.\"\n  }\n\nIf the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example).\n\n.. _impersonated_settings:\n\nImpersonated Role Settings\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPostgreSQL applies the connection role (:ref:`authenticator <roles>`) settings. Additionally, PostgREST applies the :ref:`impersonated roles <user_impersonation>` settings as transaction-scoped settings.\nThis allows finer-grained control over actions made by a role.\n\nFor example, consider `statement_timeout <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT>`__. It allows you to abort any statement that takes more than a specified time. It is disabled by default.\n\n.. code-block:: postgres\n\n  ALTER ROLE authenticator SET statement_timeout TO '10s';\n  ALTER ROLE anonymous SET statement_timeout TO '1s';\n\nWith the above settings, all users get a global statement timeout of 10 seconds and :ref:`anonymous <roles>` users get a timeout of 1 second.\n\nSettings with privileged context\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSettings that have a context which requires privileges won't be applied by default. This is so we don't cause permission errors.\nFor more details see `Understanding Postgres Parameter Context <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.\n\nHowever, starting from PostgreSQL 15, you can grant privileges for these settings with:\n\n.. code-block:: postgres\n\n  GRANT SET ON PARAMETER <setting> TO <authenticator>;\n\nHoisted Function Settings\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPostgREST can \"hoist\" function settings to transaction-scoped settings. This allows functions settings to override the impersonated and connection role settings.\n\n.. code-block:: postgres\n\n  CREATE OR REPLACE FUNCTION myfunc()\n  RETURNS void as $$\n    SELECT pg_sleep(3); -- simulating some long-running process\n  $$\n  LANGUAGE SQL\n  SET statement_timeout TO '4s';\n\nWhen calling the above function (see :ref:`functions`), the statement timeout will be 4 seconds.\n\n.. note::\n\n   Only the settings in :ref:`db-hoisted-tx-settings` will be hoisted.\n\n.. _main_query:\n\nMain query\n----------\n\nThe main query is generated by requesting :ref:`tables_views` or :ref:`functions`. All generated queries use prepared statements (:ref:`db-prepared-statements`).\n\n.. _tx_end:\n\nTransaction End\n---------------\n\nIf the transaction doesn't fail, it will always end in a COMMIT. Unless :ref:`db-tx-end` is configured to ROLLBACK in any case or conditionally with the :ref:`prefer_tx`. This is useful for testing purposes.\n\nAborting transactions\n---------------------\n\nAny database failure(like a failed constraint) will result in a rollback of the transaction. You can also :ref:`RAISE an error inside a function <raise_error>` to cause a rollback.\n\n.. _pre-request:\n\nPre-Request\n-----------\n\nThe pre-request is a function that can run after the :ref:`tx_settings` are set and before the :ref:`main_query`. It's enabled with :ref:`db-pre-request`.\n\nThis provides an opportunity to modify settings or raise an exception to prevent the request from completing.\n\n.. _pre_req_headers:\n\nSetting headers via pre-request\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs an example, let's add some cache headers for all requests that come from an Internet Explorer(6 or 7) browser.\n\n.. code-block:: postgres\n\n   create or replace function custom_headers()\n   returns void as $$\n   declare\n     user_agent text := current_setting('request.headers', true)::json->>'user-agent';\n   begin\n     if user_agent similar to '%MSIE (6.0|7.0)%' then\n       perform set_config('response.headers',\n         '[{\"Cache-Control\": \"no-cache, no-store, must-revalidate\"}]', false);\n     end if;\n   end; $$ language plpgsql;\n\n   -- set this function on postgrest.conf\n   -- db-pre-request = custom_headers\n\nNow when you make a GET request to a table or view, you'll get the cache headers.\n\n.. code-block:: bash\n\n  curl \"http://localhost:3000/people\" -i \\\n   -H \"User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)\"\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "# This file is auto-generated by postgrest-nixpkgs-upgrade\nsphinx==8.2.3\nsphinx-copybutton==0.5.2\nsphinx-rtd-dark-mode==1.3.0\nsphinx-rtd-theme==3.0.2\nsphinx-tabs==3.4.7\nsphinxext-opengraph==0.9.1"
  },
  {
    "path": "docs/shared/installation.rst",
    "content": ".. tabs::\n\n  .. group-tab:: macOS\n\n    You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.\n\n    .. code:: bash\n\n      brew install postgrest\n\n  .. group-tab:: FreeBSD\n\n    You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.\n\n    .. code:: bash\n\n      pkg install hs-postgrest\n\n  .. group-tab:: Linux\n\n    .. tabs::\n\n      .. tab:: Arch Linux\n\n        You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.\n\n        .. code:: bash\n\n          pacman -S postgrest\n\n      .. tab:: Nix via nixpkgs\n\n        You can install PostgREST from nixpkgs.\n\n        .. code:: bash\n\n          nix-env -i postgrest\n\n      .. tab:: Nix via flake\n\n        You can install PostgREST via flake.\n\n        .. code:: nix\n\n          {\n            inputs.postgrest.url = \"github:postgrest/postgrest\";\n            # ...\n          }\n\n  .. group-tab:: Windows\n\n    You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.\n\n    .. code:: bash\n\n      choco install postgrest\n      scoop install postgrest\n"
  },
  {
    "path": "docs/tutorials/tut0.rst",
    "content": ".. _tut0:\n\nTutorial 0 - Get it Running\n===========================\n\n:author: `begriffs <https://github.com/begriffs>`_\n\nWelcome to PostgREST! In this pre-tutorial we're going to get things running so you can create your first simple API.\n\nPostgREST is a standalone web server which turns a PostgreSQL database into a RESTful API. It serves an API that is customized based on the structure of the underlying database.\n\n.. container:: img-translucent\n\n  .. image:: ../_static/tuts/tut0-request-flow.png\n\nTo make an API we'll simply be building a database. All the endpoints and permissions come from database objects like tables, views, roles, and functions. These tutorials will cover a number of common scenarios and how to model them in the database.\n\nBy the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.\n\nStep 1. Install PostgreSQL\n--------------------------\n\nIf you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.\n\nIf Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Make sure that Docker service is `started <https://docs.docker.com/engine/daemon/start/#start-the-daemon-using-operating-system-utilities>`_. Next, let's pull and start the database image:\n\n.. code-block:: bash\n\n  sudo docker run --name tutorial -p 5432:5432 \\\n                  -e POSTGRES_PASSWORD=notused \\\n                  -d postgres\n\nThis will run the Docker instance as a daemon and expose port 5432 to the host system so that it looks like an ordinary PostgreSQL server to the rest of the system.\n\n.. note::\n\n  This only works if there is no other PostgreSQL instance running on the default port on your computer. If this port is already in use, you will receive a message similar to this:\n\n  .. code-block:: text\n\n    docker: Error response from daemon: [...]: Bind for 0.0.0.0:5432 failed: port is already allocated.\n\n  In this case, you will need to change the **first** of the two 5432 to something else, for example to :code:`5433:5432`. Remember to also adjust the port in your config file in Step 5!\n\n\nStep 2. Install PostgREST\n-------------------------\n\nUsing a Package Manager\n~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can use your OS package manager to install PostgREST.\n\n.. include:: ../shared/installation.rst\n\nThen, try running it with:\n\n.. code-block:: bash\n\n  postgrest -h\n\nIt should print the help page with its version and the available options.\n\nDownloading a Pre-Built Binary\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPostgREST is also distributed as a single binary, with versions compiled for major distributions of macOS, Windows, Linux and FreeBSD. Visit the `latest release <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.\n\nThe pre-built binaries for download are :code:`.tar.xz` compressed files (except Windows which is a zip file). To extract the binary, go into the terminal and run\n\n.. code-block:: bash\n\n  # download from https://github.com/PostgREST/postgrest/releases/latest\n\n  tar xJf postgrest-<version>-<platform>.tar.xz\n\nThe result will be a file named simply :code:`postgrest` (or :code:`postgrest.exe` on Windows). At this point try running it with\n\n.. code-block:: bash\n\n  ./postgrest -h\n\nIf everything is working correctly it will print out its version and the available options. You can continue to run this binary from where you downloaded it, or copy it to a system directory like :code:`/usr/local/bin` on Linux so that you will be able to run it from any directory.\n\n.. note::\n\n  PostgREST requires libpq, the PostgreSQL C library, to be installed on your system. Without the library you'll get an error like \"error while loading shared libraries: libpq.so.5.\" Here's how to fix it:\n\n  .. raw:: html\n\n    <p>\n    <details>\n      <summary>Ubuntu or Debian</summary>\n      <div class=\"highlight-bash\"><div class=\"highlight\">\n        <pre>sudo apt-get install libpq-dev</pre>\n      </div></div>\n    </details>\n    <details>\n      <summary>Fedora, CentOS, or Red Hat</summary>\n      <div class=\"highlight-bash\"><div class=\"highlight\">\n        <pre>sudo yum install postgresql-libs</pre>\n      </div></div>\n    </details>\n    <details>\n      <summary>macOS</summary>\n      <div class=\"highlight-bash\"><div class=\"highlight\">\n        <pre>brew install postgresql</pre>\n      </div></div>\n    </details>\n    <details>\n      <summary>Windows</summary>\n        <p>All of the DLL files that are required to run PostgREST are available in the windows installation of PostgreSQL server.\n        Once installed they are found in the BIN folder, e.g: C:\\Program Files\\PostgreSQL\\10\\bin. Add this directory to your PATH\n        variable. Run the following from an administrative command prompt (adjusting the actual BIN path as necessary of course)\n          <pre>setx /m PATH \"%PATH%;C:\\Program Files\\PostgreSQL\\10\\bin\"</pre>\n        </p>\n    </details>\n    </p>\n\nStep 3. Create Database for API\n-------------------------------\n\nConnect to the SQL console (psql) inside the container. To do so, run this from your command line:\n\n.. code-block:: bash\n\n  sudo docker exec -it tutorial psql -U postgres\n\nYou should see the psql command prompt:\n\n::\n\n  psql (16.2)\n  Type \"help\" for help.\n\n  postgres=#\n\nThe first thing we'll do is create a `named schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ for the database objects which will be exposed in the API. We can choose any name we like, so how about \"api.\" Execute this and the other SQL statements inside the psql prompt you started.\n\n.. code-block:: postgres\n\n  create schema api;\n\nOur API will have one endpoint, :code:`/todos`, which will come from a table.\n\n.. code-block:: postgres\n\n  create table api.todos (\n    id int primary key generated by default as identity,\n    done boolean not null default false,\n    task text not null,\n    due timestamptz\n  );\n\n  insert into api.todos (task) values\n    ('finish tutorial 0'), ('pat self on back');\n\nNext make a role to use for anonymous web requests. When a request comes in, PostgREST will switch into this role in the database to run queries.\n\n.. code-block:: postgres\n\n  create role web_anon nologin;\n\n  grant usage on schema api to web_anon;\n  grant select on api.todos to web_anon;\n\nThe :code:`web_anon` role has permission to access things in the :code:`api` schema, and to read rows in the :code:`todos` table.\n\nIt's a good practice to create a dedicated role for connecting to the database, instead of using the highly privileged ``postgres`` role. So we'll do that, name the role ``authenticator`` and also grant it the ability to switch to the ``web_anon`` role :\n\n.. code-block:: postgres\n\n  create role authenticator noinherit login password 'mysecretpassword';\n  grant web_anon to authenticator;\n\n\nNow quit out of psql; it's time to start the API!\n\n.. code-block:: psql\n\n  \\q\n\nStep 4. Run PostgREST\n---------------------\n\nPostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:\n\n.. code-block:: ini\n\n  db-uri = \"postgres://authenticator:mysecretpassword@localhost:5432/postgres\"\n  db-schemas = \"api\"\n  db-anon-role = \"web_anon\"\n\nThe configuration file has other :ref:`options <configuration>`, but this is all we need.\nIf you are not using Docker, make sure that your port number is correct and replace `postgres` with the name of the database where you added the todos table.\n\n.. note::\n\n  In case you had to adjust the port in Step 2, remember to adjust the port here, too!\n\nNow run the server:\n\n.. code-block:: bash\n\n  # Running postgrest installed from a package manager\n  postgrest tutorial.conf\n\n  # Running postgrest binary\n  ./postgrest tutorial.conf\n\nYou should see something similar to:\n\n.. code-block:: text\n\n  Starting PostgREST 12.0.2...\n  Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit\n  API server listening on port 3000\n\nIt's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.\n\n.. code-block:: bash\n\n  curl http://localhost:3000/todos\n\nThe API replies:\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 1,\n      \"done\": false,\n      \"task\": \"finish tutorial 0\",\n      \"due\": null\n    },\n    {\n      \"id\": 2,\n      \"done\": false,\n      \"task\": \"pat self on back\",\n      \"due\": null\n    }\n  ]\n\nWith the current role permissions, anonymous requests have read-only access to the :code:`todos` table. If we try to add a new todo we are not able.\n\n.. code-block:: bash\n\n  curl http://localhost:3000/todos -X POST \\\n       -H \"Content-Type: application/json\" \\\n       -d '{\"task\": \"do bad thing\"}'\n\nResponse is 401 Unauthorized:\n\n.. code-block:: json\n\n  {\n    \"code\": \"42501\",\n    \"details\": null,\n    \"hint\": null,\n    \"message\": \"permission denied for table todos\"\n  }\n\nThere we have it, a basic API on top of the database! In the next tutorials we will see how to extend the example with more sophisticated user access controls, and more tables and queries.\n\nNow that you have PostgREST running, try the next tutorial, :ref:`tut1`\n"
  },
  {
    "path": "docs/tutorials/tut1.rst",
    "content": ".. _tut1:\n\nTutorial 1 - The Golden Key\n===========================\n\n:author: `begriffs <https://github.com/begriffs>`_\n\nIn :ref:`tut0` we created a read-only API with a single endpoint to list todos. There are many directions we can go to make this API more interesting, but one good place to start would be allowing some users to change data in addition to reading it.\n\nStep 1. Add a Trusted User\n--------------------------\n\nThe previous tutorial created a :code:`web_anon` role in the database with which to execute anonymous web requests. Let's make a role called :code:`todo_user` for users who authenticate with the API. This role will have the authority to do anything to the todo list.\n\n.. code-block:: postgres\n\n  -- run this in psql using the database created\n  -- in the previous tutorial\n\n  create role todo_user nologin;\n  grant todo_user to authenticator;\n\n  grant usage on schema api to todo_user;\n  grant all on api.todos to todo_user;\n\nStep 2. Make a Secret\n---------------------\n\nClients authenticate with the API using JSON Web Tokens. These are JSON objects which are cryptographically signed using a secret only known to the server. Because clients do not know this secret, they cannot tamper with the contents of their tokens. PostgREST will detect counterfeit tokens and will reject them.\n\nLet's create a secret and provide it to PostgREST. Think of a nice long one, or use a tool to generate it. **Your secret must be at least 32 characters long.**\n\n.. note::\n\n  Unix tools can generate a nice secret for you:\n\n  .. code-block:: bash\n\n    # Allow \"tr\" to process non-utf8 byte sequences\n    export LC_CTYPE=C\n\n    # Read random bytes keeping only alphanumerics and add the secret to the configuration file\n    echo \"jwt-secret = \\\"$(< /dev/urandom tr -dc A-Za-z0-9 | head -c32)\\\"\" >> tutorial.conf\n\n\nCheck that the :code:`tutorial.conf` (created in the previous tutorial) has the secret set in :code:`jwt-secret`:\n\n.. code-block:: bash\n\n  # THE SECRET MUST BE AT LEAST 32 CHARS LONG\n  cat tutorial.conf\n\nIf the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.\n\n.. _tut1_step3:\n\nStep 3. Sign a Token\n--------------------\n\nOrdinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one \"by hand\" using ``bash`` and ``openssl``.\n\n.. code:: bash\n\n  #!/bin/bash\n  set -e\n\n  JWT_SECRET='test_secret_that_is_at_least_32_characters_long'\n\n  _base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }\n\n  header=$(echo -n '{\"alg\":\"HS256\",\"typ\":\"JWT\"}' | _base64)\n\n  payload=$(echo -n \"{\\\"role\\\":\\\"todo_user\\\"}\" | _base64)\n\n  signature=$(echo -n \"$header.$payload\" | openssl dgst -sha256 -hmac \"$JWT_SECRET\" -binary | _base64)\n\n  echo -n \"$header.$payload.$signature\"\n\n**Remember to fill in the secret you generated rather than keeping the \"test_secret_that_is_at_least_32_characters_long\".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.\n\n.. note::\n\n  While the token may look well obscured, it's easy to reverse engineer the payload. The token is merely signed, not encrypted, so don't put things inside that you don't want a determined client to see. While it is possible to read the payload of the token, it is not possible to read the secret with which it was signed.\n\nStep 4. Make a Request\n----------------------\n\nBack in the terminal, let's use :code:`curl` to add a todo. The request will include an HTTP header containing the authentication token.\n\n.. code-block:: bash\n\n  export TOKEN=\"<paste token here>\"\n\n  curl http://localhost:3000/todos -X POST \\\n       -H \"Authorization: Bearer $TOKEN\"   \\\n       -H \"Content-Type: application/json\" \\\n       -d '{\"task\": \"learn how to auth\"}'\n\nAnd now we have completed all three items in our todo list, so let's set :code:`done` to true for them all with a :code:`PATCH` request.\n\n.. code-block:: bash\n\n  curl http://localhost:3000/todos -X PATCH \\\n       -H \"Authorization: Bearer $TOKEN\"    \\\n       -H \"Content-Type: application/json\"  \\\n       -d '{\"done\": true}'\n\nA request for the todos shows three of them, and all completed.\n\n.. code-block:: bash\n\n  curl http://localhost:3000/todos\n\n.. code-block:: json\n\n  [\n    {\n      \"id\": 1,\n      \"done\": true,\n      \"task\": \"finish tutorial 0\",\n      \"due\": null\n    },\n    {\n      \"id\": 2,\n      \"done\": true,\n      \"task\": \"pat self on back\",\n      \"due\": null\n    },\n    {\n      \"id\": 3,\n      \"done\": true,\n      \"task\": \"learn how to auth\",\n      \"due\": null\n    }\n  ]\n\nStep 5. Add Expiration\n----------------------\n\nCurrently our authentication token is valid for all eternity. The server, as long as it continues using the same JWT secret, will honor the token.\n\nIt's better policy to include an expiration timestamp for tokens using the :code:`exp` claim. This is one of two JWT claims that PostgREST treats specially.\n\n+--------------+----------------------------------------------------------------+\n| Claim        | Interpretation                                                 |\n+==============+================================================================+\n| :code:`role` | The database role under which to execute SQL for API request   |\n+--------------+----------------------------------------------------------------+\n| :code:`exp`  | Expiration timestamp for token, expressed in \"Unix epoch time\" |\n+--------------+----------------------------------------------------------------+\n\n.. note::\n\n  Epoch time is defined as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), January 1st 1970, minus the number of leap seconds that have taken place since then.\n\nTo observe expiration in action, we'll add an :code:`exp` claim of five minutes in the future to our previous token. First find the epoch value of five minutes from now. In :code:`psql` run this:\n\n.. code-block:: postgres\n\n  select extract(epoch from now() + '5 minutes'::interval) :: integer;\n\nOr in ``bash``:\n\n\n.. code-block:: bash\n\n  exp=$(( EPOCHSECONDS + 5*60 ))  # five minutes\n\n  echo $exp\n\nGo back to :ref:`tut1_step3` and change the payload to\n\n.. code-block:: bash\n\n  payload=$(echo -n \"{\\\"role\\\":\\\"todo_user\\\",\\\"exp\\\":\\\"123456789\\\"}\" | _base64)\n\n  echo -n \"$header.$payload.$signature\"\n\n**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.\n\nCopy the updated token as before, and save it as a new environment variable.\n\n.. code-block:: bash\n\n  export NEW_TOKEN=\"<paste new token>\"\n\nTry issuing this request in curl before and after the expiration time:\n\n.. code-block:: bash\n\n  curl http://localhost:3000/todos \\\n       -H \"Authorization: Bearer $NEW_TOKEN\"\n\nAfter expiration, the API returns HTTP 401 Unauthorized:\n\n.. code-block:: json\n\n  {\n    \"code\": \"PGRST301\",\n    \"details\": null,\n    \"hint\": null,\n    \"message\": \"JWT expired\"\n  }\n\nBonus Topic: Immediate Revocation\n---------------------------------\n\nEven with token expiration there are times when you may want to immediately revoke access for a specific token. For instance, suppose you learn that a disgruntled employee is up to no good and his token is still valid.\n\nTo revoke a specific token we need a way to tell it apart from others. Let's add a custom :code:`email` claim that matches the email of the client issued the token.\n\nGo ahead and make a new token with the payload\n\n.. code-block:: json\n\n  {\n    \"role\": \"todo_user\",\n    \"email\": \"disgruntled@mycompany.com\"\n  }\n\nSave it to an environment variable:\n\n.. code-block:: bash\n\n  export WAYWARD_TOKEN=\"<paste new token>\"\n\nPostgREST allows us to specify a function to run during attempted authentication. The function can do whatever it likes, including raising an exception to terminate the request.\n\nFirst make a new schema and add the function:\n\n.. code-block:: postgres\n\n  create schema auth;\n  grant usage on schema auth to web_anon, todo_user;\n\n  create or replace function auth.check_token() returns void\n    language plpgsql\n    as $$\n  begin\n    if current_setting('request.jwt.claims', true)::json->>'email' =\n       'disgruntled@mycompany.com' then\n      raise insufficient_privilege\n        using hint = 'Nope, we are on to you';\n    end if;\n  end\n  $$;\n\nNext update :code:`tutorial.conf` and specify the new function:\n\n.. code-block:: ini\n\n  # add this line to tutorial.conf\n\n  db-pre-request = \"auth.check_token\"\n\nRestart PostgREST for the change to take effect. Next try making a request with our original token and then with the revoked one.\n\n.. code-block:: bash\n\n  # this request still works\n\n  curl http://localhost:3000/todos -X PATCH \\\n       -H \"Authorization: Bearer $TOKEN\"    \\\n       -H \"Content-Type: application/json\"  \\\n       -d '{\"done\": true}'\n\n  # this one is rejected\n\n  curl http://localhost:3000/todos -X PATCH      \\\n       -H \"Authorization: Bearer $WAYWARD_TOKEN\" \\\n       -H \"Content-Type: application/json\"       \\\n       -d '{\"task\": \"AAAHHHH!\", \"done\": false}'\n\nThe server responds with 403 Forbidden:\n\n.. code-block:: json\n\n  {\n    \"code\": \"42501\",\n    \"details\": null,\n    \"hint\": \"Nope, we are on to you\",\n    \"message\": \"insufficient_privilege\"\n  }\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"REST API for any Postgres database\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-25.05-darwin\";\n  };\n\n  nixConfig = {\n    extra-substituters = \"https://postgrest.cachix.org\";\n    extra-trusted-public-keys = \"postgrest.cachix.org-1:icgW4R15fz1+LqvhPjt4EnX/r19AaqxiVV+1olwlZtI=\";\n  };\n\n  outputs = { nixpkgs, ... }:\n    let\n      systems = [\n        \"aarch64-darwin\"\n        \"aarch64-linux\"\n        \"x86_64-darwin\"\n        \"x86_64-linux\"\n      ];\n\n      pgrstFor = system: import ./default.nix {\n        inherit system;\n        nixpkgsVersion = {\n          owner = \"nixos\";\n          repo = \"nixpkgs\";\n          inherit (nixpkgs) rev;\n          tarballHash = nixpkgs.narHash;\n        };\n      };\n\n      genSystems = f: nixpkgs.lib.genAttrs systems (system: f (pgrstFor system));\n    in\n    {\n      packages = genSystems (attrs: {\n        default = attrs.postgrestPackage.bin;\n        profiled = attrs.postgrestProfiled.bin;\n      } // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {\n        static = attrs.postgrestStatic;\n      });\n\n      apps = genSystems (attrs: {\n        default = {\n          type = \"app\";\n          program = \"${attrs.postgrestStatic or attrs.postgrestPackage.bin}/bin/postgrest\";\n          meta.description = \"REST API for any Postgres database\";\n        };\n      });\n\n      devShells = genSystems (postgrest: {\n        default = import ./shell.nix { inherit postgrest; };\n      });\n    };\n}\n"
  },
  {
    "path": "main/Main.hs",
    "content": "module Main (main) where\n\nimport System.IO (BufferMode (..), hSetBuffering)\n\nimport qualified PostgREST.CLI as CLI\n\nimport Protolude\n\nmain :: IO ()\nmain = do\n  setBuffering\n  opts <- CLI.readCLIShowHelp\n  CLI.main opts\n\nsetBuffering :: IO ()\nsetBuffering = do\n  -- LineBuffering: the entire output buffer is flushed whenever a newline is\n  -- output, the buffer overflows, a hFlush is issued or the handle is closed\n  hSetBuffering stdout LineBuffering\n  hSetBuffering stdin LineBuffering\n  hSetBuffering stderr LineBuffering\n"
  },
  {
    "path": "nix/README.md",
    "content": "# Nix development and build environment\n\nWith Nix it's possible to quickly and reliably recreate the full environments\nfor developing, testing and building PostgREST.\n\n## Getting started with Nix\n\nYou'll need to [get Nix](https://nixos.org/download.html). Follow the recommended installation for your operating system from the official download website.\n\n## Building PostgREST\n\nTo build PostgREST from your local checkout of the repository, run:\n\n```bash\n$ nix-build --attr postgrestPackage\n\n```\n\nThis will create a `result` directory that contains the PostgREST binary at\n`result/bin/postgrest`. The `--attr` parameter (or short: `-A`) tells Nix to\nbuild the `postgrestPackage` attribute from the Nix expression it finds in our\n`default.nix` (see below for details). Nix will take care of getting the right\nGHC version and all the build dependencies.\n\nYou can also build a statically linked binary with:\n\n```bash\n$ nix-build --attr postgrestStatic\n\n$ ldd result/bin/postgrest\n$       not a dynamic executable\n```\n\n## Binary cache\n\nWe recommend that you use the PostgREST binary cache on\n[cachix](https://cachix.org/):\n\n```bash\n# Install cachix:\n$ nix-env -iA cachix -f https://cachix.org/api/v1/install\n\n# Set cachix up to use the PostgREST binary cache:\n$ cachix use postgrest\n\n```\n\nWithout cachix, your machine will have to rebuild all the dependencies that are\nderived on top of `Musl` for the static builds, which can take a very long time.\n\n## Developing\n\nA development environment for PostgREST is available with `nix-shell`. The\nfollowing command will put you into a new shell that has GHC and Cabal on the\nPATH:\n\n```bash\n$ nix-shell\n\n```\n\nWithin `nix-shell`, you can run Cabal commands as usual. You can also run\nstack with the `--nix` option, which causes stack to pick up the non-Haskell\ndependencies from the same pinned Nixpkgs version that the Nix builds use.\n\n## Working with `nix-shell` and the PostgREST utility scripts\n\nThe PostgREST utilities available in `nix-shell` all have names that begin with\n`postgrest-`, so you can use tab completion (typing `postgrest-` and pressing\n`<tab>`) in `nix-shell` to see all that are available:\n\n```bash\n# Note: The utilities listed here might not be up to date.\n[nix-shell]$ postgrest-<tab>\npostgrest-build                   postgrest-parallel-curl\npostgrest-check                   postgrest-profiled-run\npostgrest-clean                   postgrest-push-cachix\npostgrest-commitlint              postgrest-release\npostgrest-coverage                postgrest-repl\npostgrest-coverage-draft-overlay  postgrest-run\npostgrest-docs-build              postgrest-style\npostgrest-docs-check              postgrest-style-check\npostgrest-docs-dictcheck          postgrest-test-big-schema\npostgrest-docs-linkcheck          postgrest-test-doctests\npostgrest-docs-render             postgrest-test-io\npostgrest-docs-serve              postgrest-test-memory\npostgrest-docs-spellcheck         postgrest-test-replica\npostgrest-dump-minimal-imports    postgrest-test-spec\npostgrest-dump-schema             postgrest-test-spec-idempotence\npostgrest-gen-ctags               postgrest-watch\npostgrest-gen-jwt                 postgrest-with-all\npostgrest-gen-secret              postgrest-with-git\npostgrest-git-hooks               postgrest-with-pgrst\npostgrest-hsie-graph-modules      postgrest-with-pg-13\npostgrest-hsie-graph-symbols      postgrest-with-pg-14\npostgrest-hsie-minimal-imports    postgrest-with-pg-15\npostgrest-lint                    postgrest-with-pg-16\npostgrest-loadtest                postgrest-with-pg-17\npostgrest-loadtest-against        postgrest-with-slow-pg\npostgrest-loadtest-report         postgrest-with-slow-postgrest\npostgrest-nixpkgs-upgrade\n...\n\n[nix-shell]$\n\n```\n\nThe `docker` module has large dependencies to be build before the shell becomes\navailable, which could take an especially long time if the cachix binary cache\nis not used. You can activate it by passing a flag to `nix-shell` with\n`nix-shell --arg docker true`. This will make the respective utilities available:\n\n```bash\n$ nix-shell --arg docker true\n[nix-shell]$ postgrest-docker-<tab>\npostgrest-docker-load\n...\n\n```\n\nNote that `postgrest-docker-load` is now also available.\n\nTo run one-off commands, you can also use `nix-shell --run <command>`, which\nwill launch the Nix shell, run that one command and exit. Note that the tab\ncompletion will not work with `nix-shell --run`, as Nix has yet to evaluate\nour Nix expressions to see which utilities are available.\n\n```bash\n$ nix-shell --run postgrest-style\n\n# Note that you need to quote any arguments that you would like to pass to\n# the command to be run in nix-shell:\n$ nix-shell --run \"postgrest-foo --bar\"\n\n```\n\nA third option is to install utilities that you use very often locally:\n\n```bash\n$ nix-env -f default.nix -iA devTools\n\n# `postgrest-style` can now be run directly:\n$ postgrest-style\n\n```\n\nIf you use `nix-shell` very often, you might like to use\nhttps://github.com/xzfc/cached-nix-shell, which skips evaluating all our Nix\nexpressions if nothing changed, reducing startup time for the shell\nconsiderably.\n\nNote: Once inside nix-shell, the utilities work from any directory inside\nthe PostgREST repo. Paths are resolved relative to the repo root:\n\n```bash\n[nix-shell]$ cd src\n# Even though the current directory is ./src, the config path must still start\n# from the repo root:\n[nix-shell]$ postgrest-run test/io/configs/simple.conf\n```\n\n## Testing\n\nIn nix-shell, you'll find utility scripts that make it very easy to run our\ntest suite, including setting up all required dependencies and\ntemporary test databases:\n\n```bash\n# Run the tests against the most recent version of PostgreSQL:\n$ nix-shell --run postgrest-test-spec\n\n# Run the tests against all supported versions of PostgreSQL:\n$ nix-shell --run \"postgrest-with-all postgrest-test-spec\"\n\n# Run the tests against a specific version of PostgreSQL (use tab-completion in\n# nix-shell to see all available versions):\n$ nix-shell --run \"postgrest-with-pg-13 postgrest-test-spec\"\n\n```\n\nThe io-test that test PostgREST as a black box with inputs and outputs can be\nrun with `postgrest-test-io`. The test runner under the hood is\n[pytest](https://docs.pytest.org/) and you can pass it the usual options:\n\n```bash\n# Filter the tests to run by name, including all that contain 'config':\n[nix-shell]$ postgrest-test-io -k config\n\n# Run tests in parallel using xdist, specifying the number of processes:\n[nix-shell]$ postgrest-test-io -n auto\n[nix-shell]$ postgrest-test-io -n 8\n```\n\nThe memory tests check that we don't surpass a memory threshold for big request bodies.\n\n```bash\n# Build the dependencies needed for the memory test\n$ nix-shell --arg memory true\n\n# Run the memory test\n[nix-shell]$ postgrest-test-memory\n```\n\nThe loadtests ensure that performance doesn't drop on a change. Underlyingly they use\n[vegeta](https://github.com/tsenart/vegeta).\n\n```bash\n# Run the loadtests on the latest commit(HEAD)\n[nix-shell]$ postgrest-loadtest\n\n# You can loadtest comparing to a different branch\n[nix-shell]$ postgrest-loadtest-against master\n\n# You can simulate latency client/postgrest and postgrest/database\n[nix-shell]$ PGRST_DELAY=5ms PGDELAY=5ms postgrest-loadtest\n\n# You can build postgrest directly with cabal for faster iteration\n[nix-shell]$ PGRST_BUILD_CABAL=1 postgrest-loadtest\n\n# Produce a markdown report to be used on CI\n[nix-shell]$ postgrest-loadtest-report\n```\n\ndoctests for some of our modules are also available:\n\n```bash\n[nix-shell]$ postgrest-test-doctest\n```\n\n## Code coverage\n\nCode coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized on a browser.\n\n```bash\n# Will run all the tests and produce a coverage dir\n[nix-shell]$ postgrest-coverage\n...\n\npostgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html\n```\n\n## Linting and styling code\n\nThe nix-shell also contains scripts for linting and styling the PostgREST\nsource code:\n\n```bash\n# Linting\n$ nix-shell --run postgrest-lint\n\n# Styling / auto-formatting code\n$ nix-shell --run postgrest-style\n\n```\n\nThere is also `postgrest-style-check` that exits with a non-zero exit code if\nthe check resulted in any uncommitted changes. It's mostly useful for CI.\n\n## Documentation\n\nThe following commands can help you when working on the PostgREST docs:\n\n```bash\n# Build the docs\n[nix-shell]$ postgrest-docs-build\n\n# Build the docs and start a livereload server on `http://localhost:5500`\n[nix-shell]$ postgrest-docs-serve\n\n# Run aspell, to verify spelling mistakes\n[nix-shell]$ postgrest-docs-spellcheck\n\n# Detect obsolete entries in postgrest.dict\n[nix-shell]$ postgrest-docs-dictcheck\n\n# Build and run all the validation scripts\n[nix-shell]$ postgrest-docs-check\n```\n\n## General development tools\n\nTools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple wrappers around\n`cabal` and should do what you expect. `postgrest-check` runs most checks that will\nalso run in CI, with the exception of the IO and Memory checks that need to be run\nseparately.\n\n`postgrest-with-pg-*` take a command as an argument and will run it\nwith a temporary database. `postgrest-with-all` will run the command against\nall supported PostgreSQL versions. Tests run without `postgrest-with-*` are\nrun against the latest PostgreSQL version by default.\n\n`postgrest-watch` takes a command as an argument that it will re-run if any source\nfile is changed. For example, `postgrest-watch postgrest-with-all postgrest-test-spec`\nwill re-run the full spec test suite against all PostgreSQL versions on every change.\n\n## REPL\n\nYou can use `postgrest-repl` to manually inspect the PostgREST modules.\n\n```bash\n$ postgrest-repl\n\nghci> import PostgREST.<tab>\nPostgREST.Admin                        PostgREST.Config.Database              PostgREST.Plan.MutatePlan              PostgREST.Response.OpenAPI\nPostgREST.ApiRequest                   PostgREST.Config.JSPath                PostgREST.Plan.ReadPlan                PostgREST.SchemaCache\n...\n\nghci> import PostgREST.MediaType\nghci> decodeMediaType \"application/json\"\nMTApplicationJSON\n```\n\n## Working with locally modified Haskell packages\n\nSometimes, we need to modify Haskell libraries in order to debug them or enhance them.\nFor example, if you want to debug the [`hasql-pool`](https://hackage.haskell.org/package/hasql-pool)\nlibrary:\n\nFirst, copy the package to the repo root. We'll use GitHub in this example.\n\n```bash\n$ git clone --depth=1 --branch=0.10.1 https://github.com/nikita-volkov/hasql-pool.git\n$ rm -rf ./hasql-pool/.git\n```\n\nThen, pin the local package to the [`haskell-packages.nix`](./overlays/haskell-packages.nix) file.\n\n```nix\n  overrides =\n    # ...\n    rec {\n\n      # Different subpath may be needed if the cabal file is not in the library's base directory\n      hasql-pool = lib.dontCheck\n        (prev.callCabal2nixWithOptions \"hasql-pool\" ../../hasql-pool \"--subpath=.\" {} );\n\n    };\n```\n\nNext, both [`cabal.project`](/cabal.project) and [`stack.yaml`](/stack.yaml) need to be updated\nwith the local library:\n\n```cabal\n-- cabal.project\npackages:\n  ./hasql-pool/hasql-pool.cabal\n```\n\n```yaml\n# stack.yaml\nextra-deps:\n  - ./hasql-pool/hasql-pool.cabal\n```\n\nLastly, run `nix-shell` to build the local package. You don't need to exit and\nenter the Nix shell every time you modify the library's code, re-executing\n`postgrest-run` should be enough.\n\nThis is done for development purposes only. Local libraries must not be left\nin production ready code.\n\n## Tour\n\nThe following is not required for working on PostgREST with Nix, but it will\ngive you some more background and details on how it works.\n\n### `default.nix`\n\n[`default.nix`](../default.nix) is our 'repository expression' that pulls all\nthe pieces that we define with Nix together. It returns a set (like a dict in\nother programming languages), where each attribute is a derivation that Nix\nknows how to build, like the `postgrest` attribute from earlier.\n\nInternally, our `default.nix` uses the `pkgs.callPackage` function to import\nthe modules that we defined in the `nix` directory. It automatically passes the\narguments those modules require if they are available in `pkgs` (this means\nthat `pkgs` is defined in terms of itself, better not to think too much about\nthat).\n\nWe also use `default.nix` to load our pinned version of the `nixpkgs`\nrepository. This set of packages will always be the same, independently from\nwhere or when you use it. The pinned version is taken from `flake.lock` and\ncan be updated with `postgrest-nixpkgs-upgrade`.\n\n### `shell.nix`\n\n[`shell.nix`](../shell.nix) defines an environment in which PostgREST can be\nbuilt and developed. It extends the build environment from our `postgrest`\nattribute with useful utilities that will be put on the PATH in `nix-shell`.\n\n### `nix/overlays`\n\nOur overlays to the Nix package set are defined here. They allow us to tweak our\n`pkgs` in `default.nix` by adding new packages or overriding existing ones.\n\n## Upgrading dependencies\n\nSee the [upgrading checklist](UPGRADE.md) for how to upgrade the PostgREST\ndependencies.\n"
  },
  {
    "path": "nix/UPGRADE.md",
    "content": "# Checklist for upgrading Nix dependencies\n\nThe Nix dependencies of PostgREST should be updated regularly, in most cases it\nshould be a very simple operation.\n\n```bash\n# Update pinned version of Nixpkgs\nnix-shell --run postgrest-nixpkgs-upgrade\n\n# Verify that everything builds\nnix-build\n```\n\nThe following checklist guides you through the complete process in more detail.\n\n## Upgrade the pinned version of `nixpkgs`\n\nThe pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined\nin [`nix/nixpkgs-version.nix`](nixpkgs-version.nix). The pin refers directly to\na GitHub tarball for the given revision, which is more efficient than pulling\nthe complete Git repository. To upgrade it to the current `main` of\n`nixpkgs`, you can use a small utility script defined in\n[`nix/nixpkgs-update.nix`](nixpkgs-update.nix):\n\n```bash\n# From the root of the repository, enter nix-shell\nnix-shell\n\n# Run the utility script to pin the latest revision in main\npostgrest-nixpkgs-upgrade\n\n# Exit the nix-shell with Ctrl-d\n\n```\n\n## Review overlays\n\nCheck whether the individual [overlays](overlays) are still required.\n\n## Check if patches are still required and update them as needed\n\nWe track a number of PostgREST-specific patches in [`nix/patches`](patches).\nCheck whether the pull-requests/issues linked in the\n[`default.nix`](patches/default.nix) have progressed and remove/modify the\npatches if they did. If conflicting changes occurred, you might have to rebase\nthe respective patches.\n\n## Build everything\n\nUsing the PostgREST binary Nix cache is recommended. Install\n[Cachix](https://cachix.org/) and run `cachix use postgrest`.\n\nRun `nix-build` in the root directory of the project to build all PostgREST\nartifacts. This might take a long time, e.g. when our static GHC version needs\nto be rebuilt due to changes to some underlying package. If there are any\nerrors, this is probably due to one of our patches. Try to fix them and re-run\n`nix-build` until everything builds.\n\n## Update the PostgREST binary cache\n\nIf you have access to the PostgREST cachix signing key, you can push the\nartifacts that you built locally to the binary cache. This will accelerate the\nCI builds and tests, sometimes dramatically. This might sometimes even be\nrequired to avoid build timeouts in CI.\n\nYou'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating\na file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which\navoids having the secret in your shell history.\n\nTo push all new artifacts to Cachix, run:\n\n```\nnix-store -qR --include-outputs $$(nix-instantiate) | cachix push postgrest\n\n# Or, equivalently\nnix-shell --run postgrest-push-cachix\n\n```\n\nThe `nix-store` command will query the nix-store to list all dependencies and\nbuild artifacts of PostgREST. The `cachix` command will efficiently push\neverything that is not yet cached to the binary cache.\n"
  },
  {
    "path": "nix/hsie/Main.hs",
    "content": "{-# LANGUAGE DeriveAnyClass    #-}\n{-# LANGUAGE DeriveGeneric     #-}\n{-# LANGUAGE NamedFieldPuns    #-}\n{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE RecordWildCards   #-}\n{-# LANGUAGE TupleSections     #-}\n{-# LANGUAGE TypeFamilies      #-}\n\n-- | Haskell Imports and Exports tool\n--\n-- This tool parses imports and exports from Haskell source files and provides\n-- analysis on these imports. For example, you can check whether consistent\n-- import aliases are used across your codebase.\n\nmodule Main (main) where\n\nimport qualified Data.Aeson                              as JSON\nimport qualified Data.ByteString.Lazy.Char8              as LBS8\nimport qualified Data.Csv                                as Csv\nimport qualified Data.Map                                as Map\nimport qualified Data.Set                                as Set\nimport qualified Data.Text                               as T\nimport qualified Data.Text.IO                            as T\nimport qualified Dot\nimport qualified GHC\nimport qualified GHC.Paths\nimport qualified Language.Haskell.GHC.ExactPrint.Parsers as ExactPrint\nimport qualified Options.Applicative                     as O\nimport qualified System.FilePath                         as FP\n\nimport Data.Aeson.Encode.Pretty   (encodePretty)\nimport Data.Function              ((&))\nimport Data.List                  (intercalate)\nimport Data.Maybe                 (catMaybes, mapMaybe)\nimport Data.Text                  (Text)\nimport GHC.Generics               (Generic)\nimport GHC.Hs.Extension           (GhcPs)\nimport GHC.Types.Error            (getMessages)\nimport GHC.Types.Name.Occurrence  (occNameString)\nimport GHC.Types.Name.Reader      (rdrNameOcc)\nimport GHC.Unit.Module.Name       (moduleNameString)\nimport GHC.Utils.Error            (pprMsgEnvelopeBagWithLoc)\nimport System.Directory.Recursive (getFilesRecursive)\nimport System.Exit                (exitFailure)\n\n-- TYPES\n\ndata Options =\n  Options\n    { command :: Command\n    , sources :: [FilePath]\n    }\n\ndata Command\n  = Dump OutputFormat\n  | GraphSymbols\n  | GraphModules\n  | CheckAliases\n  | CheckWildcards [Text]\n\ndata OutputFormat = OutputCsv | OutputJson\n\ndata ImportedSymbol =\n  ImportedSymbol\n    { impFromModule :: Text\n    , impModule     :: Text\n    , impQualified  :: ImportQualified\n    , impAlias      :: Maybe Text\n    , impType       :: ImportType\n    , impSymbol     :: Maybe Text\n    , impInternal   :: ModuleInternal\n    , impSource     :: FilePath\n    , impFile       :: FilePath\n    }\n    deriving (Generic, Csv.ToNamedRecord, Csv.DefaultOrdered, JSON.ToJSON)\n\ndata ImportQualified\n  = Qualified\n  | NotQualified\n  deriving (Eq, Generic, JSON.ToJSON)\n\ninstance Csv.ToField ImportQualified where\n  toField Qualified    = \"qualified\"\n  toField NotQualified = \"not qualified\"\n\ndata ModuleInternal\n  = Internal\n  | External\n  deriving (Eq, Generic, JSON.ToJSON)\n\ninstance Csv.ToField ModuleInternal where\n  toField Internal = \"internal\"\n  toField External = \"external\"\n\ndata ImportType\n  = Wildcard\n  | Hiding\n  | Explicit\n  deriving (Eq, Generic, JSON.ToJSON)\n\ninstance Csv.ToField ImportType where\n  toField Wildcard = \"wildcard\"\n  toField Hiding   = \"hiding\"\n  toField Explicit = \"explicit\"\n\n-- | Mapping of modules to their aliases and to the files they are found in\ntype ModuleAliases = [(Text, [(Text, [FilePath])])]\n\n-- | Mapping of modules to files\ntype WildcardImports = [(FilePath, [Text])]\n\n\n-- MAIN\n\nmain :: IO ()\nmain =\n  run =<< O.customExecParser prefs infoOpts\n  where\n    prefs = O.prefs $ O.subparserInline <> O.showHelpOnEmpty\n    infoOpts =\n      O.info (O.helper <*> opts) $\n        O.fullDesc\n        <> O.header \"hsie - Swiss army knife for HaSkell Imports and Exports\"\n        <> O.progDesc \"Parse Haskell code to analyze imports and exports\"\n    opts =\n      Options <$> commandOption <*> O.some srcOption\n    srcOption =\n      O.argument O.str $\n        O.metavar \"SRCDIR\"\n        <> O.help \"Haskell source directory\"\n        <> O.action \"directory\"\n    commandOption =\n      O.subparser $\n        command \"dump-imports\" \"Dump imported symbols as CSV or JSON\"\n          (Dump <$> jsonOutputFlag)\n        <> command \"graph-modules\" \"Print dot graph of module imports\"\n             (pure GraphModules)\n        <> command \"graph-symbols\" \"Print dot graph of symbol imports\"\n             (pure GraphSymbols)\n        <> command \"check-aliases\"\n             \"Check that aliases of imported modules are consistent\"\n             (pure CheckAliases)\n        <> command \"check-wildcards\"\n             \"Check that no modules are imported as unqualified wildcards\"\n             (CheckWildcards <$> O.many okModuleOption)\n    command name desc options =\n      O.command name . O.info (O.helper <*> options) $ O.progDesc desc\n    jsonOutputFlag =\n      O.flag OutputCsv OutputJson $\n        O.long \"json\" <> O.short 'j' <> O.help \"Output JSON\"\n    okModuleOption =\n      O.strOption $\n        O.long \"ok\"\n        <> O.short 'o'\n        <> O.metavar \"OKMODULE\"\n        <> O.help \"Module that is ok to import as unqualified wildcard\"\n\nrun :: Options -> IO ()\nrun Options{command, sources} =\n  runCommand command . markInternal . concat =<< mapM sourceSymbols sources\n  where\n    runCommand :: Command -> [ImportedSymbol] -> IO ()\n    runCommand (Dump format) = LBS8.putStr . dump format\n    runCommand GraphSymbols = T.putStr . symbolsGraph\n    runCommand GraphModules = T.putStr . Dot.encode . modulesGraph\n    runCommand CheckAliases = runInconsistentAliases . inconsistentAliases\n    runCommand (CheckWildcards okModules) = runWildcards . wildcards okModules\n\n    runInconsistentAliases :: ModuleAliases -> IO ()\n    runInconsistentAliases [] = T.putStrLn \"No inconsistent module aliases found.\"\n    runInconsistentAliases xs = T.putStr (formatInconsistentAliases xs) >> exitFailure\n\n    runWildcards :: WildcardImports -> IO ()\n    runWildcards [] = T.putStrLn \"No unwanted wildcard imports found.\"\n    runWildcards xs = T.putStr (formatWildcards xs) >> exitFailure\n\n-- | Mark imports from modules that are among the analyzed ones as internal.\nmarkInternal :: [ImportedSymbol] -> [ImportedSymbol]\nmarkInternal symbols =\n  fmap mark symbols\n  where\n    mark s = s { impInternal = if isInternal s then Internal else External }\n    isInternal = flip Set.member internalModules . impModule\n    internalModules = Set.fromList $ fmap impFromModule symbols\n\n\n-- SYMBOLS\n\n-- | Parse all imported symbols from a source of Haskell source files\nsourceSymbols :: FilePath -> IO [ImportedSymbol]\nsourceSymbols source = do\n  files <- filterExts [\".hs\", \".imports\"] <$> getFilesRecursive source\n  concat <$> mapM moduleSymbols files\n  where\n    filterExts exts = filter $ flip elem exts . FP.takeExtension\n    moduleSymbols filepath = do\n      GHC.HsModule{..} <- parseModule filepath\n      return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports\n\n-- | Parse a Haskell module\nparseModule :: FilePath -> IO GHC.HsModule\nparseModule filepath = do\n  result <- ExactPrint.parseModule GHC.Paths.libdir filepath\n  case result of\n    Right hsmod ->\n      return $ GHC.unLoc hsmod\n    Left errs ->\n      fail $ \"Errors with \" <> show filepath <> \":\\n    \"\n        <> show (pprMsgEnvelopeBagWithLoc $ getMessages errs)\n\n-- | Symbols imported in an import declaration.\n--\n-- If the import is a wildcard, i.e. no symbols are selected for import, then\n-- only one item is returned.\nimportSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]\nimportSymbols source filepath GHC.ImportDecl{..} =\n  case ideclHiding of\n    Just (hiding, syms) ->\n      symbol (if hiding then Hiding else Explicit) . Just . GHC.unLoc <$> GHC.unLoc syms\n    Nothing ->\n      [ symbol Wildcard Nothing ]\n  where\n    symbol hiding sym =\n      ImportedSymbol\n        { impFile = relativePath filepath\n        , impSource = source\n        , impFromModule = T.pack $ moduleFromPath filepath\n        , impModule = T.pack . moduleNameString . GHC.unLoc $ ideclName\n        , impQualified = if ideclQualified /= GHC.NotQualified then Qualified else NotQualified\n        , impAlias = T.pack . moduleNameString . GHC.unLoc <$> ideclAs\n        , impInternal = External\n        , impType = hiding\n        , impSymbol = T.pack . occNameString . rdrNameOcc . GHC.ieName <$> sym\n        }\n    moduleFromPath =\n      intercalate \".\" . FP.splitDirectories . FP.dropExtension . relativePath\n    relativePath = FP.makeRelative source\n\n\n-- DUMP\n\n-- | Dump list of symbols as CSV or JSON\ndump :: OutputFormat -> [ImportedSymbol] -> LBS8.ByteString\ndump OutputCsv  = Csv.encodeDefaultOrderedByName\ndump OutputJson = encodePretty\n\n\n-- ALIASES\n\n-- | Find modules that are imported under different aliases\ninconsistentAliases :: [ImportedSymbol] -> ModuleAliases\ninconsistentAliases symbols =\n  foldr (insertSetMapMap . moduleAlias) Map.empty symbols\n    & Map.map (aliases . Map.toList)\n    & Map.filter ((<) 1 . length)\n    & Map.toList\n  where\n    moduleAlias ImportedSymbol{..} =\n      (impModule, impAlias, FP.joinPath [impSource, impFile])\n    insertSetMapMap (k1, k2, v) =\n      Map.insertWith (Map.unionWith Set.union) k1\n        (Map.singleton k2 $ Set.singleton v)\n    aliases :: [(Maybe Text, Set.Set FilePath)] -> [(Text, [FilePath])]\n    aliases = mapMaybe (\\(k, v) -> fmap (, Set.toList v) k)\n\nformatInconsistentAliases :: ModuleAliases -> Text\nformatInconsistentAliases modules =\n  \"The following imports have inconsistent aliases:\\n\\n\"\n    <> T.concat (fmap formatModule modules)\n  where\n    formatModule (modName, aliases) =\n      \"Module '\"\n        <> modName\n        <> \"' has the aliases:\\n\"\n        <> T.concat (fmap formatAlias aliases)\n        <> \"\\n\"\n    formatAlias (alias, sourceFiles) =\n      \"  '\"\n        <> alias\n        <> \"' in file\"\n        <> (if length sourceFiles > 2 then \"s\" else \"\")\n        <> \":\\n\"\n        <> T.concat (fmap formatFile sourceFiles)\n    formatFile sourceFile =\n      \"    \" <> T.pack sourceFile <> \"\\n\"\n\n\n-- WILDCARDS\n\n-- | Find modules that are imported as wildcards, excluding whitelisted modules.\n--\n-- Wildcard imports are ones that are not qualified and do not specify which\n-- symbols should be imported.\nwildcards :: [Text] -> [ImportedSymbol] -> WildcardImports\nwildcards okModules =\n  groupByFile . filter isWildcard . filter (not . isOkModule)\n  where\n    isWildcard ImportedSymbol{..} =\n      impQualified == NotQualified && impType /= Explicit\n    isOkModule = flip Set.member (Set.fromList okModules) . impModule\n    groupByFile = Map.toList . fmap Set.toList . foldr insertMap Map.empty\n    insertMap ImportedSymbol{..} =\n      Map.insertWith Set.union impFile (Set.singleton impModule)\n\nformatWildcards :: WildcardImports -> Text\nformatWildcards files =\n  \"Modules in the following files were imported as wildcards:\\n\\n\"\n    <> T.concat (fmap formatFile files)\n  where\n    formatFile (filepath, modules) =\n      \"In \" <> T.pack filepath <> \":\\n\" <> T.concat (fmap formatModule modules) <> \"\\n\"\n    formatModule moduleName = \"  \" <> moduleName <> \"\\n\"\n\n\n-- GRAPHS\n\nmodulesGraph :: [ImportedSymbol] -> Dot.DotGraph\nmodulesGraph symbols =\n  Dot.DotGraph Dot.Strict Dot.Directed (Just \"Modules\") $ fmap edge edges\n  where\n    edge (from, to) =\n      Dot.StatementEdge $ Dot.EdgeStatement\n        (Dot.ListTwo (edgeNode from) (edgeNode to) mempty) mempty\n    edgeNode t = Dot.EdgeNode $ Dot.NodeId (Dot.Id t) Nothing\n    edges = unique . fmap edgeTuple . filter ((==) Internal . impInternal) $ symbols\n    edgeTuple ImportedSymbol{..} = (impFromModule, impModule)\n    unique = Set.toList . Set.fromList\n\n-- Building Text directly as the Dot package currently doesn't support subgraphs.\nsymbolsGraph :: [ImportedSymbol] -> Text\nsymbolsGraph symbols =\n  \"digraph Symbols {\\n\"\n    <> \"  rankdir=LR\\n\"\n    <> \"  ranksep=5\\n\"\n    <> T.concat (fmap edge edges)\n    <> T.concat (fmap cluster symbolsByModule)\n    <> \"}\\n\"\n  where\n    edge (from, to, symbol) =\n      \"  \"\n        <> quoted from\n        <> \" -> \"\n        <> quoted (to <> maybe \"\" (\".\" <>) symbol)\n        <> \"\\n\"\n    cluster (moduleName, clusterSymbols) =\n      \"  subgraph \"\n        <> quoted (\"cluster_\" <> moduleName)\n        <> \" {\\n\"\n        <> \"    \" <> quoted moduleName <> \"\\n\"\n        <> T.concat (fmap (clusterNode moduleName) clusterSymbols)\n        <> \"  }\\n\"\n    clusterNode moduleName symbol =\n      \"    \" <> quoted (moduleName <> \".\" <> symbol) <> \"\\n\"\n    quoted t = \"\\\"\" <> t <> \"\\\"\"\n    edges = unique . fmap edgeTuple . filter ((==) Internal . impInternal) $ symbols\n    edgeTuple ImportedSymbol{..} = (impFromModule, impModule, impSymbol)\n    unique = Set.toList . Set.fromList\n    symbolsByModule =\n      Map.toList . Map.map (catMaybes . Set.toList) . foldr insertMap Map.empty $ edges\n    insertMap (_, to, symbol) = Map.insertWith Set.union to $ Set.singleton symbol\n"
  },
  {
    "path": "nix/hsie/README.md",
    "content": "# hsie - Swiss army knife for HaSkell Imports and Exports\n\nThis tool parses Haskell source code to analyse the imports and exports in a\nproject. It's available in PostgREST's `nix-shell` by default.\n\n## Dumping imports\n\nGiven source code in the directories `src` and `main`, for example, you can run:\n\n```\nhsie dump-imports src main\n```\n\nThis dumps all imports of the modules in the given directory to a CSV file,\nprinted on `stdout`.\n\nTo dump to a JSON file (e.g., to further process with `jq`), add the `--json`\nflag:\n\n```\nhsie dump-imports --json src main\n```\n\n## Graphing imports\n\nThe tool can generate `graphviz` graphs of module and symbol imports by printing\na file to `stdout` that can directly be rendered with `dot`:\n\n```\nhsie graph-modules src main | dot -Tpng -o modules.png\n```\n\nThe command `graph-modules` prints a graph of which modules insert which other\nmodules. `graph-symbols` shows which symbols are imported from which modules.\n\n## Checking imports\n\nTo check whether modules are imported under consistent aliases in your project,\nrun:\n\n```\nhsie check-aliases main src\n```\n\nThis will exit with a non-zero exit code if any inconsistent aliases are found.\n\nThe following command checks whether any modules are imported as wildcards, i.e.\nnot qualified and without specifying symbols.\n\n```\nhsie check-wildcards main src\n```\n\nTo whitelist certain modules to be imported as wildcards, use `--ok`:\n\n```\nhsie check-wildcards main src --ok Protolude --ok Test.Module\n```\n\n## Current limitations\n\nThis tool uses the GHC parser to parse Haskell source code. Language extensions\nrequired to parse each file are detected based on the `{-# LANGUAGE ... #-}`\npragmas. If they are not available (e.g., as they are listed as default\nextensions in the `.cabal` file), parses may fail. We can fix this by using\nan extended set of non-conflicting extensions by default, as `hlint` does for\nexample.\n"
  },
  {
    "path": "nix/hsie/default.nix",
    "content": "{ ghcWithPackages\n, runCommand\n}:\nlet\n  name = \"hsie\";\n  src = ./Main.hs;\n  modules = ps: [\n    ps.aeson\n    ps.aeson-pretty\n    ps.cassava\n    ps.dir-traverse\n    ps.dot\n    ps.ghc-exactprint\n    ps.ghc-paths\n    ps.optparse-applicative\n  ];\n  ghc = ghcWithPackages modules;\n  hsie =\n    runCommand \"haskellimports\" { inherit name src; }\n      ''\n        cd $TMP\n        cp $src $TMP/Main.hs\n        ${ghc}/bin/ghc -O -Werror -Wall -package ghc Main.hs -o Main\n        cp Main $out\n      '';\n  bin =\n    runCommand name { inherit hsie name; }\n      ''\n        mkdir -p $out/bin\n        ln -s $hsie $out/bin/$name\n      '';\n  bash-completion =\n    runCommand \"${name}-bash-completion\" { inherit bin name; }\n      \"$bin/bin/$name --bash-completion-script $bin/bin/$name > $out\";\nin\nhsie // { inherit bash-completion bin; }\n"
  },
  {
    "path": "nix/overlays/build-toolbox/build-toolbox.nix",
    "content": "# Creates an environment that exposes bash-completion arguments from all checkedShellScripts\n{ buildEnv }:\n{ name\n, tools\n, extra ? { }\n}:\nlet\n  bash-completion = builtins.map (tool: (builtins.getAttr tool tools).bash-completion) (builtins.attrNames tools);\n\n  env = buildEnv {\n    inherit name;\n    paths = builtins.map (tool: (builtins.getAttr tool tools).bin) (builtins.attrNames tools);\n  };\n\nin\nenv // tools // { inherit bash-completion; } // extra\n"
  },
  {
    "path": "nix/overlays/build-toolbox/default.nix",
    "content": "_: super:\n# Overlay that adds `buildToolbox`, an enhanced version of `buildEnv`\n{\n  buildToolbox = super.callPackage ./build-toolbox.nix { };\n}\n"
  },
  {
    "path": "nix/overlays/checked-shell-script/checked-shell-script.nix",
    "content": "# Create a bash script that is checked with shellcheck. You can either use it\n# directly, or use the .bin attribute to get the script in a bin/ directory,\n# to be used in a path for example.\n{ argbash\n, bash\n, coreutils\n, git\n, lib\n, moreutils\n, runCommand\n, shellcheck\n, stdenv\n, writeTextFile\n}:\n{ name\n, docs\n, args ? [ ]\n, positionalCompletion ? \"\"\n, redirectTixFiles ? true\n, withEnv ? null\n, withPath ? [ ]\n, withTmpDir ? false\n, workingDir ? null\n}: text:\nassert workingDir == null || lib.hasPrefix \"/\" workingDir;\nlet\n  # square brackets are a pain to escape - if even possible. just don't use them...\n  escape = builtins.replaceStrings [ \"\\n\" ] [ \" \\\\n\" ];\n\n  argsTemplate =\n    writeTextFile {\n      inherit name;\n      destination = \"/${name}.m4\"; # destination is needed to have the proper basename for completion\n\n      text =\n        ''\n          # BASH_ARGV0 sets $0 - which is used in parser.sh for usage information\n          # stripping the /nix/store/... path for nicer display\n          BASH_ARGV0=\"$(basename \"$0\")\"\n\n          # ARG_HELP([${name}], [${escape docs}])\n          ${lib.strings.concatMapStrings (arg: \"# \" + arg) args}\n          # ARG_POSITIONAL_DOUBLEDASH()\n          # ARG_DEFAULTS_POS()\n          # ARGBASH_GO\n\n        '';\n    };\n\n  argsParser =\n    runCommand \"${name}-parser\" { }\n      ''\n        ${argbash}/bin/argbash ${argsTemplate}/${name}.m4 > $out\n\n        # This forces optional arguments to go *before* positional arguments,\n        # which allows leftovers to pass optional arguments to sub-commands.\n        # Example: This way `postgrest-watch -h` will return the help output for watch, while\n        # `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec.\n        # Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274\n        sed '/_positionals_count + 1/a\\\\t\\t\\t\\tset -- \"''${@:1:1}\" \"--\" \"''${@:2}\"' $out | ${moreutils}/bin/sponge $out\n      '';\n\n  bash-completion =\n    runCommand \"${name}-completion\" { } (\n      ''\n        ${argbash}/bin/argbash --type completion --strip all ${argsTemplate}/${name}.m4 > $out\n      ''\n\n      + lib.optionalString (positionalCompletion != \"\") ''\n        sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' $out | ${moreutils}/bin/sponge $out\n      ''\n    );\n\n  bin =\n    writeTextFile {\n      inherit name;\n      executable = true;\n      destination = \"/bin/${name}\";\n\n      text =\n        ''\n          #!${bash}/bin/bash\n          source ${argsParser}\n          set -euo pipefail\n        ''\n\n        + lib.optionalString redirectTixFiles ''\n          # storing tix files in a temporary throw away directory avoids mix/tix conflicts after changes\n          hpctixdir=$(${coreutils}/bin/mktemp -d)\n          export HPCTIXFILE=\"$hpctixdir\"/postgrest.tix\n          trap 'rm -rf $hpctixdir' EXIT\n        ''\n\n        + lib.optionalString (workingDir != null) ''\n          cd \"$(${git}/bin/git rev-parse --show-toplevel)\"\n\n          if test ! -f postgrest.cabal; then\n            >&2 echo \"Couldn't find postgrest.cabal. Please make sure to\" \\\n                     \"run this command somewhere in the PostgREST repo.\"\n            exit 1\n          fi\n\n          cd \"''${PWD}${workingDir}\"\n        ''\n\n        + lib.optionalString withTmpDir ''\n          tmpdir=\"$(${coreutils}/bin/mktemp -d --tmpdir ${name}-XXX)\"\n\n          # we keep the tmpdir when an error occurs for debugging\n          trap 'echo Temporary directory kept at: $tmpdir' ERR\n          # remove the tmpdir when cancelled (postgrest-watch)\n          trap 'rm -rf \"$tmpdir\"' SIGINT SIGTERM\n        ''\n\n        + lib.optionalString (withEnv != null) ''\n          env=\"$(cat ${withEnv})\"\n          export PATH=\"$env/bin:$PATH\"\n        ''\n\n        + lib.optionalString (lib.length withPath > 0) ''\n          export PATH=\"${lib.concatMapStrings (p: p + \"/bin:\") withPath}$PATH\"\n        ''\n\n        + \"(${text})\"\n\n        + lib.optionalString withTmpDir ''\n\n          rm -rf \"$tmpdir\"\n        '';\n\n      checkPhase =\n        ''\n          # check syntax\n          ${stdenv.shell} -n $out/bin/${name}\n\n          # check for shellcheck recommendations\n          ${shellcheck}/bin/shellcheck -x $out/bin/${name}\n        '';\n    };\n\n  script =\n    runCommand name { inherit bin name; } \"ln -s $bin/bin/$name $out\";\nin\nscript // { inherit bin bash-completion; }\n"
  },
  {
    "path": "nix/overlays/checked-shell-script/default.nix",
    "content": "_: super:\n# Overlay that adds `checkedShellScript`, an enhanced version of\n# writeShellScript and writeShellScriptBin\n{\n  checkedShellScript = super.callPackage ./checked-shell-script.nix { };\n}\n"
  },
  {
    "path": "nix/overlays/default.nix",
    "content": "{\n  build-toolbox = import ./build-toolbox;\n  checked-shell-script = import ./checked-shell-script;\n  gitignore = import ./gitignore.nix;\n  haskell-packages = import ./haskell-packages.nix;\n}\n"
  },
  {
    "path": "nix/overlays/gitignore.nix",
    "content": "_: super:\n# Overlay that adds the `gitignoreSource` function from Hercules-CI.\n# This function is useful for filtering which files are added to the Nix store.\n# See: https://github.com/hercules-ci/gitignore.nix\n\n# To update to a newer revision, the simplest way is to add a new commit hash\n# from GitHub under `rev` and to then add the hash that Nix suggests on first\n# use.\n{\n  gitignoreSource =\n    let\n      gitignoreSrc = super.fetchFromGitHub {\n        owner = \"hercules-ci\";\n        repo = \"gitignore\";\n        rev = \"a20de23b925fd8264fd7fad6454652e142fd7f73\";\n        sha256 = \"sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=\";\n      };\n    in\n    (super.callPackage gitignoreSrc { }).gitignoreSource;\n}\n"
  },
  {
    "path": "nix/overlays/haskell-packages.nix",
    "content": "{ compiler }:\n\nself: super:\nlet\n  inherit (self.haskell) lib;\n\n  overrides =\n    _: prev:\n    rec {\n      # To pin custom versions of Haskell packages:\n      #   protolude =\n      #     prev.callHackageDirect\n      #       {\n      #         pkg = \"protolude\";\n      #         ver = \"0.3.0\";\n      #         sha256 = \"<sha256>\";\n      #       }\n      #       { };\n      #\n      # To temporarily pin unreleased versions from GitHub:\n      #   <name> =\n      #     prev.callCabal2nixWithOptions \"<name>\" (super.fetchFromGitHub {\n      #       owner = \"<owner>\";\n      #       repo  = \"<repo>\";\n      #       rev = \"<commit>\";\n      #       sha256 = \"<sha256>\";\n      #    }) \"--subpath=<subpath>\" {};\n      #\n      # To fill in the sha256:\n      #   update-nix-fetchgit nix/overlays/haskell-packages.nix\n      #\n      # - Nowadays you can just delete the sha256 attribute above and nix will assume a fake sha.\n      # Once you build the derivation it will suggest the correct sha.\n      # - If the library fails its test suite (usually when it runs IO tests), wrap the expression with `lib.dontCheck ()`\n      # - <subpath> is usually \".\"\n      # - When adding a new library version here, postgrest.cabal and stack.yaml must also be updated\n      #\n      # Notes:\n      # - When adding a new package version here, update cabal.\n      #   + Update postgrest.cabal with the package version\n      #   + Update the index-state in cabal.project.freeze. Run `cabal update` which should return the latest index state.\n      # - When adding a new package version here, you have to update stack.\n      #   + To update stack.yaml add:\n      #   extra-deps:\n      #     - <package>-<ver>\n      #   + For stack.yaml.lock, CI should report an error with the correct lock, copy/paste that one into the file\n      # - To modify and try packages locally, see \"Working with locally modified Haskell packages\" in the Nix README.\n\n      # Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329\n      # jailbreak, because hspec limit for tests\n      fuzzyset = prev.fuzzyset_0_2_4;\n\n      # TODO: Remove once available in nixpkgs haskellPackages\n      configurator-pg =\n        prev.callHackageDirect\n          {\n            pkg = \"configurator-pg\";\n            ver = \"0.2.11\";\n            sha256 = \"sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=\";\n          }\n          { };\n\n      # TODO: Remove once available in nixpkgs haskellPackages\n      streaming-commons =\n        prev.callHackageDirect\n          {\n            pkg = \"streaming-commons\";\n            ver = \"0.2.3.1\";\n            sha256 = \"sha256-Gl2eaJcWe1sxmcE/octWlH9uSnERguf+5H66K4fV87s=\";\n          }\n          { };\n\n      # Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.\n      hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);\n      hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;\n      hasql-implicits = lib.dontCheck prev.hasql-implicits_0_1_1_3;\n      hasql-notifications = lib.dontCheck prev.hasql-notifications_0_2_2_2;\n      hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;\n      hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;\n      postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);\n    };\nin\n{\n  haskell =\n    super.haskell // {\n      packages = super.haskell.packages // {\n        \"${compiler}\" =\n          super.haskell.packages.\"${compiler}\".override { inherit overrides; };\n      };\n    };\n}\n"
  },
  {
    "path": "nix/static.nix",
    "content": "{ compiler\n, name\n, pkgs\n, src\n}:\nlet\n  # This builds a static PostgREST executable based on pkgsStatic.\n  inherit (pkgs) pkgsStatic;\n  inherit (pkgsStatic.haskell) lib;\n\n  packagesStatic = pkgsStatic.haskell.packages.native-bignum.\"${compiler}\";\n\n  makeExecutableStatic = drv: pkgs.lib.pipe drv [\n    lib.compose.justStaticExecutables\n\n    # To successfully compile a redistributable, fully static executable we need to:\n    # 1. avoid any references to /nix/store to prevent blowing up the closure size.\n    (drv: drv.overrideAttrs {\n      allowedReferences = [\n        pkgsStatic.openssl.etc\n      ];\n    })\n\n    # 2. be able to run the executable.\n    (drv: drv.overrideAttrs {\n      passthru.tests.version = pkgsStatic.testers.testVersion {\n        package = drv;\n      };\n    })\n  ];\n\nin\n{\n  inherit packagesStatic;\n\n  postgrestStatic = makeExecutableStatic (packagesStatic.callCabal2nix name src { });\n}\n"
  },
  {
    "path": "nix/tools/cabalTools.nix",
    "content": "{ buildToolbox\n, cabal-install\n, checkedShellScript\n, devCabalOptions\n, postgrest\n}:\nlet\n  build =\n    checkedShellScript\n      {\n        name = \"postgrest-build\";\n        docs = \"Build PostgREST interactively using cabal-install.\";\n        args = [ \"ARG_LEFTOVERS([Cabal arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} \"''${_arg_leftovers[@]}\"\n      '';\n\n  clean =\n    checkedShellScript\n      {\n        name = \"postgrest-clean\";\n        docs = \"Clean the PostgREST project, including all cabal-install artifacts.\";\n        workingDir = \"/\";\n      }\n      ''\n        # clean old coverage data, too\n        rm -rf .hpc coverage\n        # clean old hie files\n        find . -name \"*.hie\" -type f -delete\n        exec ${cabal-install}/bin/cabal v2-clean\n      '';\n\n  update =\n    checkedShellScript\n      {\n        name = \"postgrest-cabal-update\";\n        docs = \"Update cabal's package list from hackage.haskell.org\";\n        workingDir = \"/\";\n      }\n      ''\n        exec ${cabal-install}/bin/cabal v2-update\n      '';\n\n  run =\n    checkedShellScript\n      {\n        name = \"postgrest-run\";\n        docs = \"Run PostgREST after building it interactively with cabal-install\";\n        args =\n          [\n            \"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])\"\n            \"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])\"\n            \"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])\"\n            \"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])\"\n            \"ARG_USE_ENV([PGRST_ADMIN_SERVER_PORT], [3001], [PostgREST admin server port])\"\n            \"ARG_LEFTOVERS([PostgREST arguments])\"\n          ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        export PGRST_DB_ANON_ROLE\n        export PGRST_DB_POOL\n        export PGRST_DB_POOL_ACQUISITION_TIMEOUT\n        export PGRST_JWT_SECRET\n        export PGRST_ADMIN_SERVER_PORT\n\n        exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \\\n          postgrest \"''${_arg_leftovers[@]}\"\n      '';\n\n\n  runProfiled =\n    checkedShellScript\n      {\n        name = \"postgrest-profiled-run\";\n        docs = \"Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization.\";\n        args =\n          [\n            \"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])\"\n            \"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])\"\n            \"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])\"\n            \"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])\"\n            \"ARG_LEFTOVERS([PostgREST arguments])\"\n          ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        export PGRST_DB_ANON_ROLE\n        export PGRST_DB_POOL\n        export PGRST_DB_POOL_ACQUISITION_TIMEOUT\n        export PGRST_JWT_SECRET\n\n        exec ${cabal-install}/bin/cabal --builddir=\"dist-prof\" v2-run --enable-profiling --disable-shared exe:postgrest -- \\\n          +RTS -p -h -RTS \"''${_arg_leftovers[@]}\"\n      '';\n\n  repl =\n    checkedShellScript\n      {\n        name = \"postgrest-repl\";\n        docs = \"Interact with PostgREST modules using the cabal repl\";\n        args = [ \"ARG_LEFTOVERS([cabal v2-repl arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        exec ${cabal-install}/bin/cabal v2-repl \"''${_arg_leftovers[@]}\"\n      '';\nin\nbuildToolbox\n{\n  name = \"postgrest-cabal\";\n  tools = {\n    inherit\n      build\n      clean\n      update\n      run\n      runProfiled\n      repl;\n  };\n}\n"
  },
  {
    "path": "nix/tools/devTools.nix",
    "content": "{ buildToolbox\n, cabal-install\n, cachix\n, checkedShellScript\n, curl\n, devCabalOptions\n, entr\n, git\n, graphviz\n, hsie\n, nix\n, silver-searcher\n, stdenv\n, style\n, tests\n, withTools\n, haskellPackages\n, ctags\n, openssl\n}:\nlet\n  watch =\n    checkedShellScript\n      {\n        name = \"postgrest-watch\";\n        docs =\n          ''\n            Watch the project for changes and reinvoke the given command.\n\n            Example:\n              postgrest-watch postgrest-test-io\n          '';\n        args =\n          [\n            \"ARG_POSITIONAL_SINGLE([command], [Command to run])\"\n            \"ARG_LEFTOVERS([command arguments])\"\n          ];\n        positionalCompletion = \"_command\";\n        redirectTixFiles = false; # will be done by sub-command\n        workingDir = \"/\";\n      }\n      ''\n        while true; do\n          (! ${silver-searcher}/bin/ag -l . | ${entr}/bin/entr -dr \"$_arg_command\" \"''${_arg_leftovers[@]}\")\n        done\n      '';\n\n  pushCachix =\n    checkedShellScript\n      {\n        name = \"postgrest-push-cachix\";\n        docs = ''\n          Push all build artifacts to cachix.\n\n          Requires authentication with `cachix authtoken ...`.\n        '';\n        args =\n          [\n            \"ARG_OPTIONAL_SINGLE([system], , [System], [${stdenv.system}])\"\n          ];\n        workingDir = \"/\";\n      }\n      ''\n        ${nix}/bin/nix-instantiate --argstr system \"$_arg_system\" \\\n          | xargs ${nix}/bin/nix-store -qR --include-outputs \\\n          | ${cachix}/bin/cachix push postgrest\n      '';\n\n  check =\n    checkedShellScript\n      {\n        name = \"postgrest-check\";\n        docs =\n          ''\n            Run most checks that will also run on CI, but only against the\n            latest PostgreSQL version.\n\n            This currently excludes the memory and spec-idempotence tests,\n            as those are particularly expensive.\n          '';\n        workingDir = \"/\";\n      }\n      ''\n        ${tests}/bin/postgrest-test-spec\n        ${tests}/bin/postgrest-test-observability\n        ${tests}/bin/postgrest-test-doctests\n        ${tests}/bin/postgrest-test-io\n        ${tests}/bin/postgrest-test-big-schema\n        ${tests}/bin/postgrest-test-replica\n        ${style}/bin/postgrest-lint\n        ${style}/bin/postgrest-style-check\n      '';\n\n  gitHooks =\n    let\n      name = \"postgrest-git-hooks\";\n    in\n    checkedShellScript\n      {\n        inherit name;\n        docs =\n          ''\n            Enable or disable git pre-commit and pre-push hooks.\n\n            Basic is faster and will only run:\n              - pre-commit: postgrest-style\n              - pre-push: postgrest-lint\n\n            Full takes a lot more time and will run:\n              - pre-commit: postgrest-style && postgrest-lint\n              - pre-push: postgrest-check\n\n            Changes made by postgrest-style will be staged automatically.\n\n            Example usage:\n              postgrest-git-hooks disable\n              postgrest-git-hooks enable basic\n              postgrest-git-hooks enable full\n\n            The \"run\" operation and \"--hook\" argument are only used internally.\n          '';\n        args =\n          [\n            \"ARG_POSITIONAL_SINGLE([operation], [Operation])\"\n            \"ARG_TYPE_GROUP_SET([OPERATION], [OPERATION], [operation], [disable,enable,run])\"\n            \"ARG_POSITIONAL_SINGLE([mode], [Mode], [basic])\"\n            \"ARG_TYPE_GROUP_SET([MODE], [MODE], [mode], [basic,full])\"\n            \"ARG_OPTIONAL_SINGLE([hook], , [Hook], [pre-commit])\"\n            \"ARG_TYPE_GROUP_SET([HOOK], [HOOK], [hook], [pre-commit,pre-push])\"\n          ];\n        positionalCompletion =\n          ''\n            if test \"$prev\" == \"${name}\"; then\n              COMPREPLY=( $(compgen -W \"enable disable\" -- \"$cur\") )\n            elif test \"$prev\" == \"enable\" || test \"$prev\" == \"disable\"; then\n              COMPREPLY=( $(compgen -W \"basic full\" -- \"$cur\") )\n            fi\n          '';\n        workingDir = \"/\";\n      }\n      ''\n        if [ run != \"$_arg_operation\" ]; then\n          # Remove all hooks first and ignore failures because the file might be missing.\n          # This assumes that we're only adding lines that include \"postgrest-git-hooks\"\n          # to the hook file.\n          sed -i -e '/postgrest-git-hooks/d' .git/hooks/pre-{commit,push} 2> /dev/null || true\n\n          if [ disable != \"$_arg_operation\" ]; then\n            # The nix-shell && + nix-shell || pattern makes sure we can run the hook\n            # in a pure nix-shell, where nix-shell itself is not available, too.\n\n            # The $(nix-shell --run \"command -v ...\") pattern ensures we only need to enable\n            # the hooks once and still run the latest of our hook scripts, even when we\n            # update them in the repo.\n\n            echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-commit run' \"$_arg_mode\" \\\n              >> .git/hooks/pre-commit\n            # shellcheck disable=SC2016\n            echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run \"command -v postgrest-git-hooks\") --hook=pre-commit run' \"$_arg_mode\" \\\n             >> .git/hooks/pre-commit\n            chmod +x .git/hooks/pre-commit\n\n            echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-push run' \"$_arg_mode\" \\\n              >> .git/hooks/pre-push\n            # shellcheck disable=SC2016\n            echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run \"command -v postgrest-git-hooks\") --hook=pre-push run' \"$_arg_mode\" \\\n             >> .git/hooks/pre-push\n            chmod +x .git/hooks/pre-push\n          fi\n        else\n          # When run from a git hook, the GIT_ environment variables conflict with our withGit helper.\n          # The following unsets all GIT_ variables.\n          unset \"''${!GIT_@}\"\n\n          # shellcheck disable=SC2317\n          function restore () {\n            ref=\"$(git stash list --format=format:%gD --grep \"$1\" -n1)\"\n            # this will avoid merge conflicts when applying the stash\n            ${git}/bin/git restore --source=\"$ref\" .\n            # restore untracked files, too. could fail with no files\n            if [ \"$(git show --numstat --format=oneline \"$ref^3\" | wc -l)\" -gt 1 ]; then\n              ${git}/bin/git restore --overlay --source=\"$ref^3\" .\n            fi\n            ${git}/bin/git stash drop \"$ref\"\n          }\n\n          case \"$_arg_mode\" in\n            basic)\n              case \"$_arg_hook\" in\n                pre-commit)\n                  # To be able to automatically add only changes from postgrest-style to the staging area,\n                  # we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping\n                  # the stash afterwards.\n                  ${style}/bin/postgrest-style\n\n                  stash=\"postgrest-git-hooks-$RANDOM\"\n                  ${git}/bin/git stash push --include-untracked --keep-index -m \"$stash\"\n                  if [ \"$(git stash list --grep $stash)\" ]; then\n                    # Only create the stash pop trap, if we actually created a stash.\n                    # Otherwise stash pop will cause havoc.\n                    trap 'restore \"$stash\"' EXIT\n                  fi\n\n                  ${style}/bin/postgrest-style\n                  ${git}/bin/git add .\n                  ;;\n                pre-push)\n                  # Create a clean working tree without any uncommitted changes.\n                  ${withTools.withGit} HEAD ${style}/bin/postgrest-lint\n                  ;;\n              esac\n              ;;\n            full)\n              case \"$_arg_hook\" in\n                pre-commit)\n                  # To be able to automatically add only changes from postgrest-style to the staging area,\n                  # we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping\n                  # the stash afterwards.\n                  ${style}/bin/postgrest-style\n\n                  stash=\"postgrest-git-hooks-$RANDOM\"\n                  ${git}/bin/git stash push --include-untracked --keep-index -m \"$stash\"\n                  if [ \"$(git stash list --grep $stash)\" ]; then\n                    # Only create the stash pop trap, if we actually created a stash.\n                    # Otherwise stash pop will cause havoc.\n                    trap 'restore \"$stash\"' EXIT\n                  fi\n\n                  ${style}/bin/postgrest-style\n                  ${git}/bin/git add .\n\n                  ${style}/bin/postgrest-lint\n                  ;;\n                pre-push)\n                  # Create a clean working tree without any uncommitted changes.\n                  ${withTools.withGit} HEAD ${check}\n                  ;;\n              esac\n              ;;\n          esac\n        fi\n      '';\n\n  dumpMinimalImports =\n    checkedShellScript\n      {\n        name = \"postgrest-dump-minimal-imports\";\n        docs = \"Dump minimal imports into given directory.\";\n        args = [ \"ARG_POSITIONAL_SINGLE([dumpdir], [Output directory])\" ];\n        workingDir = \"/\";\n        withTmpDir = true;\n      }\n      ''\n        mkdir -p \"$_arg_dumpdir\"\n        ${cabal-install}/bin/cabal v2-build ${devCabalOptions} \\\n          --builddir=\"$tmpdir\" \\\n          --ghc-option=-ddump-minimal-imports \\\n          --ghc-option=-dumpdir=\"$_arg_dumpdir\" \\\n          1>&2\n\n        # Fix OverloadedRecordFields imports\n        # shellcheck disable=SC2016\n        sed -E 's/\\$sel:.*://g' -i \"$_arg_dumpdir\"/*\n      '';\n\n  hsieMinimalImports =\n    checkedShellScript\n      {\n        name = \"postgrest-hsie-minimal-imports\";\n        docs = \"Run hsie with a provided dump of minimal imports.\";\n        args = [ \"ARG_LEFTOVERS([hsie arguments])\" ];\n        withTmpDir = true;\n      }\n      ''\n        ${dumpMinimalImports} \"$tmpdir\"\n        ${hsie} \"$tmpdir\" \"''${_arg_leftovers[@]}\"\n      '';\n\n  hsieGraphModules =\n    checkedShellScript\n      {\n        name = \"postgrest-hsie-graph-modules\";\n        docs = \"Create a PNG graph of modules imported within the codebase.\";\n        args = [ \"ARG_POSITIONAL_SINGLE([outfile], [Output filename])\" ];\n      }\n      ''\n        ${hsie} graph-modules main src | ${graphviz}/bin/dot -Tpng -o \"$_arg_outfile\"\n      '';\n\n  hsieGraphSymbols =\n    checkedShellScript\n      {\n        name = \"postgrest-hsie-graph-symbols\";\n        docs = \"Create a PNG graph of symbols imported within the codebase.\";\n        args = [ \"ARG_POSITIONAL_SINGLE([outfile], [Output filename])\" ];\n      }\n      ''\n        ${hsieMinimalImports} graph-symbols | ${graphviz}/bin/dot -Tpng -o \"$_arg_outfile\"\n      '';\n\n  parallelCurl =\n    checkedShellScript\n      {\n        name = \"postgrest-parallel-curl\";\n        docs = \"wrapper for using <num> parallel curl requests on the same <host>\";\n        args = [\n          \"ARG_POSITIONAL_SINGLE([num], [number of parallel requests])\"\n          \"ARG_POSITIONAL_SINGLE([host], [host])\"\n          \"ARG_LEFTOVERS([extra arguments for curl])\"\n        ];\n      }\n      ''\n        curl_command=\"${curl}/bin/curl --parallel --parallel-immediate \"\n        curl_command+=\"''${_arg_leftovers[*]} \"\n\n        x=1\n        while [ $x -le \"$1\" ]\n        do\n          curl_command+=\"$_arg_host \"\n          x=$((x + 1))\n        done\n\n        eval \"$curl_command\"\n      '';\n\n  genCtags =\n    checkedShellScript\n      {\n        name = \"postgrest-gen-ctags\";\n        docs = \"Generate ctags for Haskell and Python code\";\n        workingDir = \"/\";\n      }\n      ''\n        ${haskellPackages.haskdogs}/bin/haskdogs\n        ${ctags}/bin/ctags -a -R --fields=+l --languages=python --python-kinds=-iv -f ./tags test/io/\n      '';\n\n  genJwt =\n    checkedShellScript\n      {\n        name = \"postgrest-gen-jwt\";\n        docs = ''\n          Generate a JWT. Example: postgrest-gen-jwt --exp 10 postgrest_test_author\n\n          # This can be used to quickly prove a JWT expiry\n          $ curl localhost:3000/authors_only -H \"Authorization: Bearer \\$(postgrest-gen-jwt --exp -31 postgrest_test_author)\"\n        '';\n        args = [\n          \"ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])\"\n          \"ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])\"\n          \"ARG_OPTIONAL_SINGLE([exp],, [seconds for JWT expiry, it accepts negative values], [3600])\"\n        ];\n      }\n      ''\n        # Based on https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands\n\n        # Construct the header\n        jwt_header=$(echo -n '{\"alg\":\"HS256\",\"typ\":\"JWT\"}' | base64 | sed s/\\+/-/g | sed 's/\\//_/g' | sed -E s/=+$//)\n\n        # Construct the exp value\n        expiry=$((EPOCHSECONDS + _arg_exp))\n\n        # Construct the payload\n        payload=$(echo -n \"{\\\"role\\\": \\\"$_arg_role\\\", \\\"exp\\\": $expiry}\" | base64 | sed s/\\+/-/g |sed 's/\\//_/g' |  sed -E s/=+$//)\n\n        # Convert secret to hex\n        hexsecret=$(echo -n \"$_arg_secret\" | xxd -p | paste -sd \"\")\n\n        # Calculate hmac signature -- note option to pass in the key as hex bytes\n        hmac_signature=$(echo -n \"$jwt_header.$payload\" |  ${openssl}/bin/openssl dgst -sha256 -mac HMAC -macopt hexkey:\"$hexsecret\" -binary \\\n          | base64  | sed s/\\+/-/g | sed 's/\\//_/g' | sed -E s/=+$//)\n\n        # Create the full token\n        jwt=\"$jwt_header.$payload.$hmac_signature\"\n\n        echo -n \"$jwt\"\n      '';\n\n  genSecret =\n    checkedShellScript\n      {\n        name = \"postgrest-gen-secret\";\n        docs = \"Generate a JWT secret\";\n      }\n      ''\n        export LC_CTYPE=C\n\n        LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c32\n      '';\nin\nbuildToolbox\n{\n  name = \"postgrest-dev\";\n  tools = {\n    inherit\n      check\n      dumpMinimalImports\n      gitHooks\n      hsieGraphModules\n      hsieGraphSymbols\n      hsieMinimalImports\n      parallelCurl\n      genCtags\n      genJwt\n      genSecret\n      pushCachix\n      watch;\n  };\n}\n"
  },
  {
    "path": "nix/tools/docker/README.md",
    "content": "# Docker image built with Nix\n\nIn order to build an optimal PostgREST Docker image, we create the image from\nscratch (i.e., without a parent image like `debian` or `alpine`), and only\ninclude the file that is essential for running PostgREST: the static\nPostgREST binary.\n\nThis is similar to what you would get with the following `Dockerfile`:\n\n```Dockerfile\n# `scratch` is a minimal, reserved image in Docker, see\n# https://docs.docker.com/develop/develop-images/baseimages/ . It essentially\n# means \"don't use a parent image and start with an empty one\".\nFROM scratch\n\n# The static PostgREST executable has no runtime dependencies, so it's all we\n# need to include for running the application.\nADD /absolute/path/to/postgrest /bin/postgrest\n\nEXPOSE 3000\n\n# This is the user id that Docker will run our image under by default. Note\n# that we don't actually add the user to `/etc/passwd` or `/etc/shadow`. This\n# means that tools like whoami would not work properly, but we don't include\n# those in the image anyway. Not adding the user has the benefit that the image\n# can be run under any user you specify.\nUSER 1000\n\nCMD [ \"/bin/postgrest\" ]\n```\n\n# Building the Docker image with Nix\n\nAs we are building the static PostgREST executable with Nix and that's the main\ninput to the Docker file, we can also create the Docker image directly with Nix\nusing the [`dockerTools`\nutilities](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools). Those\nutilities don't actually use `Dockerfiles` or Docker to build Docker images,\nbut create them directly by putting together the required `json` and `tar`\nfiles that make up an image. This is more efficient, does not rely on Docker or\nroot permissions and results in fully reproducible builds. See\n[`nix/docker/default.nix`](./default.nix) for details how the image is built.\n\n# Building and loading the image\n\nThe Nix expression provides a helper script `postgrest-docker-load` that loads\nthe optimized image into your local Docker instance (using `docker load -i\n<image file>` under the hood). You can use it by running:\n\n```\n# Running from the root directory of the repository:\n\n# Build the `docker` attribute from `default.nix`, the result will be symlinked\n# to `result`:\nnix-build -A docker\n\n# Run the loading script:\nresult/bin/postgrest-docker-load\n```\n\nThe Docker image built with Nix always has the name \"postgrest:latest\" when\nloaded.\n\n# Inspecting the optimized image\n\nThe image does not come with the usual utilities like `bash` and `ls`.\n\nYou can, however, explore the `tar` file of the image by saving it with `docker\nsave postgrest:latest > image.tar`.\n\n[Dive](https://github.com/wagoodman/dive) is also useful for looking at the\ncontents of the image:\n\n```\n┃ ● Layers ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ Current Layer Contents ├────────────────────────────────────────────────────────────────────────────────\nCmp   Size  Command                                                                                        Permission     UID:GID       Size  Filetree\n     14 MB  FROM 20ee65c811575d2                                                                           dr-xr-xr-x         0:0      14 MB  ├── bin\n                                                                                                           -r-xr-xr-x         0:0      14 MB  │   └── postgrest\n│ Layer Details ├───────────────────────────────────────────────────────────────────────────────────────── drwxr-xr-x         0:0      783 B  ├── etc\n                                                                                                           -r--r--r--         0:0      783 B  │   └── postgrest.conf\nTags:   (unavailable)                                                                                      dr-xr-xr-x         0:0      23 kB  └── nix\nId:     20ee65c811575d206eb673e1887e7f7e6b7ccde902a63ccb924c5faa50b32cee                                   dr-xr-xr-x         0:0      23 kB      └── store\nDigest: sha256:ece77302b83fd38fb54395dabc10c2eba06fc1d1933801d36cc2c4732d9c8f38                            dr-xr-xr-x         0:0      23 kB          └── s440jbrn94wmpzy7f8yfsp6jr2shllw5-openssl-1.1.1g-etc\nCommand:                                                                                                   dr-xr-xr-x         0:0      23 kB              └── etc\n                                                                                                           dr-xr-xr-x         0:0      23 kB                  └── ssl\n                                                                                                           -r--r--r--         0:0      412 B                      ├── ct_log_list.cnf\n│ Image Details ├───────────────────────────────────────────────────────────────────────────────────────── -r--r--r--         0:0      412 B                      ├── ct_log_list.cnf.dist\n                                                                                                           dr-xr-xr-x         0:0        0 B                      ├── engines-1.1\n                                                                                                           -r--r--r--         0:0      11 kB                      ├── openssl.cnf\nTotal Image size: 14 MB                                                                                    -r--r--r--         0:0      11 kB                      └── openssl.cnf.dist\nPotential wasted space: 0 B\nImage efficiency score: 100 %\n\nCount   Total Space  Path\n```\n\n# Deriving from the optimized image\n\nSince the docker image is minimal, it does not contain a shell or other utilities.\nTo derive a non-minimal image, you can do the following:\n\n```Dockerfile\n# derive from any base image you want\nFROM alpine:latest\n\n# copy PostgREST over\nCOPY --from=postgrest/postgrest /bin/postgrest /bin\n\n# add your other stuff\n```\n"
  },
  {
    "path": "nix/tools/docker/default.nix",
    "content": "{ buildToolbox\n, postgrest\n, dockerTools\n, checkedShellScript\n}:\nlet\n  image =\n    dockerTools.buildImage {\n      name = \"postgrest\";\n      tag = \"latest\";\n      copyToRoot = postgrest;\n\n      # Set the current time as the image creation date. This makes the build\n      # non-reproducible, but that should not be an issue for us.\n      created = \"now\";\n\n      extraCommands =\n        ''\n          rmdir share\n        '';\n\n      config = {\n        Cmd = [ \"/bin/postgrest\" ];\n        User = \"1000\";\n        ExposedPorts = {\n          \"3000/tcp\" = { };\n        };\n      };\n    };\n\n  load =\n    checkedShellScript\n      {\n        name = \"postgrest-docker-load\";\n        docs = \"Load the PostgREST image into Docker.\";\n      }\n      ''\n        docker load -i ${image}\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-docker\";\n  tools = { inherit load; };\n  extra = { inherit image; };\n}\n"
  },
  {
    "path": "nix/tools/docs.nix",
    "content": "{ aspell\n, aspellDicts\n, buildToolbox\n, checkedShellScript\n, lib\n, plantuml\n, python3\n, python3Packages\n, writeTextFile\n, writers\n}:\nlet\n  selectPythonPackages = ps: [\n    ps.sphinx\n    ps.sphinx-copybutton\n    ps.sphinx-rtd-dark-mode\n    ps.sphinx-rtd-theme\n    ps.sphinx-tabs\n    ps.sphinxext-opengraph\n  ];\n\n  requirements = writeTextFile {\n    name = \"requirements.txt\";\n    text = lib.concatMapStringsSep \"\\n\" (pkg: \"${pkg.pname}==${pkg.version}\") (selectPythonPackages python3Packages);\n  };\n\n  python = python3.withPackages selectPythonPackages;\n\n  build =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-build\";\n        docs = \"Build the documentation.\";\n        args = [ \"ARG_POSITIONAL_SINGLE([language], [Language to build docs for.], [\\\"\\\"])\" ];\n        workingDir = \"/docs\";\n      }\n      ''\n        # https://github.com/sphinx-doc/sphinx/issues/11739\n        export LC_ALL=C\n\n        function build() {\n          ${python}/bin/sphinx-build --color -W -a -n . -b \"$@\"\n        }\n\n        if [ \"$_arg_language\" == \"\" ]; then\n          # clean previous build, otherwise some errors might be suppressed\n          rm -rf \"../.docs-build/html/default\"\n\n          if [ -d languages ]; then\n            # default to updating all existing locales\n            build gettext ../.docs-build/gettext\n            ${python}/bin/sphinx-intl update -p ../.docs-build/gettext\n          fi\n\n          build html \"../.docs-build/html/default\"\n        else\n          # clean previous build, otherwise some errors might be suppressed\n          rm -rf \"../.docs-build/html/$_arg_language\"\n\n          # update and build specific locale, can be used to create new locale\n          build gettext ../.docs-build/gettext\n          ${python}/bin/sphinx-intl update -p ../.docs-build/gettext -l \"$_arg_language\"\n\n          build html \"../.docs-build/html/$_arg_language\" -D \"language=$_arg_language\"\n        fi\n      '';\n\n  render =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-render\";\n        docs = \"Render the diagrams.\";\n        workingDir = \"/docs/_diagrams\";\n      }\n      ''\n        ${plantuml}/bin/plantuml -tsvg uml/*.uml -o ../../_static\n        ${plantuml}/bin/plantuml -tsvg -darkmode uml/dark/*.uml -o ../../../_static\n      '';\n\n  server =\n    writers.writePython3\n      \"postgrest-docs-server\"\n      { libraries = selectPythonPackages python3Packages ++ [ python3Packages.livereload ]; }\n      ''\n        import sys\n        from livereload import Server, shell\n        from subprocess import call\n\n        build = sys.argv[1]\n        locale = sys.argv[2]\n\n        if locale == \"\":\n            locale = \"default\"\n        else:\n            build += \" \" + locale\n\n        call(build, shell=True)\n\n        server = Server()\n        server.watch(\"**/*.rst\", shell(build))\n        server.watch(f\"locales/{locale}/LC_MESSAGES/*.po\", shell(build))\n        server.serve(root=f\"../.docs-build/html/{locale}\")\n      '';\n\n  serve =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-serve\";\n        docs = \"Serve the documentation locally with live reload.\";\n        args = [ \"ARG_POSITIONAL_SINGLE([language], [Language to serve docs for.], [\\\"\\\"])\" ];\n        workingDir = \"/docs\";\n      }\n      ''\n        ${server} ${build} \"$_arg_language\"\n      '';\n\n  spellcheck =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-spellcheck\";\n        docs = \"Verify spelling mistakes. Bypass if the word is present in postgrest.dict.\";\n        workingDir = \"/docs\";\n      }\n      ''\n        export LC_ALL=C\n\n        FILES=$(find . -type f -iname '*.rst' | tr '\\n' ' ')\n\n        # shellcheck disable=SC2086 disable=SC2016\n        cat $FILES \\\n         | grep -v '^\\(\\.\\.\\|  \\)' \\\n         | sed -E 's/`+[^`]+`+//g' \\\n         | ${aspell}/bin/aspell -d ${aspellDicts.en}/lib/aspell/en_US -p ./postgrest.dict list \\\n         | sort -f \\\n         | tee misspellings\n        test ! -s misspellings\n      '';\n\n  dictcheck =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-dictcheck\";\n        docs = \"Detect obsolete entries in postgrest.dict that are not used anymore.\";\n        workingDir = \"/docs\";\n      }\n      ''\n        export LC_ALL=C\n\n        FILES=$(find . -type f -iname '*.rst' | tr '\\n' ' ')\n\n        tail -n+2 postgrest.dict \\\n         | tr '\\n' '\\0' \\\n         | xargs -0 -i \\\n           sh -c \"grep \\\"{}\\\" $FILES > /dev/null || echo \\\"{}\\\"\" \\\n         | tee unuseddict\n        test ! -s unuseddict\n      '';\n\n  linkcheck =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-linkcheck\";\n        docs = \"Verify that external links are working correctly.\";\n        workingDir = \"/docs\";\n      }\n      ''\n        export LC_ALL=C\n\n        ${python}/bin/sphinx-build --color -b linkcheck . ../.docs-build\n      '';\n\n  check =\n    checkedShellScript\n      {\n        name = \"postgrest-docs-check\";\n        docs = \"Build and run all the validation scripts.\";\n        workingDir = \"/docs\";\n      }\n      ''\n        ${build}\n        ${dictcheck}\n        ${spellcheck}\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-docs\";\n  tools = {\n    inherit\n      build\n      check\n      dictcheck\n      linkcheck\n      render\n      serve\n      spellcheck;\n  };\n  extra = { inherit requirements; };\n}\n"
  },
  {
    "path": "nix/tools/gen_rsa_materials.py",
    "content": "# Generate RSA JWK/public material for loadtests.\n\nimport argparse\nimport sys\nfrom pathlib import Path\n\nimport jwcrypto.jwk as jwk\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Generate RSA JWK/private key pair for loadtests\"\n    )\n    parser.add_argument(\n        \"--rsa\",\n        dest=\"jwk_path\",\n        metavar=\"JWK_PATH\",\n        type=Path,\n        required=True,\n        help=\"Path to write the RSA JWK file\",\n    )\n    parser.add_argument(\n        \"--private-key\",\n        dest=\"private_key_path\",\n        metavar=\"PRIVATE_KEY_PATH\",\n        type=Path,\n        required=True,\n        help=\"Path to write the RSA private key file\",\n    )\n\n    args = parser.parse_args()\n\n    key = jwk.JWK.generate(kty=\"RSA\", size=4096)\n    private_jwk, public_jwk = key.export_private(), key.export_public()\n\n    try:\n        args.jwk_path.write_text(public_jwk)\n        print(f\"Created RSA JWK on {args.jwk_path}\")\n    except OSError as e:\n        print(f\"Error writing to {args.jwk_path}:{e}\", file=sys.stderr)\n        sys.exit(1)\n\n    try:\n        args.private_key_path.write_text(private_jwk)\n        print(f\"Created private key on {args.private_key_path}\")\n    except OSError as e:\n        print(f\"Error writing to {args.private_key_path}:{e}\", file=sys.stderr)\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "nix/tools/generate_targets.py",
    "content": "# generates a file to be used by the vegeta load testing tool\n\n# It includes a worst case scenario for the JWT cache:\n# - all requests will have a unique JWT so no cache hits\n# - all jwts have an expiration that will be long enough to be\n#   valid at time of request but short enough that already\n#   validated jwts will expire later during the loadtest run\n# - the above guarantees JWT cache purging will happen\n# - we want this to track resource consumption in the worst case\n\n# And a more normal scenario where non-expiring JWTs are picked\n# from an array\nimport time\nimport argparse\nimport subprocess\nimport sys\nimport random\nimport jwt\nfrom typing import Optional\nfrom pathlib import Path\nfrom enum import Enum\n\nURL = \"http://postgrest\"\n\nsecret_key = b\"reallyreallyreallyreallyverysafe\"\n\n\ndef generate_jwt(\n    now: int,\n    exp_inc: Optional[int],\n    rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],\n) -> str:\n    \"\"\"Generate an HS256 or RS256 JWT\"\"\"\n    payload = {\n        \"sub\": f\"user_{random.getrandbits(32)}\",\n        \"iat\": now,\n        \"role\": \"postgrest_test_author\",\n    }\n\n    if exp_inc is not None:\n        payload[\"exp\"] = now + exp_inc\n\n    if rsa_private_key is None:\n        key = secret_key\n        alg = \"HS256\"\n    else:\n        key = rsa_private_key\n        alg = \"RS256\"\n    return jwt.encode(payload, key, alg)\n\n\nHTTP_METHODS = (\n    \"GET\",\n    \"OPTIONS\",\n)\n\nHttpMethod = Enum(\n    \"HttpMethod\",\n    {method: method for method in HTTP_METHODS},\n    type=str,\n    module=__name__,\n)\n\n\ndef append_targets(lines: list[str], token: str, http_method: HttpMethod):\n    lines.append(f\"{http_method.value} {URL}/authors_only\")\n    lines.append(f\"Authorization: Bearer {token}\")\n    lines.append(\"\")  # blank line to separate requests\n\n\n# we use this to chain commands on loadtest.nix\ndef run_command(command: list[str]):\n    if not command:\n        return\n\n    if command[0] == \"--\":\n        command = command[1:]\n\n    if not command:\n        return\n\n    try:\n        subprocess.run(command, check=True)\n    except subprocess.CalledProcessError as exc:\n        print(\n            f\"Error executing command {' '.join(command)}: {exc}\",\n            file=sys.stderr,\n        )\n        sys.exit(exc.returncode)\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Generate Vegeta targets with unique JWTs\"\n    )\n    parser.add_argument(\n        \"targets_path\",\n        metavar=\"TARGETS_PATH\",\n        help=\"Path to write the generated targets file\",\n    )\n    parser.add_argument(\n        \"--private-key\",\n        dest=\"private_key_path\",\n        metavar=\"PRIVATE_KEY_PATH\",\n        type=Path,\n        default=None,\n        help=\"Path to the RSA private key file (required when --rsa is used)\",\n    )\n    parser.add_argument(\n        \"--worst\",\n        dest=\"worst\",\n        action=argparse.BooleanOptionalAction,\n        default=False,\n        help=\"Generate worst case targets for a JWT cache\",\n    )\n    parser.add_argument(\n        \"--rsa\",\n        dest=\"jwk_path\",\n        metavar=\"JWK_PATH\",\n        type=Path,\n        default=None,\n        help=\"Path to an existing RSA JWK file used for signing tokens\",\n    )\n    parser.add_argument(\n        \"--method\",\n        dest=\"http_method\",\n        choices=list(HTTP_METHODS),\n        required=True,\n        help=\"HTTP method for the vegeta targets\",\n    )\n    parser.add_argument(\n        \"command\",\n        nargs=argparse.REMAINDER,\n        help=\"Command (and arguments) to run after generating the targets\",\n    )\n\n    args = parser.parse_args()\n\n    rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None\n\n    is_hs = args.jwk_path is None\n\n    http_method = HttpMethod(args.http_method)\n\n    nsamples = 1000\n\n    if is_hs:\n        ntargets = 200000\n    else:\n        # The asymmetric targets take too long to compute so we reduce them\n        ntargets = 50000\n\n    if not is_hs:\n        if args.private_key_path is None:\n            parser.error(\"--rsa requires the --private-key option\")\n        try:\n            private_key_data = args.private_key_path.read_text()\n        except OSError as e:\n            err = (\n                f\"Error reading RSA private key from {args.private_key_path}: \"\n                f\"{e}. Generate RSA materials first with gen_rsa_materials.py.\"\n            )\n            print(err, file=sys.stderr)\n            sys.exit(1)\n\n        try:\n            rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)\n        except Exception as exc:  # broad exception to capture parsing errors\n            err = (\n                f\"Error loading RSA private key from {args.private_key_path}: \" f\"{exc}\"\n            )\n            print(err, file=sys.stderr)\n            sys.exit(1)\n\n    print(f\"Generating {ntargets} targets...\")\n\n    start_time = time.time()\n\n    now = int(start_time)\n\n    lines = []\n\n    # We want to ensure 401 Unauthorized responses don't happen during\n    # JWT validation, this can happen when the jwt `exp` is too short.\n    # At the same time, we want to ensure the `exp` is not too big,\n    # so expires will occur and postgREST needs to\n    # clean cached expired JWTs\n    if args.worst:\n        # estimated time takes to build and run postgrest itself\n        build_run_postgrest_time = 2\n\n        # estimated time it takes to generate the targets file\n        # the division numbers are tuned by hand\n        if is_hs:  # hs generation is much faster\n            gen_time = ntargets // 66666\n        else:  # asymmetric is slower so the time is higher\n            gen_time = ntargets // 220\n\n        # estimated exp time so some JWTs will expire\n        inc = build_run_postgrest_time + gen_time\n\n        for i in range(ntargets):\n            token = generate_jwt(now, inc + i // 1000, rsa_private_key)\n            append_targets(lines, token, http_method)\n\n    else:\n        tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]\n        for i in range(ntargets):\n            token = random.choice(tokens)\n            append_targets(lines, token, http_method)\n\n    try:\n        with open(args.targets_path, \"w\") as f:\n            f.write(\"\\n\".join(lines))\n    except IOError as e:\n        print(f\"Error writing to {args.targets_path}: {e}\", file=sys.stderr)\n        sys.exit(1)\n\n    elapsed = time.time() - start_time\n    print(f\"Created {ntargets} targets\", end=\" \")\n    print(f\"in {args.targets_path} ({elapsed:.2f}s)\")\n\n    run_command(args.command)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "nix/tools/gitTools.nix",
    "content": "{ buildToolbox\n, checkedShellScript\n, commitlint\n, writeText\n}:\nlet\n  # Rules format: [<severity>, <\"always\"/\"never\">, <value>]\n  commitlintConfig = writeText \"commitlint.config.mjs\" ''\n    export default {\n      rules: {\n        \"type-enum\": [2, \"always\", [\n            'add',      // Add a new feature\n            'amend',    // To amend an unrealease commit\n            'change',   // Breaking changes\n            'chore',    // Update sponsors, changelog, readme etc\n            'ci',       // CI configuration files and scripts\n            'docs',     // Documentation\n            'fix',      // Bug fix\n            'nix',      // Related to Nix\n            'perf',     // Performance improvements\n            'refactor', // Refactoring code\n            'remove',   // Remove a feature or fix\n            'test',     // Adding tests\n          ]],\n\n          'subject-case':       [2, 'never', ['pascal-case', 'start-case']],\n          'subject-empty':      [2, 'never'],\n          'subject-full-stop':  [2, 'never', '.'],\n          'subject-max-length': [2, 'always', 80],\n          'subject-min-length': [2, 'always', 5],\n\n          'scope-case':         [2, 'always', 'lower-case'],\n\n          'body-leading-blank': [2, 'always'],\n      },\n    };\n  '';\n\n  commitCheck =\n    checkedShellScript\n      {\n        name = \"postgrest-commitlint\";\n        docs = \"Script to validate commit messages\";\n        workingDir = \"/\";\n        args = [\n          \"ARG_OPTIONAL_SINGLE([from],, [commit ref start from], [main])\"\n          \"ARG_OPTIONAL_SINGLE([to],, [commit ref end at], [HEAD])\"\n        ];\n      }\n      ''\n        # Run commitlint with the given configuration\n\n        ${commitlint}/bin/commitlint --config ${commitlintConfig} --from \"$_arg_from\" --to \"$_arg_to\"\n      '';\nin\nbuildToolbox\n{\n  name = \"postgrest-commitlint\";\n  tools = { inherit commitCheck; };\n}\n"
  },
  {
    "path": "nix/tools/loadtest.nix",
    "content": "{ buildToolbox\n, checkedShellScript\n, jq\n, python3Packages\n, vegeta\n, withTools\n, writers\n}:\nlet\n  runner =\n    checkedShellScript\n      {\n        name = \"postgrest-loadtest-runner\";\n        docs = \"Run vegeta. Assume PostgREST to be running.\";\n        args = [\n          \"ARG_LEFTOVERS([additional vegeta arguments])\"\n          \"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to connect to running PostgREST instance])\"\n        ];\n      }\n      ''\n        echo \"Starting vegeta loadtest...\"\n\n        # ARG_USE_ENV only adds defaults or docs for environment variables\n        # We manually implement a required check here\n        # See also: https://github.com/matejak/argbash/issues/80\n        : \"''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}\"\n\n        ${vegeta}/bin/vegeta -cpus 1 attack \\\n                                     -dns-ttl -1 \\\n                                     -unix-socket \"$PGRST_SERVER_UNIX_SOCKET\" \\\n                                     -max-workers 1 \\\n                                     -workers 1 \\\n                                     -rate 0 \\\n                                     -duration 60s \\\n                                     \"''${_arg_leftovers[@]}\"\n      '';\n\n  loadtest =\n    checkedShellScript\n      {\n        name = \"postgrest-loadtest\";\n        docs = \"Run the vegeta loadtests with PostgREST.\";\n        args = [\n          \"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])\"\n          \"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])\"\n          \"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])\"\n          \"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])\"\n          \"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])\"\n          \"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])\"\n          \"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])\"\n          \"ARG_LEFTOVERS([additional vegeta arguments])\"\n        ];\n        workingDir = \"/\";\n      }\n      ''\n        # previously required settings to make this work with older branches\n        export PGRST_DB_ANON_ROLE=\"postgrest_test_anonymous\"\n        export PGRST_DB_URI=\"postgresql://\"\n        export PGRST_DB_SCHEMAS=\"test\"\n\n        export PGRST_DB_CONFIG=\"false\"\n        export PGRST_DB_POOL=\"1\"\n        export PGRST_DB_TX_END=\"rollback-allow-override\"\n        export PGRST_LOG_LEVEL=\"crit\"\n        export PGRST_JWT_SECRET=\"reallyreallyreallyreallyverysafe\"\n\n        mkdir -p \"$(dirname \"$_arg_output\")\"\n        abs_output=\"$(realpath \"$_arg_output\")\"\n\n        case \"$_arg_kind\" in\n          jwt-hs)\n            export PGRST_JWT_CACHE_MAX_ENTRIES=\"0\"\n\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          jwt-hs-cache)\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          jwt-hs-cache-worst)\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" --worst \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          jwt-rsa)\n            export PGRST_JWT_CACHE_MAX_ENTRIES=\"0\"\n\n            ${genRsaMaterials} --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json\n            export PGRST_JWT_SECRET=\"@$_arg_testdir/gen_jwk.json\"\n\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          jwt-rsa-cache)\n            ${genRsaMaterials} --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json\n            export PGRST_JWT_SECRET=\"@$_arg_testdir/gen_jwk.json\"\n\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          jwt-rsa-cache-worst)\n            export PGRST_JWT_SECRET=\"@$_arg_testdir/gen_jwk.json\"\n\n            ${genRsaMaterials} --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json\n            export PGRST_JWT_SECRET=\"@$_arg_testdir/gen_jwk.json\"\n\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            ${withGenTargets} --method \"$_arg_method\" --worst --rsa=\"$_arg_testdir\"/gen_jwk.json --private-key=\"$_arg_testdir\"/gen_private.json \"$_arg_testdir\"/gen_targets.http \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -lazy -targets gen_targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          mixed)\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/fixtures.sql \\\n            ${withTools.withPgrst} -m \"$_arg_monitor\" \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -targets targets.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n\n          # here we sleep purposefully to check how much memory does the schema cache consume in the final report\n          errors)\n            # shellcheck disable=SC2145\n            ${withTools.withPg} -f \"$_arg_testdir\"/errors.sql \\\n            ${withTools.withPgrst} --timeout 2 --sleep 5 -m \"$_arg_monitor\" \\\n            sh -c \"cd \\\"$_arg_testdir\\\" && \\\n            ${runner} -targets errors.http -output \\\"$abs_output\\\" \\\"''${_arg_leftovers[@]}\\\"\"\n            ;;\n        esac\n\n        ${vegeta}/bin/vegeta report -type=text \"$_arg_output\"\n\n        if [ \"$_arg_kind\" != \"errors\" ]; then\n          # fail in case 401 happened on jwt loadtests\n          unauthorized_count=\"$(${vegeta}/bin/vegeta report -type=json \"$_arg_output\" \\\n            | ${jq}/bin/jq -r '.status_codes[\"401\"] // 0')\"\n\n          if [ \"$unauthorized_count\" -gt 0 ]; then\n            last_unauthorized_body=\"$(${vegeta}/bin/vegeta encode \"$_arg_output\" \\\n              | ${jq}/bin/jq -rn '\n                  reduce inputs as $item (null;\n                    if $item.code == 401 then $item else . end\n                  )\n                  | if . == null then\n                      empty\n                    else\n                      (.body | @base64d)\n                    end\n                ')\"\n\n            echo \"loadtest failed: found $unauthorized_count 401 Unauthorized responses\" >&2\n            if [ -n \"$last_unauthorized_body\" ]; then\n              printf '%s\\n' \"Last 401 response body:\" >&2\n              printf '%s\\n' \"$last_unauthorized_body\" >&2\n            fi\n\n            exit 1\n          fi\n        fi\n      '';\n\n  loadtestAgainst =\n    let\n      name = \"postgrest-loadtest-against\";\n    in\n    checkedShellScript\n      {\n        inherit name;\n        docs =\n          ''\n            Run the vegeta loadtest against every target branch and HEAD:\n            - once on the every <target-#> branch\n            - once in the current worktree\n          '';\n        args = [\n          \"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)\"\n          \"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])\"\n        ];\n        positionalCompletion =\n          ''\n            if test \"$prev\" == \"${name}\"; then\n              __gitcomp_nl \"$(__git_refs)\"\n            fi\n          '';\n        workingDir = \"/\";\n      }\n      ''\n        # run loadtest for every target\n        for tgt in \"''${_arg_target[@]}\"; do\n\n        cat << EOF\n\n        Running \"$_arg_kind\" loadtest on \"$tgt\"...\n\n        EOF\n\n        # Runs the test files from the current working tree\n        # to make sure both tests are run with the same files.\n        # Save the results in the current working tree, too,\n        # otherwise they'd be lost in the temporary working tree\n        # created by withTools.withGit.\n        ${withTools.withGit} \"$tgt\" ${loadtest} -k \"$_arg_kind\" -m \"$PWD/loadtest/$tgt.csv\" --output \"$PWD/loadtest/$tgt.bin\" --testdir \"$PWD/test/load\"\n\n        cat << EOF\n\n        Done running on \"$tgt\".\n\n        EOF\n\n        done\n\n        # run loadtest once on HEAD\n\n        cat << EOF\n\n        Running \"$_arg_kind\" loadtest on HEAD...\n\n        EOF\n\n        ${loadtest} -k \"$_arg_kind\" -m \"$PWD/loadtest/head.csv\" --output \"$PWD/loadtest/head.bin\" --testdir \"$PWD/test/load\"\n\n        cat << EOF\n\n        Done running on HEAD.\n\n        EOF\n      '';\n\n  reporter =\n    checkedShellScript\n      {\n        name = \"postgrest-loadtest-reporter\";\n        docs = \"Create a named json report for a single result file.\";\n        args = [\n          \"ARG_POSITIONAL_SINGLE([file], [Filename of result to create report for])\"\n          \"ARG_LEFTOVERS([additional vegeta arguments])\"\n        ];\n        workingDir = \"/\";\n      }\n      ''\n        ${vegeta}/bin/vegeta report -type=json \"$_arg_file\" \\\n          | ${jq}/bin/jq --arg branch \"$(basename \"$_arg_file\" .bin)\" '. + {branch: $branch}'\n      '';\n\n  toMarkdown =\n    writers.writePython3 \"postgrest-loadtest-to-markdown\"\n      {\n        libraries = [ python3Packages.pandas python3Packages.tabulate ];\n      }\n      ''\n        import sys\n        import pandas as pd\n\n        pd.read_json(sys.stdin) \\\n          .set_index('param') \\\n          .drop(['branch', 'earliest', 'end', 'latest']) \\\n          .fillna(\"\") \\\n          .convert_dtypes() \\\n          .to_markdown(sys.stdout, floatfmt='.0f')\n      '';\n\n\n  report =\n    checkedShellScript\n      {\n        name = \"postgrest-loadtest-report\";\n        docs = \"Create a report of all loadtest reports as markdown.\";\n        args = [\n          \"ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])\"\n        ];\n        workingDir = \"/\";\n      }\n      ''\n        marker=''${_arg_group:+\"($_arg_group)\"}\n\n        echo -e \"## Loadtest results $marker\\n\"\n\n        find loadtest -type f -iname '*.bin' -exec ${reporter} {} \\; \\\n          | ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join(\".\"), (.branch): getpath($path)}]' \\\n          | ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \\\n          | ${toMarkdown}\n\n        echo -e \"\\n\\n## Loadtest elapsed seconds vs CPU/MEM usage $marker\\n\"\n\n        find loadtest -type f -iname '*.csv' \\\n          | sort -m \\\n          | ${mergeMonitorResults}\n      '';\n\n  withGenTargets =\n    writers.writePython3 \"postgrest-with-gen-loadtest-targets\"\n      {\n        libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];\n        doCheck = false; # postgrest-style conflicts with this\n      }\n      (builtins.readFile ./generate_targets.py);\n\n  genRsaMaterials =\n    writers.writePython3 \"postgrest-gen-rsa-materials\"\n      {\n        libraries = [ python3Packages.jwcrypto ];\n        doCheck = false; # postgrest-style conflicts with this\n      }\n      (builtins.readFile ./gen_rsa_materials.py);\n\n  mergeMonitorResults =\n    writers.writePython3 \"postgrest-merge-monitor-results\"\n      {\n        libraries = [ python3Packages.pandas python3Packages.tabulate ];\n      }\n      (builtins.readFile ./merge_monitor_result.py);\nin\nbuildToolbox {\n  name = \"postgrest-loadtest\";\n  tools = { inherit loadtest loadtestAgainst report; };\n}\n"
  },
  {
    "path": "nix/tools/merge_monitor_result.py",
    "content": "import os\nimport sys\nimport pandas as pd\n\nKEY = \"Elapsed seconds\"\nBASE_METRICS = [\"CPU (%)\", \"Real (MB)\"]\nbranch_order = []\nmerged = None\n\npaths = [p.strip() for p in sys.stdin.read().split() if p.strip()]\n\nfor csv_path in paths:\n    # br is branch (variable shortened to pass linter)\n    br = os.path.splitext(os.path.basename(csv_path))[0]\n    branch_order.append(br)\n\n    df = pd.read_csv(csv_path)\n\n    if KEY not in df.columns:\n        sys.exit(f\"{csv_path} is missing the {KEY} column\")\n\n    for m in BASE_METRICS:\n        if m not in df.columns:\n            sys.exit(f\"Error: '{csv_path}' missing required column '{m}'.\")\n\n    # add branch marker to every metric column\n    df = df.rename(columns={c: f\"{c} [{br}]\" for c in df.columns if c != KEY})\n\n    # outer join so missing rows appear\n    merged = df if merged is None else merged.merge(df, on=KEY, how=\"outer\")\n\n# Re-order columns so related metrics are adjacent\nordered_cols = [KEY]\nfor metric in BASE_METRICS:\n    for br in branch_order:\n        col_name = f\"{metric} [{br}]\"\n        if col_name in merged.columns:\n            ordered_cols.append(col_name)\n\nmerged = merged[ordered_cols]\n\n# replace nan with empty string\nmerged = merged.fillna(\"\")\nmerged.to_markdown(sys.stdout, index=False, tablefmt=\"github\")\n"
  },
  {
    "path": "nix/tools/monitor_pid.py",
    "content": "# Monitor a process pid with psutil and emits a CSV.\nimport sys\nimport time\nimport psutil\nimport pandas as pd\n\nKEY = \"Elapsed seconds\"\nBASE_METRICS = [\"CPU (%)\", \"Real (MB)\"]\nSAMPLE_INTERVAL_SECS = 1\n\nif len(sys.argv) != 2 or not sys.argv[1].isdigit():\n    sys.exit(f\"Usage: {sys.argv[0]} <PID>\")\n\npid = int(sys.argv[1])\ntry:\n    proc = psutil.Process(pid)\nexcept psutil.NoSuchProcess:\n    sys.exit(f\"Error: process {pid} not found.\")\n\nprint(f\"Starting monitoring of {pid} pid\", file=sys.stderr)\n\nrecords = []\nstart = time.time()\n# ignore first result as per docs recommendation\n# https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent\nproc.cpu_percent(None)\n\nwhile True:\n    try:\n        if not proc.is_running():\n            break\n        time.sleep(SAMPLE_INTERVAL_SECS)\n\n        elapsed_secs = int(time.time() - start)\n        cpu = proc.cpu_percent(None)\n        meminfo = proc.memory_info()\n        bytes_in_MB = 1024**2\n        rss_mb = meminfo.rss / bytes_in_MB\n\n        records.append(\n            [\n                str(elapsed_secs),\n                f\"{cpu:.3f}\",\n                f\"{rss_mb:.3f}\",\n            ]\n        )\n\n    except psutil.NoSuchProcess:\n        break\n\nend = time.time()\ntotal_time = end - start\nprint(f\"Finished {pid} pid monitoring in {total_time:.3f}\", file=sys.stderr)\n\ncols = [KEY] + BASE_METRICS\ndf = pd.DataFrame(records, columns=cols, dtype=str)\ndf.to_csv(sys.stdout, index=False)\n"
  },
  {
    "path": "nix/tools/nixpkgsTools.nix",
    "content": "{ buildToolbox\n, checkedShellScript\n}:\n# Utility script for pinning the latest stable version of Nixpkgs.\n\n# Instead of running `nix flake update` manually, we run this script\n# to also pin readthedocs dependencies at the same time.\nlet\n  upgrade =\n    checkedShellScript\n      {\n        name = \"postgrest-nixpkgs-upgrade\";\n        docs = \"Pin the newest version of Nixpkgs.\";\n        workingDir = \"/\";\n      }\n      ''\n        nix flake update\n\n        echo \"# This file is auto-generated by postgrest-nixpkgs-upgrade\" > docs/requirements.txt\n        cat \"$(nix-build -A docs.requirements)\" >> docs/requirements.txt\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-nixpkgs\";\n  tools = { inherit upgrade; };\n}\n"
  },
  {
    "path": "nix/tools/release.nix",
    "content": "{ buildToolbox\n, checkedShellScript\n}:\nlet\n  release =\n    checkedShellScript\n      {\n        name = \"postgrest-release\";\n        docs = \"Patch postgrest.cabal, CHANGELOG.md, commit and push all in one go.\";\n        args = [ \"ARG_OPTIONAL_BOOLEAN([major], [m], [Bump to new major version (only applies on main branch).])\" ];\n        workingDir = \"/\";\n      }\n      ''\n        current_branch=\"$(git rev-parse --abbrev-ref HEAD)\"\n        trap \"echo You need to be on the main branch or a release branch to proceed. Exiting ...\" ERR\n        [[ \"$current_branch\" =~ ^main$|^v[0-9]+$ ]]\n        trap \"\" ERR\n\n        trap \"echo You have uncommitted changes in postgrest.cabal. Exiting ...\" ERR\n        git diff --exit-code HEAD postgrest.cabal > /dev/null\n        trap \"\" ERR\n\n        # TODO: Support C+D bumps when implementing hackage releases\n        bump () {\n          current_version=\"$(grep -oP '^version:\\s*\\K.*' postgrest.cabal)\"\n          # shellcheck disable=SC2034\n          IFS=. read -r A B C D <<< \"$current_version\"\n          echo \"Current version is $current_version\"\n\n          case \"$1\" in\n            A)\n              new_version=\"$((A+1)).0\"\n              new_docs_version=\"$((A+1))\"\n              ;;\n            B)\n              new_version=\"$A.$((B+1))\"\n              new_docs_version=\"$A\"\n              ;;\n            devel)\n              new_version=\"$((A+1))\"\n              new_docs_version=\"devel\"\n              ;;\n          esac\n\n          echo \"Updating postgrest.cabal ...\"\n          sed -i -E \"s/^(version:\\s+).*$/\\1$new_version/\" postgrest.cabal > /dev/null\n          echo \"Updating docs/conf.py ...\"\n          sed -i -E \"s/^(version = ).*$/\\1\\\"$new_docs_version\\\"/\" docs/conf.py > /dev/null\n\n          git add postgrest.cabal docs/conf.py > /dev/null\n        }\n\n        today_date_for_changelog=\"$(date '+%Y-%m-%d')\"\n        if [[ \"$current_branch\" == \"main\" ]]; then\n          bump A\n        else\n          bump B\n        fi\n\n        echo \"Updating CHANGELOG.md ...\"\n        sed -i -E \"s/Unreleased/&\\n\\n## [$new_version] - $today_date_for_changelog/\" CHANGELOG.md > /dev/null\n        git add CHANGELOG.md > /dev/null\n\n        echo \"Committing ...\"\n        git commit -m \"bump version to $new_version\" > /dev/null\n\n        if [[ \"$current_branch\" == \"main\" ]]; then\n          bump devel\n\n          # The order of operations is important here:\n          # - bump devel is run and $A is updated to the new version\n          # - the branch is created with the new A, but the commit before the devel bump\n          # - the devel bump is committed\n          git branch \"v$A\"\n\n          echo \"Committing (devel bump)...\"\n          git commit -m \"bump version to $new_version\" > /dev/null\n        fi\n\n        trap \"echo Remote not found. Please push manually ...\" ERR\n        remote=\"$(git remote -v | grep 'PostgREST/postgrest' | grep push | cut -f1)\"\n        trap \"\" ERR\n\n        if [[ \"$current_branch\" == \"main\" ]]; then\n          push1=\"git push $remote $current_branch\"\n          push2=\"git push $remote v$A\"\n        else\n          push1=\"git push $remote $current_branch\"\n          push2=\"\"\n        fi\n\n        echo \"To push the version bump(s), the following will be run:\"\n        echo\n        echo \"$push1\"\n        echo \"$push2\"\n        echo\n\n        read -r -p 'Proceed? (y/N) ' REPLY\n        case \"$REPLY\" in\n          y|Y)\n            $push1\n            $push2\n            ;;\n          *)\n            echo \"Aborting ...\"\n            ;;\n        esac\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-release\";\n  tools = { inherit release; };\n}\n"
  },
  {
    "path": "nix/tools/style.nix",
    "content": "{ actionlint\n, black\n, buildToolbox\n, checkedShellScript\n, deadnix\n, git\n, hlint\n, hsie\n, nixpkgs-fmt\n, python3Packages\n, ruff\n, silver-searcher\n, statix\n, stylish-haskell\n, writeText\n}:\nlet\n  style =\n    checkedShellScript\n      {\n        name = \"postgrest-style\";\n        docs = \"Automatically format Haskell, Nix and Python files.\";\n        workingDir = \"/\";\n      }\n      ''\n        # Format Nix files\n        ${statix}/bin/statix fix\n        ${nixpkgs-fmt}/bin/nixpkgs-fmt . > /dev/null 2> /dev/null\n\n        # Format Haskell files\n        # --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753\n        ${silver-searcher}/bin/ag -l --vimgrep -g '\\.l?hs$' . \\\n          | xargs ${stylish-haskell}/bin/stylish-haskell -i\n\n        # Format Python files\n        ${black}/bin/black . 2> /dev/null\n      '';\n\n  # Script to check whether any uncommitted changes result from postgrest-style\n  styleCheck =\n    checkedShellScript\n      {\n        name = \"postgrest-style-check\";\n        docs = \"Check whether postgrest-style results in any uncommitted changes.\";\n        workingDir = \"/\";\n      }\n      ''\n        ${style}\n\n        trap \"echo postgrest-style-check failed. Run postgrest-style to fix issues automatically.\" ERR\n\n        ${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py'\n      '';\n\n  hlintConfig = writeText \"hlintConfig.yml\" ''\n\n    # Arguments passed to hlint\n    - arguments: [-j, -XQuasiQuotes, -XNoPatternSynonyms]\n\n    # Warnings\n    - warn: { lhs: \"a == a\", rhs: \"True\",  note: \"This comparison always evaluates to True\" }\n    - warn: { lhs: \"a /= a\", rhs: \"False\", note: \"This comparison always evaluates to False\" }\n    - warn: { lhs: \"a < a\",  rhs: \"False\", note: \"This comparison always evaluates to False\" }\n    - warn: { lhs: \"a > a\",  rhs: \"False\", note: \"This comparison always evaluates to False\" }\n    - warn: { lhs: \"a <= a\", rhs: \"True\",  note: \"This comparison always evaluates to True\" }\n    - warn: { lhs: \"a >= a\", rhs: \"True\",  note: \"This comparison always evaluates to True\" }\n  '';\n\n  lint =\n    checkedShellScript\n      {\n        name = \"postgrest-lint\";\n        docs = \"Lint all Haskell files, bash scripts and github workflows.\";\n        workingDir = \"/\";\n      }\n      ''\n        echo \"Linting workflows...\"\n        ${actionlint}/bin/actionlint\n\n        echo \"Scanning nix files for unused code...\"\n        ${deadnix}/bin/deadnix -f\n\n        # ruff has gaps in scanning for unused code, so we use vulture\n        echo \"Scanning python files for unused code...\"\n        ${silver-searcher}/bin/ag -l --vimgrep -g '\\.l?py$' . \\\n          | xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py\n\n        echo \"Linting python files...\"\n        ${ruff}/bin/ruff check .\n\n        echo \"Checking consistency of import aliases in Haskell code...\"\n        ${hsie} check-aliases main src\n\n        echo \"Linting Haskell files...\"\n        # --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753\n        ${silver-searcher}/bin/ag -l --vimgrep -g '\\.l?hs$' . \\\n          | xargs ${hlint}/bin/hlint --hint=${hlintConfig}\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-style\";\n  tools = { inherit style styleCheck lint; };\n}\n"
  },
  {
    "path": "nix/tools/tests.nix",
    "content": "{ buildToolbox\n, cabal-install\n, checkedShellScript\n, curl\n, devCabalOptions\n, ghc\n, glibcLocales ? null\n, gnugrep\n, hpc-codecov\n, hostPlatform\n, jq\n, lib\n, postgrest\n, python3\n, runtimeShell\n, stdenv\n, weeder\n, withTools\n}:\nlet\n  testSpec =\n    checkedShellScript\n      {\n        name = \"postgrest-test-spec\";\n        docs = \"Run the Haskell test suite. Use --match PATTERN for running individual specs\";\n        args = [ \"ARG_LEFTOVERS([hspec arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${withTools.withPg} -f test/spec/fixtures/load.sql \\\n          ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- \"''${_arg_leftovers[@]}\"\n      '';\n\n  testObservability =\n    checkedShellScript\n      {\n        name = \"postgrest-test-observability\";\n        docs = \"Run the Haskell observability test suite.\";\n        args = [ \"ARG_LEFTOVERS([hspec arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${withTools.withPg} -f test/observability/fixtures/load.sql \\\n          ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- \"''${_arg_leftovers[@]}\"\n      '';\n\n  testDoctests =\n    checkedShellScript\n      {\n        name = \"postgrest-test-doctests\";\n        docs = \"Run the Haskell doctest test suite\";\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        # This makes nix-env -iA tests.doctests.bin work.\n        export NIX_GHC=${postgrest.env.NIX_GHC}\n        ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests\n      '';\n\n  testSpecIdempotence =\n    checkedShellScript\n      {\n        name = \"postgrest-test-spec-idempotence\";\n        docs = \"Check that the Haskell tests can be run multiple times against the same db.\";\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${withTools.withPg} -f test/spec/fixtures/load.sql \\\n          ${runtimeShell} -c \" \\\n            ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \\\n            ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec\"\n      '';\n\n  ioTestPython =\n    python3.withPackages (ps: [\n      ps.pyjwt\n      ps.pytest\n      ps.pytest-xdist\n      ps.pyyaml\n      ps.requests\n      ps.requests-unixsocket\n      ps.syrupy\n    ]);\n\n  testIO =\n    checkedShellScript\n      {\n        name = \"postgrest-test-io\";\n        docs = \"Run the pytest-based IO tests. Add -k to run tests that match a given expression.\";\n        args = [ \"ARG_LEFTOVERS([pytest arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest\n        ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/load.sql \\\n          ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io \"''${_arg_leftovers[@]}\"\n      '';\n\n  testBigSchema =\n    checkedShellScript\n      {\n        name = \"postgrest-test-big-schema\";\n        docs = \"Run a pytest-based IO test on a big schema. Add -k to run tests that match a given expression.\";\n        args = [ \"ARG_LEFTOVERS([pytest arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest\n        ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/big_schema.sql \\\n          ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py \"''${_arg_leftovers[@]}\"\n      '';\n\n  testReplica =\n    checkedShellScript\n      {\n        name = \"postgrest-test-replica\";\n        docs = \"Run a pytest-based IO test on a replica. Add -k to run tests that match a given expression.\";\n        args = [ \"ARG_LEFTOVERS([pytest arguments])\" ];\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n      }\n      ''\n        ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest\n        ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/fixtures/replica.sql \\\n          ${ioTestPython}/bin/pytest -v test/io/test_replica.py \"''${_arg_leftovers[@]}\"\n      '';\n\n  dumpSchema =\n    checkedShellScript\n      {\n        name = \"postgrest-dump-schema\";\n        docs = \"Dump the loaded schema's SchemaCache in JSON format.\";\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n        withPath = [ jq ];\n      }\n      ''\n        ${withTools.withPg} -f test/spec/fixtures/load.sql \\\n            ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \\\n            postgrest --dump-schema\n      '';\n\n  coverage =\n    checkedShellScript\n      {\n        name = \"postgrest-coverage\";\n        docs = \"Run spec and io tests while collecting hpc coverage data. First runs weeder to detect dead code.\";\n        args = [ \"ARG_LEFTOVERS([hpc report arguments])\" ];\n        workingDir = \"/\";\n        redirectTixFiles = false;\n        withEnv = postgrest.env;\n        withTmpDir = true;\n      }\n      (\n        # required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin\n        lib.optionalString (stdenv.isLinux && hostPlatform.libc == \"glibc\") ''\n          export LOCALE_ARCHIVE=\"${glibcLocales}/lib/locale/locale-archive\"\n        '' +\n\n        ''\n          # clean up previous coverage reports\n          mkdir -p coverage\n          rm -rf coverage/*\n\n          # build once before running all the tests\n          ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:observability\n\n          (\n            trap 'echo Found dead code: Check file list above.' ERR ;\n            ${weeder}/bin/weeder --config=./test/weeder.toml\n          )\n\n          # collect all tests\n          HPCTIXFILE=\"$tmpdir\"/io.tix \\\n            ${withTools.withPg} -f test/io/fixtures/load.sql \\\n            ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io\n\n          HPCTIXFILE=\"$tmpdir\"/big_schema.tix \\\n            ${withTools.withPg} -f test/io/fixtures/big_schema.sql \\\n            ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py\n\n          HPCTIXFILE=\"$tmpdir\"/replica.tix \\\n            ${withTools.withPg} --replica -f test/io/fixtures/replica.sql \\\n            ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py\n\n          HPCTIXFILE=\"$tmpdir\"/spec.tix \\\n            ${withTools.withPg} -f test/spec/fixtures/load.sql \\\n            ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec\n\n          HPCTIXFILE=\"$tmpdir\"/observability.tix \\\n            ${withTools.withPg} -f test/observability/fixtures/load.sql \\\n            ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability\n\n          # Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc\n\n          # collect all the tix files\n          ${ghc}/bin/hpc sum  --union --exclude=Paths_postgrest --output=\"$tmpdir\"/tests.tix \\\n            \"$tmpdir\"/io*.tix \"$tmpdir\"/big_schema*.tix \"$tmpdir\"/replica*.tix \"$tmpdir\"/spec.tix \\\n            \"$tmpdir\"/observability.tix\n\n          # prepare the overlay\n          ${ghc}/bin/hpc overlay --output=\"$tmpdir\"/overlay.tix test/coverage.overlay\n          ${ghc}/bin/hpc sum --union --output=\"$tmpdir\"/tests-overlay.tix \"$tmpdir\"/tests.tix \"$tmpdir\"/overlay.tix\n\n          # check nothing in the overlay is actually tested\n          ${ghc}/bin/hpc map --function=inv --output=\"$tmpdir\"/inverted.tix \"$tmpdir\"/tests.tix\n          ${ghc}/bin/hpc combine --function=sub \\\n            --output=\"$tmpdir\"/check.tix \"$tmpdir\"/overlay.tix \"$tmpdir\"/inverted.tix\n          # returns zero exit code if any count=\"<non-zero>\" lines are found, i.e.\n          # something is covered by both the overlay and the tests\n          if ${ghc}/bin/hpc report --xml \"$tmpdir\"/check.tix | ${gnugrep}/bin/grep -qP 'count=\"[^0]'\n          then\n            ${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/overlay \"$tmpdir\"/overlay.tix || true\n            ${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/check \"$tmpdir\"/check.tix || true\n            echo \"ERROR: Something is covered by both the tests and the overlay:\"\n            echo \"postgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html\"\n            exit 1\n          else\n            # copy the result .tix file to the coverage/ dir to make it available to postgrest-coverage-draft-overlay, too\n            cp \"$tmpdir\"/tests-overlay.tix coverage/postgrest.tix\n            # prepare codecov json report\n            ${hpc-codecov}/bin/hpc-codecov --mix=.hpc --out=coverage/codecov.json coverage/postgrest.tix\n\n            # create html and stdout reports\n            ${ghc}/bin/hpc markup --destdir=coverage coverage/postgrest.tix\n            echo \"postgrest-coverage: To see the results, visit file://$(pwd)/coverage/hpc_index.html\"\n            ${ghc}/bin/hpc report coverage/postgrest.tix \"''${_arg_leftovers[@]}\"\n          fi\n        ''\n      );\n\n  coverageDraftOverlay =\n    checkedShellScript\n      {\n        name = \"postgrest-coverage-draft-overlay\";\n        docs = \"Create a draft overlay from current coverage report.\";\n        workingDir = \"/\";\n      }\n      ''\n        ${ghc}/bin/hpc draft --output=test/coverage.overlay coverage/postgrest.tix\n        sed -i 's|^module \\(.*\\):|module \\1/|g' test/coverage.overlay\n      '';\n\n  testMemory =\n    checkedShellScript\n      {\n        name = \"postgrest-test-memory\";\n        docs = \"Run the memory tests.\";\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n        withPath = [ curl ];\n      }\n      ''\n        ${cabal-install}/bin/cabal --builddir=\"dist-prof\" v2-build --enable-profiling --disable-shared exe:postgrest\n        ${cabal-install}/bin/cabal --builddir=\"dist-prof\" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \\\n          test/memory/memory-tests.sh\n      '';\n\nin\nbuildToolbox\n{\n  name = \"postgrest-tests\";\n  tools = {\n    inherit\n      testSpec\n      testObservability\n      testDoctests\n      testSpecIdempotence\n      testIO\n      testBigSchema\n      testReplica\n      dumpSchema\n      coverage\n      coverageDraftOverlay\n      testMemory;\n  };\n}\n"
  },
  {
    "path": "nix/tools/withTools.nix",
    "content": "{ buildToolbox\n, checkedShellScript\n, curl\n, git\n, lib\n, postgresqlVersions\n, postgrest\n, python3Packages\n, writeText\n, writers\n}:\nlet\n  withTmpDb =\n    { name, postgresql }:\n    let\n      commandName = \"postgrest-with-${name}\";\n    in\n    checkedShellScript\n      {\n        name = commandName;\n        docs = \"Run the given command in a temporary database with ${name}. If you wish to mutate the database, login with the postgres role.\";\n        args =\n          [\n            \"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])\"\n            \"ARG_POSITIONAL_SINGLE([command], [Command to run])\"\n            \"ARG_LEFTOVERS([command arguments])\"\n            \"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])\"\n            \"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])\"\n            \"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])\"\n            \"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])\"\n            \"ARG_USE_ENV([PGOPTIONS], [-c search_path=public,test], [PG options to use])\"\n            \"ARG_OPTIONAL_BOOLEAN([replica],, [Enable a replica for the database])\"\n          ];\n        positionalCompletion = \"_command\";\n        workingDir = \"/\";\n        redirectTixFiles = false;\n        withPath = [ postgresql ];\n        withTmpDir = true;\n      }\n      ''\n        setuplog=\"$tmpdir/setup.log\"\n\n        log () {\n          echo \"$1\" >> \"$setuplog\"\n        }\n\n        # Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation\n        # load fixtures. Otherwise postgrest-with-pg-xx postgrest-test-io would not be possible.\n        if ! test -v PGHOST; then\n\n          mkdir -p \"$tmpdir\"/{db,socket}\n          # remove data dir, even if we keep tmpdir - no need to upload it to artifacts\n          trap 'rm -rf $tmpdir/db' EXIT\n\n          export PGDATA=\"$tmpdir/db\"\n          export PGHOST=\"$tmpdir/socket\"\n          export PGUSER\n          export PGDATABASE\n          export PGRST_DB_SCHEMAS\n          export PGTZ\n          export PGOPTIONS\n\n          HBA_FILE=\"$tmpdir/pg_hba.conf\"\n          echo \"local $PGDATABASE some_protected_user password\" > \"$HBA_FILE\"\n          echo \"local $PGDATABASE all trust\" >> \"$HBA_FILE\"\n          echo \"local replication all trust\" >> \"$HBA_FILE\"\n\n          log \"Initializing database cluster...\"\n          # We try to make the database cluster as independent as possible from the host\n          # by specifying the timezone, locale and encoding.\n          # initdb -U creates a superuser(man initdb)\n          TZ=$PGTZ initdb --no-locale --encoding=UTF8 --nosync -U postgres --auth=trust \\\n            >> \"$setuplog\"\n\n          log \"Starting the database cluster...\"\n\n          # Instead of listening on a local port, we will listen on a unix domain socket.\n          # NOTE: unix domain socket filename name must remain under max limit.\n          # On Linux, it's 108 chars (including '\\0' terminator)\n          # On MacOS, it's 104 chars\n          # See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket\n\n          pg_ctl -l \"$tmpdir/db.log\" -w start -o \"-F -c listen_addresses=\\\"\\\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\\\"all\\\" \" \\\n            >> \"$setuplog\"\n\n          log \"Creating a minimally privileged $PGUSER connection role...\"\n          createuser \"$PGUSER\" -U postgres --host=\"$tmpdir/socket\" --no-createdb --no-inherit --no-superuser --no-createrole --no-replication --login\n\n          >&2 echo \"${commandName}: You can connect with: psql 'postgres:///$PGDATABASE?host=$PGHOST' -U postgres\"\n          >&2 echo \"${commandName}: You can tail the logs with: tail -f $tmpdir/db.log\"\n\n          if test \"$_arg_replica\" = \"on\"; then\n            replica_slot=\"replica_$RANDOM\"\n            replica_dir=\"$tmpdir/$replica_slot\"\n            replica_host=\"$tmpdir/socket_$replica_slot\"\n\n            mkdir -p \"$replica_host\"\n\n            replica_dblog=\"$tmpdir/db_$replica_slot.log\"\n\n            log \"Running pg_basebackup for $replica_slot\"\n\n            pg_basebackup -v -h \"$PGHOST\" -U postgres --wal-method=stream --create-slot --slot=\"$replica_slot\" --write-recovery-conf -D \"$replica_dir\" \\\n              >> \"$setuplog\" 2>&1\n\n            log \"Starting replica on $replica_host\"\n\n            pg_ctl -D \"$replica_dir\" -l \"$replica_dblog\" -w start -o \"-F -c listen_addresses=\\\"\\\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\\\"all\\\" \" \\\n              >> \"$setuplog\"\n\n            >&2 echo \"${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres\"\n            >&2 echo \"${commandName}: You can tail the replica logs with: tail -f $replica_dblog\"\n\n            export PGREPLICAHOST=\"$replica_host\"\n            export PGREPLICASLOT=\"$replica_slot\"\n            export PGRST_DB_URI=\"postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST\"\n          fi\n\n          # shellcheck disable=SC2317\n          stop () {\n            log \"Stopping the database cluster...\"\n            pg_ctl stop --mode=immediate >> \"$setuplog\"\n            rm -rf \"$tmpdir/db\"\n            if test \"$_arg_replica\" = \"on\"; then\n              log \"Stopping the replica cluster...\"\n              pg_ctl -D \"$replica_dir\" stop --mode=immediate >> \"$setuplog\"\n              rm -rf \"$replica_dir\"\n            fi\n          }\n          trap stop EXIT\n        fi\n\n        if test \"$_arg_fixtures\"; then\n          load_start=$SECONDS\n          >&2 printf \"${commandName}: Loading fixtures under the postgres role...\"\n          psql -U postgres -v PGUSER=\"$PGUSER\" -v ON_ERROR_STOP=1 -f \"$_arg_fixtures\" >> \"$setuplog\"\n          load_end=$((SECONDS - load_start))\n          >&2 printf \" done in %ss. Running command...\\n\" \"$load_end\"\n        fi\n\n        (\"$_arg_command\" \"''${_arg_leftovers[@]}\")\n      '';\n\n  # Helper script for running a command against all PostgreSQL versions.\n  withPgAll =\n    let\n      runners =\n        builtins.map\n          (version:\n            ''\n              cat << EOF\n\n              Running against ${version.name}...\n\n              EOF\n\n              trap 'echo \"Failed on ${version.name}\"' exit\n\n              (${withTmpDb version} \"$_arg_command\" \"''${_arg_leftovers[@]}\")\n\n              trap \"\" exit\n\n              cat << EOF\n\n              Done running against ${version.name}.\n\n              EOF\n            '')\n          postgresqlVersions;\n    in\n    checkedShellScript\n      {\n        name = \"postgrest-with-all\";\n        docs = \"Run command against all supported PostgreSQL versions.\";\n        args =\n          [\n            \"ARG_POSITIONAL_SINGLE([command], [Command to run])\"\n            \"ARG_LEFTOVERS([command arguments])\"\n          ];\n        positionalCompletion = \"_command\";\n        workingDir = \"/\";\n      }\n      (lib.concatStringsSep \"\\n\\n\" runners);\n\n  withPg = withTmpDb (builtins.head postgresqlVersions);\n\n  withGit =\n    let\n      name = \"postgrest-with-git\";\n    in\n    checkedShellScript\n      {\n        inherit name;\n        docs =\n          ''\n            Create a new worktree of the postgrest repo in a temporary directory and\n            check out <commit>, then run <command> with arguments inside the temporary folder.\n          '';\n        args =\n          [\n            \"ARG_POSITIONAL_SINGLE([commit], [Commit-ish reference to run command with])\"\n            \"ARG_POSITIONAL_SINGLE([command], [Command to run])\"\n            \"ARG_LEFTOVERS([command arguments])\"\n          ];\n        positionalCompletion =\n          ''\n            if test \"$prev\" == \"${name}\"; then\n              __gitcomp_nl \"$(__git_refs)\"\n            else\n              _command_offset 2\n            fi\n          '';\n        workingDir = \"/\";\n      }\n      ''\n        # not using withTmpDir here, because we don't want to keep the directory on error\n        tmpdir=\"$(mktemp -d)\"\n        trap 'rm -rf \"$tmpdir\"' EXIT\n\n        ${git}/bin/git worktree add -f \"$tmpdir\" \"$_arg_commit\" > /dev/null\n\n        cd \"$tmpdir\"\n        (\"$_arg_command\" \"''${_arg_leftovers[@]}\")\n\n        ${git}/bin/git worktree remove -f \"$tmpdir\" > /dev/null\n      '';\n\n  legacyConfig =\n    writeText \"legacy.conf\"\n      ''\n        # Using this config file to support older postgrest versions for `postgrest-loadtest-against`\n        db-uri=\"$(PGRST_DB_URI)\"\n        db-schema=\"$(PGRST_DB_SCHEMAS)\"\n        db-anon-role=\"$(PGRST_DB_ANON_ROLE)\"\n        db-pool=\"$(PGRST_DB_POOL)\"\n        server-unix-socket=\"$(PGRST_SERVER_UNIX_SOCKET)\"\n        log-level=\"$(PGRST_LOG_LEVEL)\"\n      '';\n\n  waitForPgrstReady =\n    checkedShellScript\n      {\n        name = \"postgrest-wait-for-pgrst-ready\";\n        docs = \"Wait for PostgREST to be ready to serve requests. Needs to be a separate command for timeout to work below.\";\n        args = [\n          \"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to check for running PostgREST instance])\"\n        ];\n      }\n      ''\n        # ARG_USE_ENV only adds defaults or docs for environment variables\n        # We manually implement a required check here\n        # See also: https://github.com/matejak/argbash/issues/80\n        : \"''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}\"\n\n        function check_status () {\n          ${curl}/bin/curl -s -o /dev/null -w \"%{http_code}\" --unix-socket \"$PGRST_SERVER_UNIX_SOCKET\" http://localhost/\n        }\n\n        while [[ \"$(check_status)\" != \"200\" ]];\n           do sleep 0.1;\n        done\n      '';\n\n  # Broadcast SIGINT to any running postgrest instances on the host. Uses python for cross-platform compatibility.\n  signalPostgrest =\n    writers.writePython3 \"postgrest-signal-int\"\n      { libraries = [ python3Packages.psutil ]; }\n      ''\n        import psutil\n        import signal\n\n        for proc in psutil.process_iter([\"name\"]):\n            try:\n                if proc.info[\"name\"] == \"postgrest\":\n                    proc.send_signal(signal.SIGINT)\n            except (psutil.NoSuchProcess, psutil.AccessDenied):\n                continue\n      '';\n\n  withPgrst =\n    let\n      commandName = \"postgrest-with-pgrst\";\n    in\n    checkedShellScript\n      {\n        name = commandName;\n        docs = \"Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.\";\n        args =\n          [\n            \"ARG_POSITIONAL_SINGLE([command], [Command to run])\"\n            \"ARG_LEFTOVERS([command arguments])\"\n            \"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])\"\n            \"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])\"\n            \"ARG_OPTIONAL_SINGLE([sleep], [s],   [Sleep time after PostgREST is ready, this is useful for monitoring])\"\n            \"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])\"\n          ];\n        positionalCompletion = \"_command\";\n        workingDir = \"/\";\n        withEnv = postgrest.env;\n        withTmpDir = true;\n      }\n      ''\n        export PGRST_SERVER_UNIX_SOCKET=\"$tmpdir\"/postgrest.socket\n\n        if [ -z \"''${PGRST_CMD:-}\" ]; then\n          rm -f result\n          build_start=$SECONDS\n          if [ -z \"''${PGRST_BUILD_CABAL:-}\" ]; then\n            echo -n \"${commandName}: Building postgrest (nix)... \"\n            # Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.\n            nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > \"$tmpdir\"/build.log 2>&1 || {\n              echo \"failed, output:\"\n              cat \"$tmpdir\"/build.log\n              exit 1\n            }\n            PGRST_CMD=$(echo ./result*/bin/postgrest)\n          else\n            echo -n \"${commandName}: Building postgrest (cabal)... \"\n            postgrest-build\n            PGRST_CMD=postgrest-run\n          fi\n          build_end=$((SECONDS - build_start))\n          printf \"done in %ss.\\n\" \"$build_end\"\n        fi\n\n        ver=$($PGRST_CMD ${legacyConfig} --version)\n\n        echo -n \"${commandName}: Starting $ver... \"\n\n        $PGRST_CMD ${legacyConfig} > \"$tmpdir\"/run.log 2>&1 &\n        pid=$!\n        # shellcheck disable=SC2317\n        cleanup() {\n          # Send INT to all postgrest processes.\n          # Workaround to trigger dumping postgrest.prof for postgrest-profiled-run\n          # Caveat: we cannot realistically limit this to the current process' tree,\n          # since pkill's --parent supports only direct children; therefore this\n          # would reap neighbor postgrest instances as well, because INT is asking\n          # the process to terminate too.\n          # TODO: consider cgroups to make this cleaner\n          ${signalPostgrest}\n          kill \"$pid\" || true\n        }\n        trap cleanup EXIT\n\n        wait_start=$SECONDS\n        timeout -s TERM \"$_arg_timeout\" ${waitForPgrstReady} || {\n          echo \"timed out, output:\"\n          cat \"$tmpdir\"/run.log\n          exit 1\n        }\n        wait_duration=$((SECONDS - wait_start))\n        printf \"done in %ss.\\n\" \"$wait_duration\"\n\n        echo \"${commandName}: You can tail the server logs with: tail -f $tmpdir/run.log\"\n\n        if [[ -n \"$_arg_monitor\" ]]; then\n          ${monitorPid} \"$pid\" > \"$_arg_monitor\" &\n        fi\n\n        if [[ -n \"$_arg_sleep\" ]]; then\n          sleep \"$_arg_sleep\"\n        fi\n\n        (\"$_arg_command\" \"''${_arg_leftovers[@]}\")\n      '';\n\n  monitorPid =\n    writers.writePython3 \"postgrest-monitor-pid\"\n      {\n        libraries = [ python3Packages.pandas python3Packages.tabulate python3Packages.psutil ];\n      }\n      (builtins.readFile ./monitor_pid.py);\nin\nbuildToolbox\n{\n  name = \"postgrest-with\";\n  tools = {\n    inherit\n      withGit\n      withPgAll\n      withPgrst;\n  } // builtins.listToAttrs (\n    # Create a `postgrest-with-pg-` for each PostgreSQL version\n    builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions\n  );\n  # make latest withPg available for other nix files\n  extra = { inherit withPg; };\n}\n"
  },
  {
    "path": "postgrest.cabal",
    "content": "name:               postgrest\nversion:            15\nsynopsis:           REST API for any Postgres database\ndescription:        Reads the schema of a PostgreSQL database and creates RESTful routes\n                    for tables, views, and functions, supporting all HTTP methods that security\n                    permits.\nlicense:            MIT\nlicense-file:       LICENSE\nauthor:             Joe Nelson, Adam Baker, Steve Chavez, Wolfgang Walther\nmaintainer:         Steve Chavez <stevechavezast@gmail.com>\ncategory:           Executable, PostgreSQL, Network APIs\nhomepage:           https://postgrest.org\nbug-reports:        https://github.com/PostgREST/postgrest/issues\nbuild-type:         Simple\nextra-source-files: CHANGELOG.md\ncabal-version:      >= 1.10\n\ntested-with:\n    -- nix\n    GHC == 9.4.8\n    -- cabal on Ubuntu\n    -- stack on FreeBSD, MacOS, Ubuntu, Windows\n  , GHC == 9.6.7\n    -- cabal on Ubuntu\n  , GHC == 9.8.4\n\nsource-repository head\n  type:     git\n  location: https://github.com/PostgREST/postgrest.git\n\nflag dev\n  default:     False\n  manual:      True\n  description: Development flags\n\nflag hpc\n  default:     True\n  manual:      True\n  description: Enable HPC (dev only)\n\nlibrary\n  default-language:   Haskell2010\n  default-extensions: OverloadedStrings\n                      NoImplicitPrelude\n  hs-source-dirs:     src\n  exposed-modules:    PostgREST.Admin\n                      PostgREST.App\n                      PostgREST.AppState\n                      PostgREST.Auth\n                      PostgREST.Auth.Jwt\n                      PostgREST.Auth.JwtCache\n                      PostgREST.Auth.Types\n                      PostgREST.Cache.Sieve\n                      PostgREST.CLI\n                      PostgREST.Client\n                      PostgREST.Config\n                      PostgREST.Config.Database\n                      PostgREST.Config.JSPath\n                      PostgREST.Config.PgVersion\n                      PostgREST.Config.Proxy\n                      PostgREST.Cors\n                      PostgREST.SchemaCache\n                      PostgREST.SchemaCache.Identifiers\n                      PostgREST.SchemaCache.Routine\n                      PostgREST.SchemaCache.Relationship\n                      PostgREST.SchemaCache.Representations\n                      PostgREST.SchemaCache.Table\n                      PostgREST.Error\n                      PostgREST.Error.Types\n                      PostgREST.Listener\n                      PostgREST.Logger\n                      PostgREST.MainTx\n                      PostgREST.MediaType\n                      PostgREST.Metrics\n                      PostgREST.Network\n                      PostgREST.Observation\n                      PostgREST.Query\n                      PostgREST.Query.PreQuery\n                      PostgREST.Query.QueryBuilder\n                      PostgREST.Query.SqlFragment\n                      PostgREST.Query.Statements\n                      PostgREST.Plan\n                      PostgREST.Plan.CallPlan\n                      PostgREST.Plan.MutatePlan\n                      PostgREST.Plan.Negotiate\n                      PostgREST.Plan.ReadPlan\n                      PostgREST.Plan.Types\n                      PostgREST.RangeQuery\n                      PostgREST.Unix\n                      PostgREST.ApiRequest\n                      PostgREST.ApiRequest.Preferences\n                      PostgREST.ApiRequest.QueryParams\n                      PostgREST.ApiRequest.Payload\n                      PostgREST.ApiRequest.Types\n                      PostgREST.Response\n                      PostgREST.Response.OpenAPI\n                      PostgREST.Response.GucHeader\n                      PostgREST.Response.Performance\n                      PostgREST.TimeIt\n                      PostgREST.Version\n  build-depends:      base                      >= 4.9 && < 4.20\n                    , HTTP                      >= 4000.3.7 && < 4000.5\n                    , Ranged-sets               >= 0.3 && < 0.5\n                    , aeson                     >= 2.0.3 && < 2.3\n                    , auto-update               >= 0.1.4 && < 0.3\n                    , base64-bytestring         >= 1 && < 1.3\n                    , bytestring                >= 0.10.8 && < 0.13\n                    , case-insensitive          >= 1.2 && < 1.3\n                    , cassava                   >= 0.4.5 && < 0.6\n                    , configurator-pg           >= 0.2.11 && < 0.3\n                    , containers                >= 0.5.7 && < 0.7\n                    , cookie                    >= 0.4.2 && < 0.6\n                    , directory                 >= 1.2.6 && < 1.4\n                    , either                    >= 4.4.1 && < 5.1\n                    , extra                     >= 1.7.0 && < 2.0\n                    , fuzzyset                  >= 0.2.4 && < 0.3\n                    , hasql                     >= 1.6.1.1 && < 1.7\n                    , hasql-dynamic-statements  >= 0.3.1 && < 0.4\n                    , hasql-notifications       >= 0.2.2.2 && < 0.2.3\n                    , hasql-pool                >= 1.0.1 && < 1.1\n                    , hasql-transaction         >= 1.0.1 && < 1.2\n                    , http-client               >= 0.7.19 && < 0.8\n                    , http-types                >= 0.12.2 && < 0.13\n                    , insert-ordered-containers >= 0.2.2 && < 0.3\n                    , jose-jwt                  >= 0.9.6 && < 0.11\n                    , lens                      >= 4.14 && < 5.4\n                    , lens-aeson                >= 1.0.1 && < 1.3\n                    , mtl                       >= 2.2.2 && < 2.4\n                    , neat-interpolation        >= 0.5 && < 0.6\n                    , network                   >= 2.6 && < 3.3\n                    , network-uri               >= 2.6.1 && < 2.8\n                    , optparse-applicative      >= 0.13 && < 0.19\n                    , parsec                    >= 3.1.11 && < 3.2\n                    -- Technically unused, can be removed after updating to hasql >= 1.7\n                    , postgresql-libpq          >= 0.10\n                    , prometheus-client         >= 1.1.1 && < 1.2.0\n                    , protolude                 >= 0.3.1 && < 0.4\n                    , regex-tdfa                >= 1.2.2 && < 1.4\n                    , retry                     >= 0.7.4 && < 0.10\n                    , scientific                >= 0.3.4 && < 0.4\n                    , streaming-commons         >= 0.2.3.1 && < 0.3\n                    , swagger2                  >= 2.4 && < 2.9\n                    , text                      >= 1.2.2 && < 2.2\n                    , time                      >= 1.6 && < 1.13\n                    , unordered-containers      >= 0.2.8 && < 0.3\n                    , unix-compat               >= 0.5.4 && < 0.8\n                    , vault                     >= 0.3.1.5 && < 0.4\n                    , vector                    >= 0.11 && < 0.14\n                    , wai                       >= 3.2.1 && < 3.3\n                    , wai-cors                  >= 0.2.5 && < 0.3\n                    , wai-extra                 >= 3.1.8 && < 3.2\n                    -- We already depend on wai-logger >= 2.3.7 indirectly via wai-extra,\n                    -- but we want to depend on 2.4.0 which fixes 'unknownSocket' log output\n                    -- for unix sockets; this is tested in test/io/test_io.py. See\n                    -- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3\n                    , wai-logger                >= 2.4.0\n                    , warp                      >= 3.3.19 && < 3.5\n                    , stm                       >= 2.5 && < 3\n                    , stm-hamt                  >= 1.2 && < 2\n                    , focus                     >= 1.0 && < 2\n                    , some                      >= 1.0.4.1 && < 2\n                      -- -fno-spec-constr may help keep compile time memory use in check,\n                      --   see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304\n                      -- -optP-Wno-nonportable-include-path\n                      --   prevents build failures on case-insensitive filesystems (macos),\n                      --   see https://github.com/commercialhaskell/stack/issues/3918\n  ghc-options:        -Werror -Wall -fwarn-identities\n                      -fno-spec-constr -optP-Wno-nonportable-include-path\n\n  if flag(dev)\n    ghc-options: -O0 -fwrite-ide-info\n    if flag(hpc)\n      ghc-options: -fhpc -hpcdir .hpc\n  else\n    ghc-options: -O2\n\n  if !os(windows)\n    build-depends:\n      unix\n\nexecutable postgrest\n  default-language:   Haskell2010\n  default-extensions: OverloadedStrings\n                      NoImplicitPrelude\n  hs-source-dirs:     main\n  main-is:            Main.hs\n  build-depends:      base                >= 4.9 && < 4.20\n                    , containers          >= 0.5.7 && < 0.7\n                    , postgrest\n                    , protolude           >= 0.3.1 && < 0.4\n  ghc-options:        -threaded -rtsopts \"-with-rtsopts=-N -I0 -qg\"\n                      -O2 -Werror -Wall -fwarn-identities\n                      -fno-spec-constr -optP-Wno-nonportable-include-path\n\n  if flag(dev)\n    ghc-options: -O0 -fwrite-ide-info\n                 -- https://github.com/PostgREST/postgrest/issues/387\n                 -with-rtsopts=-K1K\n    if flag(hpc)\n      ghc-options: -fhpc -hpcdir .hpc\n  else\n    ghc-options: -O2\n\ntest-suite spec\n  type:               exitcode-stdio-1.0\n  default-language:   Haskell2010\n  default-extensions: OverloadedStrings\n                      QuasiQuotes\n                      NoImplicitPrelude\n  hs-source-dirs:     test/spec\n  main-is:            Main.hs\n  other-modules:      Feature.Auth.AsymmetricJwtSpec\n                      Feature.Auth.AudienceJwtSecretSpec\n                      Feature.Auth.AuthSpec\n                      Feature.Auth.BinaryJwtSecretSpec\n                      Feature.Auth.NoAnonSpec\n                      Feature.Auth.NoJwtSecretSpec\n                      Feature.ConcurrentSpec\n                      Feature.CorsSpec\n                      Feature.ExtraSearchPathSpec\n                      Feature.NoSuperuserSpec\n                      Feature.ObservabilitySpec\n                      Feature.OpenApi.DisabledOpenApiSpec\n                      Feature.OpenApi.IgnorePrivOpenApiSpec\n                      Feature.OpenApi.OpenApiSpec\n                      Feature.OpenApi.ProxySpec\n                      Feature.OpenApi.RootSpec\n                      Feature.OpenApi.SecurityOpenApiSpec\n                      Feature.OptionsSpec\n                      Feature.Query.AggregateFunctionsSpec\n                      Feature.Query.AndOrParamsSpec\n                      Feature.Query.ComputedRelsSpec\n                      Feature.Query.CustomMediaSpec\n                      Feature.Query.DeleteSpec\n                      Feature.Query.EmbedDisambiguationSpec\n                      Feature.Query.EmbedInnerJoinSpec\n                      Feature.Query.ErrorSpec\n                      Feature.Query.InsertSpec\n                      Feature.Query.JsonOperatorSpec\n                      Feature.Query.MultipleSchemaSpec\n                      Feature.Query.NullsStripSpec\n                      Feature.Query.PgSafeUpdateSpec\n                      Feature.Query.PlanSpec\n                      Feature.Query.PostGISSpec\n                      Feature.Query.PreferencesSpec\n                      Feature.Query.QueryLimitedSpec\n                      Feature.Query.QuerySpec\n                      Feature.Query.RangeSpec\n                      Feature.Query.RawOutputTypesSpec\n                      Feature.Query.RelatedQueriesSpec\n                      Feature.Query.RpcSpec\n                      Feature.Query.ServerTimingSpec\n                      Feature.Query.SingularSpec\n                      Feature.Query.SpreadQueriesSpec\n                      Feature.Query.UnicodeSpec\n                      Feature.Query.UpdateSpec\n                      Feature.Query.UpsertSpec\n                      Feature.RollbackSpec\n                      Feature.RpcPreRequestGucsSpec\n                      SpecHelper\n  build-depends:      base              >= 4.9 && < 4.20\n                    , aeson             >= 2.0.3 && < 2.3\n                    , aeson-qq          >= 0.8.1 && < 0.9\n                    , async             >= 2.1.1 && < 2.3\n                    , base64-bytestring >= 1 && < 1.3\n                    , bytestring        >= 0.10.8 && < 0.13\n                    , case-insensitive  >= 1.2 && < 1.3\n                    , containers        >= 0.5.7 && < 0.7\n                    , hasql-pool        >= 1.0.1 && < 1.1\n                    , hasql-transaction >= 1.0.1 && < 1.2\n                    , heredoc           >= 0.2 && < 0.3\n                    , hspec             >= 2.3 && < 2.12\n                    , hspec-expectations >= 0.8.4 && < 0.9\n                    , hspec-wai         >= 0.10 && < 0.12\n                    , hspec-wai-json    >= 0.10 && < 0.12\n                    , http-types        >= 0.12.3 && < 0.13\n                    , jose-jwt          >= 0.9.6 && < 0.11\n                    , lens              >= 4.14 && < 5.4\n                    , lens-aeson        >= 1.0.1 && < 1.3\n                    , monad-control     >= 1.0.1 && < 1.1\n                    , postgrest\n                    , process           >= 1.4.2 && < 1.7\n                    , prometheus-client >= 1.1.1 && < 1.2.0\n                    , protolude         >= 0.3.1 && < 0.4\n                    , regex-tdfa        >= 1.2.2 && < 1.4\n                    , scientific        >= 0.3.4 && < 0.4\n                    , text              >= 1.2.2 && < 2.2\n                    , transformers-base >= 0.4.4 && < 0.5\n                    , wai               >= 3.2.1 && < 3.3\n                    , wai-extra         >= 3.0.19 && < 3.2\n  ghc-options:        -threaded -O0 -Werror -Wall -fwarn-identities\n                      -fno-spec-constr -optP-Wno-nonportable-include-path\n                      -fno-warn-missing-signatures\n                      -fwrite-ide-info\n                      -- https://github.com/PostgREST/postgrest/issues/387\n                      -with-rtsopts=-K33K\n\ntest-suite observability\n  type:               exitcode-stdio-1.0\n  default-language:   Haskell2010\n  default-extensions: OverloadedStrings\n                      QuasiQuotes\n                      NoImplicitPrelude\n  hs-source-dirs:     test/observability\n  main-is:            Main.hs\n  other-modules:      ObsHelper\n                      Observation.JwtCache\n  build-depends:      base              >= 4.9 && < 4.20\n                    , base64-bytestring >= 1 && < 1.3\n                    , bytestring        >= 0.10.8 && < 0.13\n                    , hasql-pool        >= 1.0.1 && < 1.1\n                    , hasql-transaction >= 1.0.1 && < 1.2\n                    , hspec             >= 2.3 && < 2.12\n                    , hspec-expectations >= 0.8.4 && < 0.9\n                    , hspec-wai         >= 0.10 && < 0.12\n                    , hspec-wai-json    >= 0.10 && < 0.12\n                    , http-types        >= 0.12.3 && < 0.13\n                    , jose-jwt          >= 0.9.6 && < 0.11\n                    , postgrest\n                    , prometheus-client >= 1.1.1 && < 1.2.0\n                    , protolude         >= 0.3.1 && < 0.4\n                    , wai               >= 3.2.1 && < 3.3\n  ghc-options:        -threaded -O0 -Werror -Wall -fwarn-identities\n                      -fno-spec-constr -optP-Wno-nonportable-include-path\n                      -fwrite-ide-info\n                      -- https://github.com/PostgREST/postgrest/issues/387\n                      -with-rtsopts=-K33K\n\ntest-suite doctests\n  type:               exitcode-stdio-1.0\n  default-language:   Haskell2010\n  default-extensions: OverloadedStrings\n                      NoImplicitPrelude\n  hs-source-dirs:     test/doc\n  main-is:            Main.hs\n  build-depends:      base              >= 4.9 && < 4.20\n                    , doctest           >= 0.8\n                    , postgrest\n                    , pretty-simple\n                    , protolude         >= 0.3.1 && < 0.4\n  ghc-options:        -threaded -O0 -Werror -Wall -fwarn-identities\n                      -fno-spec-constr -optP-Wno-nonportable-include-path\n"
  },
  {
    "path": "shell.nix",
    "content": "# The additional modules below have large dependencies and are therefore\n# disabled by default. You can activate them by passing arguments to nix-shell,\n# e.g.:\n#\n#    nix-shell --arg docker true\n#\n# We highly recommend that use the PostgREST binary cache by installing cachix\n# (https://app.cachix.org/) and running `cachix use postgrest`.\n{ docker ? false\n, postgrest ? import ./default.nix { }\n}:\nlet\n  inherit (postgrest) pkgs;\n\n  inherit (pkgs) lib;\n\n  toolboxes =\n    [\n      postgrest.cabalTools\n      postgrest.devTools\n      postgrest.docs\n      postgrest.gitTools\n      postgrest.loadtest\n      postgrest.nixpkgsTools\n      postgrest.release\n      postgrest.style\n      postgrest.tests\n      postgrest.withTools\n    ]\n    ++ lib.optional docker postgrest.docker;\n\nin\nlib.overrideDerivation postgrest.env (\n  base: {\n    buildInputs =\n      base.buildInputs ++ [\n        pkgs.cabal-install\n        pkgs.cabal2nix\n        pkgs.git\n        pkgs.postgresql\n        pkgs.update-nix-fetchgit\n        postgrest.hsie.bin\n      ]\n      ++ toolboxes;\n\n    shellHook =\n      ''\n        export HISTFILE=.history\n\n        # Bypass proxy for all hosts, it prevents HTTP client failures used in test\n        # suites. See: https://github.com/PostgREST/postgrest/issues/4633 for more info\n        export NO_PROXY=*\n\n        source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh\n        source ${pkgs.git}/share/git/contrib/completion/git-completion.bash\n        source ${postgrest.hsie.bash-completion}\n\n      ''\n      + builtins.concatStringsSep \"\\n\" (\n        builtins.map (bash-completion: \"source ${bash-completion}\") (\n          builtins.concatLists (builtins.map (toolbox: toolbox.bash-completion) toolboxes)\n        )\n      );\n  }\n)\n"
  },
  {
    "path": "src/PostgREST/Admin.hs",
    "content": "module PostgREST.Admin\n  ( runAdmin\n  ) where\n\nimport qualified Data.Aeson                as JSON\nimport qualified Network.HTTP.Types.Status as HTTP\nimport qualified Network.Wai               as Wai\nimport qualified Network.Wai.Handler.Warp  as Warp\n\nimport Control.Monad.Extra       (whenJust)\nimport Network.Socket            hiding (addrFamily)\nimport Network.Socket.ByteString\n\nimport PostgREST.AppState    (AppState)\nimport PostgREST.MediaType   (MediaType (..), toContentType)\nimport PostgREST.Metrics     (metricsToText)\nimport PostgREST.Network     (resolveSocketToAddress)\nimport PostgREST.Observation (Observation (..))\n\nimport qualified PostgREST.AppState as AppState\n\nimport qualified Network.Socket as NS\nimport           Protolude\n\nrunAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()\nrunAdmin appState maybeAdminSocket socketREST settings = do\n  whenJust maybeAdminSocket $ \\adminSocket -> do\n    address <- resolveSocketToAddress adminSocket\n    observer $ AdminStartObs address\n    void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp\n  where\n    adminApp = admin appState socketREST\n    observer = AppState.getObserver appState\n\n-- | PostgREST admin application\nadmin :: AppState.AppState -> NS.Socket -> Wai.Application\nadmin appState socketREST req respond  = do\n  isMainAppReachable  <- isRight <$> reachMainApp socketREST\n  isLoaded <- AppState.isLoaded appState\n  isPending <- AppState.isPending appState\n\n  case Wai.pathInfo req of\n    [\"live\"] ->\n      respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty\n    [\"ready\"] ->\n      let\n        status | not isMainAppReachable = HTTP.status500\n               | isPending              = HTTP.status503\n               | isLoaded               = HTTP.status200\n               | otherwise              = HTTP.status500\n      in\n      respond $ Wai.responseLBS status [] mempty\n    [\"schema_cache\"] -> do\n      sCache <- AppState.getSchemaCache appState\n      respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)\n    [\"metrics\"] -> do\n      mets <- metricsToText\n      respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance\n    _ ->\n      respond $ Wai.responseLBS HTTP.status404 [] mempty\n\n-- Try to connect to the main app socket\n-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections\nreachMainApp :: Socket -> IO (Either IOException ())\nreachMainApp appSock = do\n  sockAddr <- getSocketName appSock\n  sock <- socket (addrFamily sockAddr) Stream defaultProtocol\n  try $ do\n    connect sock sockAddr\n    withSocketsDo $ bracket (pure sock) close sendEmpty\n  where\n    sendEmpty sock = void $ send sock mempty\n    addrFamily (SockAddrInet _ _) = AF_INET\n    addrFamily (SockAddrInet6 {}) = AF_INET6\n    addrFamily (SockAddrUnix _)   = AF_UNIX\n"
  },
  {
    "path": "src/PostgREST/ApiRequest/Payload.hs",
    "content": "-- |\n-- Module      : PostgREST.ApiRequest.Payload\n-- Description : Parser for PostgREST Request Body\n--\n-- This module is in charge of parsing the request body (payload)\n--\n{-# LANGUAGE LambdaCase     #-}\n{-# LANGUAGE NamedFieldPuns #-}\nmodule PostgREST.ApiRequest.Payload\n  ( getPayload\n  ) where\n\nimport qualified Data.Aeson            as JSON\nimport qualified Data.Aeson.Key        as K\nimport qualified Data.Aeson.KeyMap     as KM\nimport qualified Data.ByteString.Char8 as BS\nimport qualified Data.ByteString.Lazy  as LBS\nimport qualified Data.Csv              as CSV\nimport qualified Data.HashMap.Strict   as HM\nimport qualified Data.Map.Strict       as M\nimport qualified Data.Set              as S\nimport qualified Data.Text.Encoding    as T\nimport qualified Data.Vector           as V\n\nimport Control.Arrow           ((***))\nimport Data.Aeson.Types        (emptyArray, emptyObject)\nimport Data.Either.Combinators (mapBoth)\nimport Network.HTTP.Types.URI  (parseSimpleQuery)\n\nimport PostgREST.ApiRequest.QueryParams  (QueryParams (..))\nimport PostgREST.ApiRequest.Types\nimport PostgREST.Error                   (ApiRequestError (..))\nimport PostgREST.MediaType               (MediaType (..))\nimport PostgREST.SchemaCache.Identifiers (FieldName)\n\nimport qualified PostgREST.MediaType as MediaType\n\nimport Protolude\n\ngetPayload :: RequestBody -> MediaType -> QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set FieldName)\ngetPayload reqBody contentMediaType QueryParams{qsColumns} action = do\n  checkedPayload <- if shouldParsePayload then payload else Right Nothing\n  let cols = case (checkedPayload, columns) of\n        (Just ProcessedJSON{payKeys}, _)       -> payKeys\n        (Just ProcessedUrlEncoded{payKeys}, _) -> payKeys\n        (Just RawJSON{}, Just cls)             -> cls\n        _                                      -> S.empty\n  return (checkedPayload, cols)\n  where\n    payload :: Either ApiRequestError (Maybe Payload)\n    payload = mapBoth InvalidBody Just $ case (contentMediaType, isProc) of\n      (MTApplicationJSON, _) ->\n        if isJust columns\n          then Right $ RawJSON reqBody\n          else note \"All object keys must match\" . payloadAttributes reqBody\n                 =<< if LBS.null reqBody && isProc\n                       then Right emptyObject\n                       else first BS.pack $\n                          -- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)\n                          maybe (Left \"Empty or invalid json\") Right $ JSON.decode reqBody\n      (MTTextCSV, _) -> do\n        json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)\n        note \"All lines must have same number of fields\" $ payloadAttributes (JSON.encode json) json\n      (MTUrlEncoded, True) ->\n        Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)\n      (MTUrlEncoded, False) ->\n        let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in\n        Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)\n      (MTTextPlain, True) -> Right $ RawPay reqBody\n      (MTTextXML, True) -> Right $ RawPay reqBody\n      (MTOctetStream, True) -> Right $ RawPay reqBody\n      (ct, _) -> Left $ \"Content-Type not acceptable: \" <> MediaType.toMime ct\n\n    shouldParsePayload = case action of\n      ActDb (ActRelationMut _ MutationDelete) -> False\n      ActDb (ActRelationMut _ _)              -> True\n      ActDb (ActRoutine _  Inv)               -> True\n      _                                       -> False\n\n    columns = case action of\n      ActDb (ActRelationMut _ MutationCreate) -> qsColumns\n      ActDb (ActRelationMut _ MutationUpdate) -> qsColumns\n      ActDb (ActRoutine     _ Inv)            -> qsColumns\n      _                                       -> Nothing\n\n    isProc = case action of\n      ActDb (ActRoutine _ _) -> True\n      _                      -> False\n    params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)\n\ntype CsvData = V.Vector (M.Map Text LBS.ByteString)\n\n{-|\n  Converts CSV like\n  a,b\n  1,hi\n  2,bye\n\n  into a JSON array like\n  [ {\"a\": \"1\", \"b\": \"hi\"}, {\"a\": 2, \"b\": \"bye\"} ]\n\n  The reason for its odd signature is so that it can compose\n  directly with CSV.decodeByName\n-}\ncsvToJson :: (CSV.Header, CsvData) -> JSON.Value\ncsvToJson (_, vals) =\n  JSON.Array $ V.map rowToJsonObj vals\n where\n  rowToJsonObj = JSON.Object . KM.fromMapText .\n    M.map (\\str ->\n        if str == \"NULL\"\n          then JSON.Null\n          else JSON.String . T.decodeUtf8 $ LBS.toStrict str\n      )\n\npayloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload\npayloadAttributes raw json =\n  -- Test that Array contains only Objects having the same keys\n  case json of\n    JSON.Array arr ->\n      case arr V.!? 0 of\n        Just (JSON.Object o) ->\n          let canonicalKeys = S.fromList $ K.toText <$> KM.keys o\n              areKeysUniform = all (\\case\n                JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys\n                _ -> False) arr in\n          if areKeysUniform\n            then Just $ ProcessedJSON raw canonicalKeys\n            else Nothing\n        Just _ -> Nothing\n        Nothing -> Just emptyPJArray\n\n    JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)\n\n    -- truncate everything else to an empty array.\n    _ -> Just emptyPJArray\n  where\n    emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty\n"
  },
  {
    "path": "src/PostgREST/ApiRequest/Preferences.hs",
    "content": "-- |\n-- Module: PostgREST.ApiRequest.Preferences\n-- Description: Track client preferences to be employed when processing requests\n--\n-- Track client preferences set in HTTP 'Prefer' headers according to RFC7240[1].\n--\n-- [1] https://datatracker.ietf.org/doc/html/rfc7240\n--\n{-# LANGUAGE NamedFieldPuns #-}\nmodule PostgREST.ApiRequest.Preferences\n  ( Preferences(..)\n  , PreferCount(..)\n  , PreferHandling(..)\n  , PreferMissing(..)\n  , PreferRepresentation(..)\n  , PreferResolution(..)\n  , PreferTransaction(..)\n  , PreferTimezone(..)\n  , PreferMaxAffected(..)\n  , fromHeaders\n  , shouldCount\n  , shouldExplainCount\n  , prefAppliedHeader\n  ) where\n\nimport qualified Data.ByteString.Char8     as BS\nimport qualified Data.Map                  as Map\nimport qualified Data.Set                  as S\nimport qualified Network.HTTP.Types.Header as HTTP\n\nimport PostgREST.Config.Database (TimezoneNames)\n\nimport Protolude\n\n-- $setup\n-- Setup for doctests\n-- >>> import Text.Pretty.Simple (pPrint)\n-- >>> deriving instance Show PreferResolution\n-- >>> deriving instance Show PreferRepresentation\n-- >>> deriving instance Show PreferCount\n-- >>> deriving instance Show PreferTransaction\n-- >>> deriving instance Show PreferMissing\n-- >>> deriving instance Show PreferHandling\n-- >>> deriving instance Show PreferTimezone\n-- >>> deriving instance Show PreferMaxAffected\n-- >>> deriving instance Show Preferences\n\n-- | Preferences recognized by the application.\ndata Preferences\n  = Preferences\n    { preferResolution     :: Maybe PreferResolution\n    , preferRepresentation :: Maybe PreferRepresentation\n    , preferCount          :: Maybe PreferCount\n    , preferTransaction    :: Maybe PreferTransaction\n    , preferMissing        :: Maybe PreferMissing\n    , preferHandling       :: Maybe PreferHandling\n    , preferTimezone       :: Maybe PreferTimezone\n    , preferMaxAffected    :: Maybe PreferMaxAffected\n    , invalidPrefs         :: [ByteString]\n    }\n\n-- |\n-- Parse HTTP headers based on RFC7240[1] to identify preferences.\n--\n-- >>> let sc = S.fromList [\"America/Los_Angeles\"]\n--\n-- One header with comma-separated values can be used to set multiple preferences:\n-- >>> pPrint $ fromHeaders True sc [(\"Prefer\", \"resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100\")]\n-- Preferences\n--     { preferResolution = Just IgnoreDuplicates\n--     , preferRepresentation = Nothing\n--     , preferCount = Just ExactCount\n--     , preferTransaction = Nothing\n--     , preferMissing = Nothing\n--     , preferHandling = Nothing\n--     , preferTimezone = Just\n--         ( PreferTimezone \"America/Los_Angeles\" )\n--     , preferMaxAffected = Just\n--         ( PreferMaxAffected 100 )\n--     , invalidPrefs = []\n--     }\n--\n-- Multiple headers can also be used:\n--\n-- >>> pPrint $ fromHeaders True sc [(\"Prefer\", \"resolution=ignore-duplicates\"), (\"Prefer\", \"count=exact\"), (\"Prefer\", \"missing=null\"), (\"Prefer\", \"handling=lenient\"), (\"Prefer\", \"invalid\"), (\"Prefer\", \"max-affected=5999\")]\n-- Preferences\n--     { preferResolution = Just IgnoreDuplicates\n--     , preferRepresentation = Nothing\n--     , preferCount = Just ExactCount\n--     , preferTransaction = Nothing\n--     , preferMissing = Just ApplyNulls\n--     , preferHandling = Just Lenient\n--     , preferTimezone = Nothing\n--     , preferMaxAffected = Just\n--         ( PreferMaxAffected 5999 )\n--     , invalidPrefs = [ \"invalid\" ]\n--     }\n--\n-- If a preference is set more than once, only the first is used:\n--\n-- >>> preferTransaction $ fromHeaders True sc [(\"Prefer\", \"tx=commit, tx=rollback\")]\n-- Just Commit\n--\n-- This is also the case across multiple headers:\n--\n-- >>> :{\n--   preferResolution . fromHeaders True sc $\n--     [ (\"Prefer\", \"resolution=ignore-duplicates\")\n--     , (\"Prefer\", \"resolution=merge-duplicates\")\n--     ]\n-- :}\n-- Just IgnoreDuplicates\n--\n--\n-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:\n--\n-- >>> pPrint $ fromHeaders True sc [(\"prefer\", \"count=exact,    tx=commit   ,return=representation , missing=default, handling=strict, anything\")]\n-- Preferences\n--     { preferResolution = Nothing\n--     , preferRepresentation = Just Full\n--     , preferCount = Just ExactCount\n--     , preferTransaction = Just Commit\n--     , preferMissing = Just ApplyDefaults\n--     , preferHandling = Just Strict\n--     , preferTimezone = Nothing\n--     , preferMaxAffected = Nothing\n--     , invalidPrefs = [ \"anything\" ]\n--     }\n--\nfromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences\nfromHeaders allowTxDbOverride acceptedTzNames headers =\n  Preferences\n    { preferResolution     = parsePrefs [MergeDuplicates, IgnoreDuplicates]\n    , preferRepresentation = parsePrefs [Full, None, HeadersOnly]\n    , preferCount          = parsePrefs [ExactCount, PlannedCount, EstimatedCount]\n    , preferTransaction    = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing\n    , preferMissing        = parsePrefs [ApplyDefaults, ApplyNulls]\n    , preferHandling       = parsePrefs [Strict, Lenient]\n    , preferTimezone       = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing\n    , preferMaxAffected    = PreferMaxAffected <$> maxAffectedPref\n    , invalidPrefs         = filter isUnacceptable prefs\n    }\n  where\n    mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]\n    mapToHeadVal = map toHeaderValue\n    acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++\n                    mapToHeadVal [Full, None, HeadersOnly] ++\n                    mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++\n                    mapToHeadVal [Commit, Rollback] ++\n                    mapToHeadVal [ApplyDefaults, ApplyNulls] ++\n                    mapToHeadVal [Strict, Lenient]\n\n    prefHeaders = filter ((==) HTTP.hPrefer . fst) headers\n    prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders\n\n    listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList\n\n    timezonePref = listStripPrefix \"timezone=\" prefs\n    isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True\n\n    maxAffectedPref = listStripPrefix \"max-affected=\" prefs >>= readMaybe . BS.unpack\n\n    isUnacceptable p = p `notElem` acceptedPrefs &&\n                       (isNothing (BS.stripPrefix \"timezone=\" p) ||  not isTimezonePrefAccepted) &&\n                       isNothing (BS.stripPrefix \"max-affected=\" p)\n\n    parsePrefs :: ToHeaderValue a => [a] -> Maybe a\n    parsePrefs vals =\n      head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs\n\n    prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a\n    prefMap = Map.fromList . fmap (\\pref -> (toHeaderValue pref, pref))\n\nprefAppliedHeader :: Preferences -> Maybe HTTP.Header\nprefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =\n  if null prefsVals\n    then Nothing\n    else Just (HTTP.hPreferenceApplied, combined)\n  where\n    combined = BS.intercalate \", \" prefsVals\n    prefsVals = catMaybes [\n        toHeaderValue <$> preferResolution\n      , toHeaderValue <$> preferMissing\n      , toHeaderValue <$> preferRepresentation\n      , toHeaderValue <$> preferCount\n      , toHeaderValue <$> preferTransaction\n      , toHeaderValue <$> preferHandling\n      , toHeaderValue <$> preferTimezone\n      , if preferHandling == Just Strict then toHeaderValue <$> preferMaxAffected else Nothing\n      ]\n\n-- |\n-- Convert a preference into the value that we look for in the 'Prefer' headers.\n--\n-- >>> toHeaderValue MergeDuplicates\n-- \"resolution=merge-duplicates\"\n--\nclass ToHeaderValue a where\n  toHeaderValue :: a -> ByteString\n\n-- | How to handle duplicate values.\ndata PreferResolution\n  = MergeDuplicates\n  | IgnoreDuplicates\n  deriving Eq\n\ninstance ToHeaderValue PreferResolution where\n  toHeaderValue MergeDuplicates  = \"resolution=merge-duplicates\"\n  toHeaderValue IgnoreDuplicates = \"resolution=ignore-duplicates\"\n\n-- |\n-- How to return the mutated data.\n--\n-- From https://tools.ietf.org/html/rfc7240#section-4.2\ndata PreferRepresentation\n  = Full        -- ^ Return the body.\n  | HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.\n  | None        -- ^ Return nothing from the mutated data.\n  deriving Eq\n\ninstance ToHeaderValue PreferRepresentation where\n  toHeaderValue Full        = \"return=representation\"\n  toHeaderValue None        = \"return=minimal\"\n  toHeaderValue HeadersOnly = \"return=headers-only\"\n\n-- | How to determine the count of (expected) results\ndata PreferCount\n  = ExactCount     -- ^ Exact count (slower).\n  | PlannedCount   -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.\n  | EstimatedCount -- ^ Use the query planner rows if the count is superior to max-rows, otherwise get the exact count.\n  deriving Eq\n\ninstance ToHeaderValue PreferCount where\n  toHeaderValue ExactCount     = \"count=exact\"\n  toHeaderValue PlannedCount   = \"count=planned\"\n  toHeaderValue EstimatedCount = \"count=estimated\"\n\nshouldCount :: Maybe PreferCount -> Bool\nshouldCount prefCount =\n  prefCount == Just ExactCount || prefCount == Just EstimatedCount\n\nshouldExplainCount :: Maybe PreferCount -> Bool\nshouldExplainCount prefCount =\n  prefCount == Just PlannedCount || prefCount == Just EstimatedCount\n\n-- | Whether to commit or roll back transactions.\ndata PreferTransaction\n  = Commit   -- ^ Commit transaction - the default.\n  | Rollback -- ^ Rollback transaction after sending the response - does not persist changes, e.g. for running tests.\n  deriving Eq\n\ninstance ToHeaderValue PreferTransaction where\n  toHeaderValue Commit   = \"tx=commit\"\n  toHeaderValue Rollback = \"tx=rollback\"\n\n-- |\n-- How to handle the insertion/update when the keys specified in ?columns are not present\n-- in the json body.\ndata PreferMissing\n  = ApplyDefaults  -- ^ Use the default column value for missing values.\n  | ApplyNulls     -- ^ Use the null value for missing values.\n  deriving Eq\n\ninstance ToHeaderValue PreferMissing where\n  toHeaderValue ApplyDefaults = \"missing=default\"\n  toHeaderValue ApplyNulls    = \"missing=null\"\n\n-- |\n-- Handling of unrecognised preferences\ndata PreferHandling\n  = Strict  -- ^ Throw error on unrecognised preferences\n  | Lenient -- ^ Ignore unrecognised preferences\n  deriving Eq\n\ninstance ToHeaderValue PreferHandling where\n  toHeaderValue Strict  = \"handling=strict\"\n  toHeaderValue Lenient = \"handling=lenient\"\n\n-- |\n-- Change timezone\nnewtype PreferTimezone = PreferTimezone ByteString\n\ninstance ToHeaderValue PreferTimezone where\n  toHeaderValue (PreferTimezone tz) = \"timezone=\" <> tz\n\n-- |\n-- Limit Affected Resources\nnewtype PreferMaxAffected = PreferMaxAffected Int64\n\ninstance ToHeaderValue PreferMaxAffected where\n  toHeaderValue (PreferMaxAffected n) = \"max-affected=\" <> show n\n"
  },
  {
    "path": "src/PostgREST/ApiRequest/QueryParams.hs",
    "content": "-- |\n-- Module      : PostgREST.ApiRequest.QueryParams\n-- Description : Parser for PostgREST Query parameters\n--\n-- This module is in charge of parsing all the querystring values in an url, e.g.\n-- the select, id, order in `/projects?select=id,name&id=eq.1&order=id,name.desc`.\n{-# LANGUAGE LambdaCase    #-}\n{-# LANGUAGE TupleSections #-}\nmodule PostgREST.ApiRequest.QueryParams\n  ( parse\n  , QueryParams(..)\n  , pRequestRange\n  ) where\n\nimport qualified Data.ByteString.Char8         as BS\nimport qualified Data.HashMap.Strict           as HM\nimport qualified Data.List                     as L\nimport qualified Data.Set                      as S\nimport qualified Data.Text                     as T\nimport qualified Data.Text.Encoding            as T\nimport qualified Network.HTTP.Base             as HTTP\nimport qualified Network.HTTP.Types.URI        as HTTP\nimport qualified Text.ParserCombinators.Parsec as P\n\nimport Control.Arrow                 ((***))\nimport Data.Either.Combinators       (mapLeft)\nimport Data.List                     (init, last)\nimport Data.Ranged.Boundaries        (Boundary (..))\nimport Data.Ranged.Ranges            (Range (..))\nimport Data.Tree                     (Tree (..))\nimport Text.Parsec.Error             (errorMessages,\n                                      showErrorMessages)\nimport Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,\n                                      anyChar, between, char, choice,\n                                      digit, eof, errorPos, letter,\n                                      lookAhead, many1, noneOf,\n                                      notFollowedBy, oneOf,\n                                      optionMaybe, sepBy, sepBy1,\n                                      string, try, (<?>))\n\nimport PostgREST.RangeQuery              (NonnegRange, allRange,\n                                          rangeGeq, rangeLimit,\n                                          rangeOffset, restrictRange)\nimport PostgREST.SchemaCache.Identifiers (FieldName)\n\nimport PostgREST.ApiRequest.Types (AggregateFunction (..),\n                                   EmbedParam (..), EmbedPath, Field,\n                                   Filter (..), FtsOperator (..),\n                                   Hint, IsVal (..), JoinType (..),\n                                   JsonOperand (..),\n                                   JsonOperation (..), JsonPath,\n                                   ListVal, LogicOperator (..),\n                                   LogicTree (..), OpExpr (..),\n                                   OpQuantifier (..), Operation (..),\n                                   OrderDirection (..),\n                                   OrderNulls (..), OrderTerm (..),\n                                   QuantOperator (..),\n                                   SelectItem (..),\n                                   SimpleOperator (..), SingleVal)\n\nimport PostgREST.Error (QPError (..))\n\nimport Protolude hiding (Sum, try)\n\ndata QueryParams =\n  QueryParams\n    { qsCanonical      :: ByteString\n    -- ^ Canonical representation of the query params, sorted alphabetically\n    , qsParams         :: [(Text, Text)]\n    -- ^ Parameters for RPC calls\n    , qsRanges         :: HM.HashMap Text (Range Integer)\n    -- ^ Ranges derived from &limit and &offset params\n    , qsOrder          :: [(EmbedPath, [OrderTerm])]\n    -- ^ &order parameters for each level\n    , qsLogic          :: [(EmbedPath, LogicTree)]\n    -- ^ &and and &or parameters used for complex boolean logic\n    , qsColumns        :: Maybe (S.Set FieldName)\n    -- ^ &columns parameter and payload\n    , qsSelect         :: [Tree SelectItem]\n    -- ^ &select parameter used to shape the response\n    , qsFilters        :: [(EmbedPath, Filter)]\n    -- ^ Filters on the result from e.g. &id=e.10\n    , qsFiltersRoot    :: [Filter]\n    -- ^ Subset of the filters that apply on the root table. These are used on UPDATE/DELETE.\n    , qsFiltersNotRoot :: [(EmbedPath, Filter)]\n    -- ^ Subset of the filters that do not apply on the root table\n    , qsFilterFields   :: S.Set FieldName\n    -- ^ Set of fields that filters apply to\n    , qsOnConflict     :: Maybe [FieldName]\n    -- ^ &on_conflict parameter used to upsert on specific unique keys\n    }\n\n-- |\n-- Parse query parameters from a query string like \"id=eq.1&select=name\".\n--\n-- The canonical representation of the query string has parameters sorted alphabetically:\n--\n-- >>> qsCanonical <$> parse True \"a=1&c=3&b=2&d\"\n-- Right \"a=1&b=2&c=3&d=\"\n--\n-- 'select' is a reserved parameter that selects the fields to be returned:\n--\n-- >>> qsSelect <$> parse False \"select=name,location\"\n-- Right [Node {rootLabel = SelectField {selField = (\"name\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = (\"location\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]\n--\n-- Filters are parameters whose value contains an operator, separated by a '.' from its value:\n--\n-- >>> qsFilters <$> parse False \"a.b=eq.0\"\n-- Right [([\"a\"],Filter {field = (\"b\",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing \"0\")})]\n--\n-- If the operator specified in a filter does not exist, parsing the query string fails:\n--\n-- >>> qsFilters <$> parse False \"a.b=noop.0\"\n-- Left (QPError \"\\\"failed to parse filter (noop.0)\\\" (line 1, column 1)\" \"unexpected \\\"o\\\" expecting \\\"not\\\" or operator (eq, gt, ...)\")\nparse :: Bool -> ByteString -> Either QPError QueryParams\nparse isRpcRead qs = do\n  rOrd                      <- pRequestOrder `traverse` order\n  rLogic                    <- pRequestLogicTree `traverse` logic\n  rCols                     <- pRequestColumns columns\n  rSel                      <- pRequestSelect select\n  (rFlts, params)           <- L.partition hasOp <$> pRequestFilter isRpcRead `traverse` filters\n  (rFltsRoot, rFltsNotRoot) <- pure $ L.partition hasRootFilter rFlts\n  rOnConflict               <- pRequestOnConflict `traverse` onConflict\n\n  let rFltsFields           = S.fromList (fst <$> filters)\n      params'               = mapMaybe (\\case {(_, Filter (fld, _) (NoOpExpr v)) -> Just (fld,v); _ -> Nothing}) params\n      rFltsRoot'            = snd <$> rFltsRoot\n\n  return $ QueryParams canonical params' ranges rOrd rLogic rCols rSel rFlts rFltsRoot' rFltsNotRoot rFltsFields rOnConflict\n  where\n    hasRootFilter, hasOp :: (EmbedPath, Filter) -> Bool\n    hasRootFilter ([], _) = True\n    hasRootFilter _       = False\n    hasOp (_, Filter (_, _) (NoOpExpr _)) = False\n    hasOp _                               = True\n\n    logic = filter (endingIn [\"and\", \"or\"] . fst) nonemptyParams\n    select = fromMaybe \"*\" $ lookupParam \"select\"\n    onConflict = lookupParam \"on_conflict\"\n    columns = lookupParam \"columns\"\n    order = filter (endingIn [\"order\"] . fst) nonemptyParams\n    limits = filter (endingIn [\"limit\"] . fst) nonemptyParams\n    -- Replace .offset ending with .limit to be able to match those params later in a map\n    offsets = first (replaceLast \"limit\") <$> filter (endingIn [\"offset\"] . fst) nonemptyParams\n    lookupParam :: Text -> Maybe Text\n    lookupParam needle = toS <$> join (L.lookup needle qParams)\n    nonemptyParams = mapMaybe (\\(k, v) -> (k,) <$> v) qParams\n\n    qString = HTTP.parseQueryReplacePlus True qs\n\n    qParams = [(T.decodeUtf8 k, T.decodeUtf8 <$> v)|(k,v) <- qString]\n\n    canonical =\n      BS.pack $ HTTP.urlEncodeVars\n        . L.sortOn fst\n        . map (join (***) BS.unpack . second (fromMaybe mempty))\n        $ qString\n\n    endingIn:: [Text] -> Text -> Bool\n    endingIn xx key = lastWord `elem` xx\n      where lastWord = L.last $ T.split (== '.') key\n\n    filters = filter (isFilter . fst) nonemptyParams\n    isFilter k = not (endingIn reservedEmbeddable k) && notElem k reserved\n    reserved = [\"select\", \"columns\", \"on_conflict\"]\n    reservedEmbeddable = [\"order\", \"limit\", \"offset\", \"and\", \"or\"]\n\n    replaceLast x s = T.intercalate \".\" $ L.init (T.split (=='.') s) <> [x]\n\n    ranges :: HM.HashMap Text (Range Integer)\n    ranges = HM.unionWith f limitParams offsetParams\n      where\n        f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)\n          where\n            l = fromMaybe 0 $ rangeLimit rl\n            o = rangeOffset ro\n\n        limitParams =\n          HM.fromList [(k, restrictRange (readMaybe v) allRange) | (k,v) <- limits]\n\n        offsetParams =\n          HM.fromList [(k, maybe allRange rangeGeq (readMaybe v)) | (k,v) <- offsets]\n\nsimpleOperator :: Parser SimpleOperator\nsimpleOperator =\n  try (string \"neq\" $> OpNotEqual) <|>\n  try (string \"cs\" $> OpContains) <|>\n  try (string \"cd\" $> OpContained) <|>\n  try (string \"ov\" $> OpOverlap) <|>\n  try (string \"sl\" $> OpStrictlyLeft) <|>\n  try (string \"sr\" $> OpStrictlyRight) <|>\n  try (string \"nxr\" $> OpNotExtendsRight) <|>\n  try (string \"nxl\" $> OpNotExtendsLeft) <|>\n  try (string \"adj\" $> OpAdjacent) <?>\n  \"unknown single value operator\"\n\nquantOperator :: Parser QuantOperator\nquantOperator =\n  try (string \"eq\" $> OpEqual) <|>\n  try (string \"gte\" $> OpGreaterThanEqual) <|>\n  try (string \"gt\" $> OpGreaterThan) <|>\n  try (string \"lte\" $> OpLessThanEqual) <|>\n  try (string \"lt\" $> OpLessThan) <|>\n  try (string \"like\" $> OpLike) <|>\n  try (string \"ilike\" $> OpILike) <|>\n  try (string \"match\" $> OpMatch) <|>\n  try (string \"imatch\" $> OpIMatch) <?>\n  \"unknown single value operator\"\n\npRequestSelect :: Text -> Either QPError [Tree SelectItem]\npRequestSelect selStr =\n  mapError $ P.parse pFieldForest (\"failed to parse select parameter (\" <> toS selStr <> \")\") (toS selStr)\n\npRequestOnConflict :: Text -> Either QPError [FieldName]\npRequestOnConflict oncStr =\n  mapError $ P.parse pColumns (\"failed to parse on_conflict parameter (\" <> toS oncStr <> \")\") (toS oncStr)\n\n-- |\n-- Parse `id=eq.1`(id, eq.1) into (EmbedPath, Filter)\n--\n-- >>> pRequestFilter False (\"id\", \"eq.1\")\n-- Right ([],Filter {field = (\"id\",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing \"1\")})\n--\n-- >>> pRequestFilter False (\"id\", \"val\")\n-- Left (QPError \"\\\"failed to parse filter (val)\\\" (line 1, column 1)\" \"unexpected \\\"v\\\" expecting \\\"not\\\" or operator (eq, gt, ...)\")\n--\n-- >>> pRequestFilter True (\"id\", \"val\")\n-- Right ([],Filter {field = (\"id\",[]), opExpr = NoOpExpr \"val\"})\npRequestFilter :: Bool -> (Text, Text) -> Either QPError (EmbedPath, Filter)\npRequestFilter isRpcRead (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)\n  where\n    treePath = P.parse pTreePath (\"failed to parse tree path (\" ++ toS k ++ \")\") $ toS k\n    oper = P.parse parseFlt (\"failed to parse filter (\" ++ toS v ++ \")\") $ toS v\n    parseFlt = if isRpcRead\n      then pOpExpr pSingleVal <|> pure (NoOpExpr v)\n      else pOpExpr pSingleVal\n    path = fst <$> treePath\n    fld = snd <$> treePath\n\npRequestOrder :: (Text, Text) -> Either QPError (EmbedPath, [OrderTerm])\npRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'\n  where\n    treePath = P.parse pTreePath (\"failed to parse tree path (\" ++ toS k ++ \")\") $ toS k\n    path = fst <$> treePath\n    ord' = P.parse pOrder (\"failed to parse order (\" ++ toS v ++ \")\") $ toS v\n\npRequestRange :: (Text, NonnegRange) -> Either QPError (EmbedPath, NonnegRange)\npRequestRange (k, v) = mapError $ (,) <$> path <*> pure v\n  where\n    treePath = P.parse pTreePath (\"failed to parse tree path (\" ++ toS k ++ \")\") $ toS k\n    path = fst <$> treePath\n\npRequestLogicTree :: (Text, Text) -> Either QPError (EmbedPath, LogicTree)\npRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree\n  where\n    path = P.parse pLogicPath (\"failed to parse logic path (\" ++ toS k ++ \")\") $ toS k\n    embedPath = fst <$> path\n    logicTree = do\n      op <- snd <$> path\n      -- Concat op and v to make pLogicTree argument regular,\n      -- in the form of \"?and=and(.. , ..)\" instead of \"?and=(.. , ..)\"\n      P.parse pLogicTree (\"failed to parse logic tree (\" ++ toS v ++ \")\") $ toS (op <> v)\n\npRequestColumns :: Maybe Text -> Either QPError (Maybe (S.Set FieldName))\npRequestColumns colStr =\n  case colStr of\n    Just str ->\n      mapError $ Just . S.fromList <$> P.parse pColumns (\"failed to parse columns parameter (\" <> toS str <> \")\") (toS str)\n    _ -> Right Nothing\n\nws :: Parser Text\nws = toS <$> many (oneOf \" \\t\")\n\nlexeme :: Parser a -> Parser a\nlexeme p = ws *> p <* ws\n\npTreePath :: Parser (EmbedPath, Field)\npTreePath = do\n  p <- pFieldName `sepBy1` pDelimiter\n  jp <- P.option [] pJsonPath\n  return (init p, (last p, jp))\n\n-- |\n-- Parse select= into a Forest of SelectItems\n--\n-- >>> P.parse pFieldForest \"\" \"id\"\n-- Right [Node {rootLabel = SelectField {selField = (\"id\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]\n--\n-- >>> P.parse pFieldForest \"\" \"client(id)\"\n-- Right [Node {rootLabel = SelectRelation {selRelation = \"client\", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = (\"id\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]\n--\n-- >>> P.parse pFieldForest \"\" \"*,client(*,nested(*))\"\n-- Right [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = \"client\", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = \"nested\", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]\n--\n-- >>> P.parse pFieldForest \"\" \"*,...client(*),other(*)\"\n-- Right [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = \"client\", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = \"other\", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]\n--\n-- >>> P.parse pFieldForest \"\" \"\"\n-- Right []\n--\n-- >>> P.parse pFieldForest \"\" \"id,clients(name[])\"\n-- Left (line 1, column 16):\n-- unexpected '['\n-- expecting letter, digit, \"-\", \"->>\", \"->\", \"::\", \".\", \")\", \",\" or end of input\n--\n-- >>> P.parse pFieldForest \"\" \"data->>-78xy\"\n-- Left (line 1, column 11):\n-- unexpected 'x'\n-- expecting digit, \"->\", \"::\", \".\", \",\" or end of input\npFieldForest :: Parser [Tree SelectItem]\npFieldForest = pFieldTree `sepBy` lexeme (char ',')\n  where\n    pFieldTree =  Node <$> try pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest <|>\n                  Node <$> try pRelationSelect       <*> between (char '(') (char ')') pFieldForest <|>\n                  Node <$> pFieldSelect <*> pure []\n\n-- |\n-- Parse field names\n--\n-- >>> P.parse pFieldName \"\" \"identifier\"\n-- Right \"identifier\"\n--\n-- >>> P.parse pFieldName \"\" \"identifier with spaces\"\n-- Right \"identifier with spaces\"\n--\n-- >>> P.parse pFieldName \"\" \"identifier-with-dashes\"\n-- Right \"identifier-with-dashes\"\n--\n-- >>> P.parse pFieldName \"\" \"123\"\n-- Right \"123\"\n--\n-- >>> P.parse pFieldName \"\" \"_\"\n-- Right \"_\"\n--\n-- >>> P.parse pFieldName \"\" \"$\"\n-- Right \"$\"\n--\n-- >>> P.parse pFieldName \"\" \":\"\n-- Left (line 1, column 1):\n-- unexpected \":\"\n-- expecting field name (* or [a..z0..9_$])\n--\n-- >>> P.parse pFieldName \"\" \"\\\":\\\"\"\n-- Right \":\"\n--\n-- >>> P.parse pFieldName \"\" \" no leading or trailing spaces \"\n-- Right \"no leading or trailing spaces\"\n--\n-- >>> P.parse pFieldName \"\" \"\\\" leading and trailing spaces \\\"\"\n-- Right \" leading and trailing spaces \"\npFieldName :: Parser Text\npFieldName =\n  pQuotedValue <|>\n  sepByDash pIdentifier <?>\n  \"field name (* or [a..z0..9_$])\"\n\nsepByDash :: Parser Text -> Parser Text\nsepByDash fieldIdent =\n  T.intercalate \"-\" . map toS <$> (fieldIdent `sepBy1` dash)\n  where\n    isDash :: GenParser Char st ()\n    isDash = try ( char '-' >> notFollowedBy (char '>') )\n    dash :: Parser Char\n    dash = isDash $> '-'\n\n-- |\n-- Parse json operators in select, order and filters\n--\n-- >>> P.parse pJsonPath \"\" \"->text\"\n-- Right [JArrow {jOp = JKey {jVal = \"text\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->!@#$%^&*_a\"\n-- Right [JArrow {jOp = JKey {jVal = \"!@#$%^&*_a\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->1\"\n-- Right [JArrow {jOp = JIdx {jVal = \"+1\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->>text\"\n-- Right [J2Arrow {jOp = JKey {jVal = \"text\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->>!@#$%^&*_a\"\n-- Right [J2Arrow {jOp = JKey {jVal = \"!@#$%^&*_a\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->>1\"\n-- Right [J2Arrow {jOp = JIdx {jVal = \"+1\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->0,other\"\n-- Right [JArrow {jOp = JIdx {jVal = \"+0\"}}]\n--\n-- >>> P.parse pJsonPath \"\" \"->0.desc\"\n-- Right [JArrow {jOp = JIdx {jVal = \"+0\"}}]\n--\n-- Fails on badly formed negatives\n--\n-- >>> P.parse pJsonPath \"\" \"->>-78xy\"\n-- Left (line 1, column 7):\n-- unexpected 'x'\n-- expecting digit, \"->\", \"::\", \".\", \",\" or end of input\n--\n-- >>> P.parse pJsonPath \"\" \"->>--34\"\n-- Left (line 1, column 5):\n-- unexpected \"-\"\n-- expecting digit\n--\n-- >>> P.parse pJsonPath \"\" \"->>-xy-4\"\n-- Left (line 1, column 5):\n-- unexpected \"x\"\n-- expecting digit\npJsonPath :: Parser JsonPath\npJsonPath = many pJsonOperation\n  where\n    pJsonOperation :: Parser JsonOperation\n    pJsonOperation = pJsonArrow <*> pJsonOperand\n\n    pJsonArrow   =\n      try (string \"->>\" $> J2Arrow) <|>\n      try (string \"->\" $> JArrow)\n\n    pJsonOperand =\n      let pJKey = JKey . toS <$> pJsonKeyName\n          pJIdx = JIdx . toS <$> ((:) <$> P.option '+' (char '-') <*> many1 digit) <* pEnd\n          pEnd = try (void $ lookAhead (string \"->\")) <|>\n                 try (void $ lookAhead (string \"::\")) <|>\n                 try (void $ lookAhead (string \".\")) <|>\n                 try (void $ lookAhead (string \",\")) <|>\n                 try eof in\n      try pJIdx <|> try pJKey\n\npJsonKeyName :: Parser Text\npJsonKeyName =\n  pQuotedValue <|>\n  sepByDash pJsonKeyIdentifier <?>\n  \"any non reserved character different from: .,>()\"\n\npJsonKeyIdentifier :: Parser Text\npJsonKeyIdentifier = T.strip . toS <$> many1 (noneOf \"(-:.,>)\")\n\npField :: Parser Field\npField = lexeme $ (,) <$> pFieldName <*> P.option [] pJsonPath\n\naliasSeparator :: Parser ()\naliasSeparator = char ':' >> notFollowedBy (char ':')\n\n-- |\n-- Parse regular fields in select\n--\n-- >>> P.parse pRelationSelect \"\" \"rel(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing})\n--\n-- >>> P.parse pRelationSelect \"\" \"alias:rel(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Just \"alias\", selHint = Nothing, selJoinType = Nothing})\n--\n-- >>> P.parse pRelationSelect \"\" \"rel!hint(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Nothing, selHint = Just \"hint\", selJoinType = Nothing})\n--\n-- >>> P.parse pRelationSelect \"\" \"rel!inner(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Nothing, selHint = Nothing, selJoinType = Just JTInner})\n--\n-- >>> P.parse pRelationSelect \"\" \"rel!hint!inner(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Nothing, selHint = Just \"hint\", selJoinType = Just JTInner})\n--\n-- >>> P.parse pRelationSelect \"\" \"alias:rel!inner!hint(*)\"\n-- Right (SelectRelation {selRelation = \"rel\", selAlias = Just \"alias\", selHint = Just \"hint\", selJoinType = Just JTInner})\n--\n-- >>> P.parse pRelationSelect \"\" \"rel->jsonpath(*)\"\n-- Left (line 1, column 6):\n-- unexpected '>'\n--\n-- >>> P.parse pRelationSelect \"\" \"rel->jsonpath!hint(*)\"\n-- Left (line 1, column 6):\n-- unexpected '>'\npRelationSelect :: Parser SelectItem\npRelationSelect = lexeme $ do\n    alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )\n    name <- pFieldName\n    guard (name /= \"count\")\n    (hint, jType) <- pEmbedParams\n    try (void $ lookAhead (string \"(\"))\n    return $ SelectRelation name alias hint jType\n\n\n-- |\n-- Parse regular fields in select\n--\n-- >>> P.parse pFieldSelect \"\" \"name\"\n-- Right (SelectField {selField = (\"name\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})\n--\n-- >>> P.parse pFieldSelect \"\" \"name->jsonpath\"\n-- Right (SelectField {selField = (\"name\",[JArrow {jOp = JKey {jVal = \"jsonpath\"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})\n--\n-- >>> P.parse pFieldSelect \"\" \"name::cast\"\n-- Right (SelectField {selField = (\"name\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just \"cast\", selAlias = Nothing})\n--\n-- >>> P.parse pFieldSelect \"\" \"alias:name\"\n-- Right (SelectField {selField = (\"name\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Just \"alias\"})\n--\n-- >>> P.parse pFieldSelect \"\" \"alias:name->jsonpath::cast\"\n-- Right (SelectField {selField = (\"name\",[JArrow {jOp = JKey {jVal = \"jsonpath\"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just \"cast\", selAlias = Just \"alias\"})\n--\n-- >>> P.parse pFieldSelect \"\" \"alias:name->!@#$%^&*_a::cast\"\n-- Right (SelectField {selField = (\"name\",[JArrow {jOp = JKey {jVal = \"!@#$%^&*_a\"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just \"cast\", selAlias = Just \"alias\"})\n--\n-- >>> P.parse pFieldSelect \"\" \"*\"\n-- Right (SelectField {selField = (\"*\",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})\n--\n-- >>> P.parse pFieldSelect \"\" \"name!hint\"\n-- Left (line 1, column 5):\n-- unexpected '!'\n-- expecting letter, digit, \"-\", \"->>\", \"->\", \"::\", \".\", \")\", \",\" or end of input\n--\n-- >>> P.parse pFieldSelect \"\" \"*!hint\"\n-- Left (line 1, column 2):\n-- unexpected '!'\n-- expecting \")\", \",\" or end of input\n--\n-- >>> P.parse pFieldSelect \"\" \"name::\"\n-- Left (line 1, column 7):\n-- unexpected end of input\n-- expecting letter or digit\npFieldSelect :: Parser SelectItem\npFieldSelect = lexeme $ try (do\n    s <- pStar\n    pEnd\n    return $ SelectField (s, []) Nothing Nothing Nothing Nothing)\n  <|> try (do\n    alias    <- optionMaybe ( try(pFieldName <* aliasSeparator) )\n    _        <- string \"count()\"\n    aggCast' <- optionMaybe (string \"::\" *> pIdentifier)\n    pEnd\n    return $ SelectField (\"*\", []) (Just Count) (toS <$> aggCast') Nothing alias)\n  <|> do\n    alias    <- optionMaybe ( try(pFieldName <* aliasSeparator) )\n    fld      <- pField\n    cast'    <- optionMaybe (string \"::\" *> pIdentifier)\n    agg      <- optionMaybe (try (char '.' *> pAggregation <* string \"()\"))\n    aggCast' <- optionMaybe (string \"::\" *> pIdentifier)\n    pEnd\n    return $ SelectField fld agg (toS <$> aggCast') (toS <$> cast') alias\n  where\n    pEnd = try (void $ lookAhead (string \")\")) <|>\n           try (void $ lookAhead (string \",\")) <|>\n           try eof\n    pStar = string \"*\" $> \"*\"\n    pAggregation = choice\n      [ string \"sum\"   $> Sum\n      , string \"avg\"   $> Avg\n      , string \"count\" $> Count\n      -- Using 'try' for \"min\" and \"max\" to allow backtracking.\n      -- This is necessary because both start with the same character 'm',\n      -- and without 'try', a partial match on \"max\" would prevent \"min\" from being tried.\n      , try (string \"max\") $> Max\n      , try (string \"min\") $> Min\n      ]\n\n\n-- |\n-- Parse spread relations in select\n--\n-- >>> P.parse pSpreadRelationSelect \"\" \"...rel(*)\"\n-- Right (SpreadRelation {selRelation = \"rel\", selHint = Nothing, selJoinType = Nothing})\n--\n-- >>> P.parse pSpreadRelationSelect \"\" \"...rel!hint!inner(*)\"\n-- Right (SpreadRelation {selRelation = \"rel\", selHint = Just \"hint\", selJoinType = Just JTInner})\n--\n-- >>> P.parse pSpreadRelationSelect \"\" \"rel(*)\"\n-- Left (line 1, column 1):\n-- unexpected \"r\"\n-- expecting \"...\"\n--\n-- >>> P.parse pSpreadRelationSelect \"\" \"alias:...rel(*)\"\n-- Left (line 1, column 1):\n-- unexpected \"a\"\n-- expecting \"...\"\n--\n-- >>> P.parse pSpreadRelationSelect \"\" \"...rel->jsonpath(*)\"\n-- Left (line 1, column 9):\n-- unexpected '>'\npSpreadRelationSelect :: Parser SelectItem\npSpreadRelationSelect = lexeme $ do\n    name <- string \"...\" >> pFieldName\n    (hint, jType) <- pEmbedParams\n    try (void $ lookAhead (string \"(\"))\n    return $ SpreadRelation name hint jType\n\npEmbedParams :: Parser (Maybe Hint, Maybe JoinType)\npEmbedParams = do\n  prm1 <- optionMaybe pEmbedParam\n  prm2 <- optionMaybe pEmbedParam\n  return (embedParamHint prm1 <|> embedParamHint prm2, embedParamJoin prm1 <|> embedParamJoin prm2)\n  where\n    pEmbedParam :: Parser EmbedParam\n    pEmbedParam =\n      char '!' *> (\n        try (string \"left\"  $> EPJoinType JTLeft)  <|>\n        try (string \"inner\" $> EPJoinType JTInner) <|>\n        try (EPHint <$> pFieldName))\n    embedParamHint prm = case prm of\n      Just (EPHint hint) -> Just hint\n      _                  -> Nothing\n    embedParamJoin prm = case prm of\n      Just (EPJoinType jt) -> Just jt\n      _                    -> Nothing\n\n-- |\n-- Parse operator expression used in horizontal filtering\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"fts().value\"\n-- Left (line 1, column 5):\n-- unexpected \")\"\n-- expecting operator (eq, gt, ...)\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"eq(any).value\"\n-- Right (OpExpr False (OpQuant OpEqual (Just QuantAny) \"value\"))\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"eq(all).value\"\n-- Right (OpExpr False (OpQuant OpEqual (Just QuantAll) \"value\"))\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"not.eq(all).value\"\n-- Right (OpExpr True (OpQuant OpEqual (Just QuantAll) \"value\"))\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"eq().value\"\n-- Left (line 1, column 4):\n-- unexpected \")\"\n-- expecting operator (eq, gt, ...)\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"is().value\"\n-- Left (line 1, column 3):\n-- unexpected \"(\"\n-- expecting operator (eq, gt, ...)\n--\n-- >>> P.parse (pOpExpr pSingleVal) \"\" \"in().value\"\n-- Left (line 1, column 3):\n-- unexpected \"(\"\n-- expecting operator (eq, gt, ...)\npOpExpr :: Parser SingleVal -> Parser OpExpr\npOpExpr pSVal = do\n  boolExpr <- try (string \"not\" *> pDelimiter $> True) <|> pure False\n  OpExpr boolExpr <$> pOperation\n  where\n    pOperation :: Parser Operation\n    pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> \"operator (eq, gt, ...)\"\n\n    pIn = In <$> (try (string \"in\" *> pDelimiter) *> pListVal)\n    pIs = Is <$> (try (string \"is\" *> pDelimiter) *> pIsVal)\n\n    pIsDist = IsDistinctFrom <$> (try (string \"isdistinct\" *> pDelimiter) *> pSVal)\n\n    pSimpleOp = do\n      op <- simpleOperator\n      pDelimiter *> (Op op <$> pSVal)\n\n    pQuantOp = do\n      op <- quantOperator\n      quant <- optionMaybe $ try (between (char '(') (char ')') (try (string \"any\" $> QuantAny) <|> string \"all\" $> QuantAll))\n      pDelimiter *> (OpQuant op quant <$> pSVal)\n\n    pIsVal =  try (ciString \"null\"     $> IsNull)\n          <|> try (ciString \"not_null\" $> IsNotNull)\n          <|> try (ciString \"true\"     $> IsTriTrue)\n          <|> try (ciString \"false\"    $> IsTriFalse)\n          <|> try (ciString \"unknown\"  $> IsTriUnknown)\n          <?> \"isVal: (null, not_null, true, false, unknown)\"\n\n    pFts = do\n      op <-  try (string \"fts\"   $> FilterFts)\n         <|> try (string \"plfts\" $> FilterFtsPlain)\n         <|> try (string \"phfts\" $> FilterFtsPhrase)\n         <|> try (string \"wfts\"  $> FilterFtsWebsearch)\n\n      lang <- optionMaybe $ try (between (char '(') (char ')') pIdentifier)\n      pDelimiter >> Fts op (toS <$> lang) <$> pSVal\n\n    -- case insensitive char and string\n    ciChar :: Char -> GenParser Char state Char\n    ciChar c = char c <|> char (toUpper c)\n    ciString :: [Char] -> GenParser Char state [Char]\n    ciString = traverse ciChar\n\npSingleVal :: Parser SingleVal\npSingleVal = toS <$> many anyChar\n\npListVal :: Parser ListVal\npListVal = lexeme (char '(') *> pListElement `sepBy1` char ',' <* lexeme (char ')')\n\npListElement :: Parser Text\npListElement = try (pQuotedValue <* notFollowedBy (noneOf \",)\")) <|> (toS <$> many (noneOf \",)\"))\n\npQuotedValue :: Parser Text\npQuotedValue = toS <$> (char '\"' *> many pCharsOrSlashed <* char '\"')\n  where\n    pCharsOrSlashed = noneOf \"\\\\\\\"\" <|> (char '\\\\' *> anyChar)\n\npDelimiter :: Parser Char\npDelimiter = char '.' <?> \"delimiter (.)\"\n\n-- |\n-- Parses the elements in the order query parameter\n--\n-- >>> P.parse pOrder \"\" \"name.desc.nullsfirst\"\n-- Right [OrderTerm {otTerm = (\"name\",[]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]\n--\n-- >>> P.parse pOrder \"\" \"json_col->key.asc.nullslast\"\n-- Right [OrderTerm {otTerm = (\"json_col\",[JArrow {jOp = JKey {jVal = \"key\"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]\n--\n-- >>> P.parse pOrder \"\" \"json_col->!@#$%^&*_a.asc.nullslast\"\n-- Right [OrderTerm {otTerm = (\"json_col\",[JArrow {jOp = JKey {jVal = \"!@#$%^&*_a\"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]\n--\n-- >>> P.parse pOrder \"\" \"clients(json_col->key).desc.nullsfirst\"\n-- Right [OrderRelationTerm {otRelation = \"clients\", otRelTerm = (\"json_col\",[JArrow {jOp = JKey {jVal = \"key\"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]\n--\n-- >>> P.parse pOrder \"\" \"clients(json_col->!@#$%^&*_a).desc.nullsfirst\"\n-- Right [OrderRelationTerm {otRelation = \"clients\", otRelTerm = (\"json_col\",[JArrow {jOp = JKey {jVal = \"!@#$%^&*_a\"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]\n--\n-- >>> P.parse pOrder \"\" \"clients(name,id)\"\n-- Left (line 1, column 8):\n-- unexpected '('\n-- expecting letter, digit, \"-\", \"->>\", \"->\", delimiter (.), \",\" or end of input\n--\n-- >>> P.parse pOrder \"\" \"name,clients(name),id\"\n-- Right [OrderTerm {otTerm = (\"name\",[]), otDirection = Nothing, otNullOrder = Nothing},OrderRelationTerm {otRelation = \"clients\", otRelTerm = (\"name\",[]), otDirection = Nothing, otNullOrder = Nothing},OrderTerm {otTerm = (\"id\",[]), otDirection = Nothing, otNullOrder = Nothing}]\n--\n-- >>> P.parse pOrder \"\" \"id.ac\"\n-- Left (line 1, column 4):\n-- unexpected \"c\"\n-- expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"\n--\n-- >>> P.parse pOrder \"\" \"id.descc\"\n-- Left (line 1, column 8):\n-- unexpected 'c'\n-- expecting delimiter (.), \",\" or end of input\n--\n-- >>> P.parse pOrder \"\" \"id.nulsfist\"\n-- Left (line 1, column 4):\n-- unexpected \"n\"\n-- expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"\n--\n-- >>> P.parse pOrder \"\" \"id.nullslasttt\"\n-- Left (line 1, column 13):\n-- unexpected 't'\n-- expecting \",\" or end of input\n--\n-- >>> P.parse pOrder \"\" \"id.smth34\"\n-- Left (line 1, column 4):\n-- unexpected \"s\"\n-- expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"\n--\n-- >>> P.parse pOrder \"\" \"id.asc.nlsfst\"\n-- Left (line 1, column 8):\n-- unexpected \"l\"\n-- expecting \"nullsfirst\" or \"nullslast\"\n--\n-- >>> P.parse pOrder \"\" \"id.asc.nullslasttt\"\n-- Left (line 1, column 17):\n-- unexpected 't'\n-- expecting \",\" or end of input\n--\n-- >>> P.parse pOrder \"\" \"id.asc.smth34\"\n-- Left (line 1, column 8):\n-- unexpected \"s\"\n-- expecting \"nullsfirst\" or \"nullslast\"\npOrder :: Parser [OrderTerm]\npOrder = lexeme (try pOrderRelationTerm <|> pOrderTerm) `sepBy1` char ','\n  where\n    pOrderTerm = do\n      fld <- pField\n      dir <- optionMaybe pOrdDir\n      nls <- optionMaybe pNulls <* pEnd <|>\n             pEnd $> Nothing\n      return $ OrderTerm fld dir nls\n\n    pOrderRelationTerm = do\n      nam <- pFieldName\n      fld <- between (char '(') (char ')') pField\n      dir <- optionMaybe pOrdDir\n      nls <- optionMaybe pNulls <* pEnd <|> pEnd $> Nothing\n      return $ OrderRelationTerm nam fld dir nls\n\n    pNulls :: Parser OrderNulls\n    pNulls = try (pDelimiter *> string \"nullsfirst\" $> OrderNullsFirst) <|>\n             try (pDelimiter *> string \"nullslast\"  $> OrderNullsLast)\n\n    pOrdDir :: Parser OrderDirection\n    pOrdDir = try (pDelimiter *> string \"asc\" $> OrderAsc) <|>\n              try (pDelimiter *> string \"desc\" $> OrderDesc)\n\n    pEnd = try (void $ lookAhead (char ',')) <|> try eof\n\n-- |\n-- Parses the elements inside or/and\n--\n-- >>> P.parse pLogicTree \"\" \"or()\"\n-- Left (line 1, column 4):\n-- unexpected \")\"\n-- expecting field name (* or [a..z0..9_$]), negation operator (not) or logic operator (and, or)\n--\n-- >>> P.parse pLogicTree \"\" \"or(id.in.1,2,id.eq.3)\"\n-- Left (line 1, column 10):\n-- unexpected \"1\"\n-- expecting \"(\"\n--\n-- >>> P.parse pLogicTree \"\" \"or)(\"\n-- Left (line 1, column 3):\n-- unexpected \")\"\n-- expecting \"(\"\n--\n-- >>> P.parse pLogicTree \"\" \"and(ord(id.eq.1,id.eq.1),id.eq.2)\"\n-- Left (line 1, column 7):\n-- unexpected \"d\"\n-- expecting \"(\"\n--\n-- >>> P.parse pLogicTree \"\" \"or(id.eq.1,not.xor(id.eq.2,id.eq.3))\"\n-- Left (line 1, column 16):\n-- unexpected \"x\"\n-- expecting logic operator (and, or)\npLogicTree :: Parser LogicTree\npLogicTree = Stmnt <$> try pLogicFilter\n             <|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree `sepBy1` lexeme (char ',') <* lexeme (char ')'))\n  where\n    pLogicFilter :: Parser Filter\n    pLogicFilter = Filter <$> pField <* pDelimiter <*> pOpExpr pLogicSingleVal\n    pNot :: Parser Bool\n    pNot = try (string \"not\" *> pDelimiter $> True)\n           <|> pure False\n           <?> \"negation operator (not)\"\n    pLogicOp :: Parser LogicOperator\n    pLogicOp = try (string \"and\" $> And)\n               <|> string \"or\" $> Or\n               <?> \"logic operator (and, or)\"\n\npLogicSingleVal :: Parser SingleVal\npLogicSingleVal = try (pQuotedValue <* notFollowedBy (noneOf \",)\")) <|> try pPgArray <|> (toS <$> many (noneOf \",)\"))\n  where\n    pPgArray :: Parser Text\n    pPgArray =  do\n      a <- string \"{\"\n      b <- many (noneOf \"{}\")\n      c <- string \"}\"\n      pure (toS $ a ++ b ++ c)\n\npLogicPath :: Parser (EmbedPath, Text)\npLogicPath = do\n  path <- pFieldName `sepBy1` pDelimiter\n  let op = last path\n      notOp = \"not.\" <> op\n  return (filter (/= \"not\") (init path), if \"not\" `elem` path then notOp else op)\n\npColumns :: Parser [FieldName]\npColumns = pFieldName `sepBy1` lexeme (char ',')\n\npIdentifier :: Parser Text\npIdentifier = T.strip . toS <$> many1 pIdentifierChar\n\npIdentifierChar :: Parser Char\npIdentifierChar = letter <|> digit <|> oneOf \"_ $\"\n\nmapError :: Either ParseError a -> Either QPError a\nmapError = mapLeft translateError\n  where\n    translateError e =\n      QPError message details\n      where\n        message = show $ errorPos e\n        details = T.strip $ T.replace \"\\n\" \" \" $ toS\n           $ showErrorMessages \"or\" \"unknown parse error\" \"expecting\" \"unexpected\" \"end of input\" (errorMessages e)\n"
  },
  {
    "path": "src/PostgREST/ApiRequest/Types.hs",
    "content": "{-# LANGUAGE DuplicateRecordFields #-}\nmodule PostgREST.ApiRequest.Types\n  ( AggregateFunction(..)\n  , Alias\n  , Cast\n  , Depth\n  , EmbedParam(..)\n  , EmbedPath\n  , Field\n  , Filter(..)\n  , Hint\n  , JoinType(..)\n  , JsonOperand(..)\n  , JsonOperation(..)\n  , JsonPath\n  , Language\n  , ListVal\n  , LogicOperator(..)\n  , LogicTree(..)\n  , NodeName\n  , OpExpr(..)\n  , Operation (..)\n  , OpQuantifier(..)\n  , OrderDirection(..)\n  , OrderNulls(..)\n  , OrderTerm(..)\n  , SingleVal\n  , IsVal(..)\n  , SimpleOperator(..)\n  , QuantOperator(..)\n  , FtsOperator(..)\n  , SelectItem(..)\n  , Payload (..)\n  , InvokeMethod (..)\n  , Mutation (..)\n  , Resource (..)\n  , DbAction (..)\n  , Action (..)\n  , RequestBody\n  ) where\n\nimport qualified Data.ByteString.Lazy as LBS\nimport qualified Data.Set             as S\n\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier (..),\n                                          Schema)\n\nimport Protolude\n\ndata InvokeMethod = Inv | InvRead Bool\n  deriving Eq\n\ndata Mutation\n  = MutationCreate\n  | MutationDelete\n  | MutationSingleUpsert\n  | MutationUpdate\n  deriving Eq\n\ndata Resource\n  = ResourceRelation Text\n  | ResourceRoutine Text\n  | ResourceSchema\n\ndata DbAction\n  = ActRelationRead {dbActQi :: QualifiedIdentifier, actHeadersOnly :: Bool}\n  | ActRelationMut  {dbActQi :: QualifiedIdentifier, actMutation :: Mutation}\n  | ActRoutine      {dbActQi :: QualifiedIdentifier, actInvMethod :: InvokeMethod}\n  | ActSchemaRead   Schema Bool\n\ndata Action\n  = ActDb           DbAction\n  | ActRelationInfo QualifiedIdentifier\n  | ActRoutineInfo  QualifiedIdentifier InvokeMethod\n  | ActSchemaInfo\n\ntype RequestBody = LBS.ByteString\n\ndata Payload\n  = ProcessedJSON -- ^ Cached attributes of a JSON payload\n      { payRaw  :: LBS.ByteString\n      -- ^ This is the raw ByteString that comes from the request body.  We\n      -- cache this instead of an Aeson Value because it was detected that for\n      -- large payloads the encoding had high memory usage, see\n      -- https://github.com/PostgREST/postgrest/pull/1005 for more details\n      , payKeys :: S.Set Text\n      -- ^ Keys of the object or if it's an array these keys are guaranteed to\n      -- be the same across all its objects\n      }\n  | ProcessedUrlEncoded { payArray  :: [(Text, Text)], payKeys :: S.Set Text }\n  | RawJSON { payRaw  :: LBS.ByteString }\n  | RawPay  { payRaw  :: LBS.ByteString }\n\n\n-- | The value in `/tbl?select=alias:field.aggregateFunction()::cast`\ndata SelectItem\n  = SelectField\n    { selField             :: Field\n    , selAggregateFunction :: Maybe AggregateFunction\n    , selAggregateCast     :: Maybe Cast\n    , selCast              :: Maybe Cast\n    , selAlias             :: Maybe Alias\n    }\n-- | The value in `/tbl?select=alias:another_tbl(*)`\n  | SelectRelation\n    { selRelation :: FieldName\n    , selAlias    :: Maybe Alias\n    , selHint     :: Maybe Hint\n    , selJoinType :: Maybe JoinType\n    }\n-- | The value in `/tbl?select=...another_tbl(*)`\n  | SpreadRelation\n    { selRelation :: FieldName\n    , selHint     :: Maybe Hint\n    , selJoinType :: Maybe JoinType\n    }\n  deriving (Eq, Show)\n\ntype NodeName = Text\ntype Depth = Integer\n\ndata OrderTerm\n  = OrderTerm\n    { otTerm      :: Field\n    , otDirection :: Maybe OrderDirection\n    , otNullOrder :: Maybe OrderNulls\n    }\n  | OrderRelationTerm\n    { otRelation  :: FieldName\n    , otRelTerm   :: Field\n    , otDirection :: Maybe OrderDirection\n    , otNullOrder :: Maybe OrderNulls\n    }\n  deriving (Eq, Show)\n\ndata OrderDirection\n  = OrderAsc\n  | OrderDesc\n  deriving (Eq, Show)\n\ndata OrderNulls\n  = OrderNullsFirst\n  | OrderNullsLast\n  deriving (Eq, Show)\n\ntype Field = (FieldName, JsonPath)\ntype Cast = Text\ntype Alias = Text\ntype Hint = Text\n\ndata AggregateFunction = Sum | Avg | Max | Min | Count\n  deriving (Show, Eq)\n\ndata EmbedParam\n  -- | Disambiguates an embedding operation when there's multiple relationships\n  -- between two tables. Can be the name of a foreign key constraint, column\n  -- name or the junction in an m2m relationship.\n  = EPHint Hint\n  | EPJoinType JoinType\n\ndata JoinType\n  = JTInner\n  | JTLeft\n  deriving (Eq, Show)\n\n-- | Path of the embedded levels, e.g \"clients.projects.name=eq..\" gives Path\n-- [\"clients\", \"projects\"]\ntype EmbedPath = [Text]\n\n-- | Json path operations as specified in\n-- https://www.postgresql.org/docs/current/static/functions-json.html\ntype JsonPath = [JsonOperation]\n\n-- | Represents the single arrow `->` or double arrow `->>` operators\ndata JsonOperation\n  = JArrow { jOp :: JsonOperand }\n  | J2Arrow { jOp :: JsonOperand }\n  deriving (Eq, Show, Ord)\n\n-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text\n-- because we reuse our escaping functions and let pg do the casting with\n-- '1'::int\ndata JsonOperand\n  = JKey { jVal :: Text }\n  | JIdx { jVal :: Text }\n  deriving (Eq, Show, Ord)\n\n-- | Boolean logic expression tree e.g. \"and(name.eq.N,or(id.eq.1,id.eq.2))\" is:\n--\n--            And\n--           /   \\\n--  name.eq.N     Or\n--               /  \\\n--         id.eq.1   id.eq.2\ndata LogicTree\n  = Expr Bool LogicOperator [LogicTree]\n  | Stmnt Filter\n  deriving (Eq, Show)\n\ndata LogicOperator\n  = And\n  | Or\n  deriving (Eq, Show)\n\ndata Filter\n  = Filter\n  { field  :: Field\n  , opExpr :: OpExpr\n  }\n  deriving (Eq, Show)\n\ndata OpExpr\n  = OpExpr Bool Operation\n  | NoOpExpr Text\n  deriving (Eq, Show)\n\ndata OpQuantifier = QuantAny | QuantAll\n  deriving (Eq, Show)\n\ndata Operation\n  = Op SimpleOperator SingleVal\n  | OpQuant QuantOperator (Maybe OpQuantifier) SingleVal\n  | In ListVal\n  | Is IsVal\n  | IsDistinctFrom SingleVal\n  | Fts FtsOperator (Maybe Language) SingleVal\n  deriving (Eq, Show)\n\ntype Language = Text\n\n-- | Represents a single value in a filter, e.g. id=eq.singleval\ntype SingleVal = Text\n\n-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)\ntype ListVal = [Text]\n\ndata IsVal\n  = IsNull\n  | IsNotNull\n  -- Trilean values\n  | IsTriTrue\n  | IsTriFalse\n  | IsTriUnknown\n  deriving (Eq, Show)\n\n-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers\ndata QuantOperator\n  = OpEqual\n  | OpGreaterThanEqual\n  | OpGreaterThan\n  | OpLessThanEqual\n  | OpLessThan\n  | OpLike\n  | OpILike\n  | OpMatch\n  | OpIMatch\n  deriving (Eq, Show)\n\ndata SimpleOperator\n  = OpNotEqual\n  | OpContains\n  | OpContained\n  | OpOverlap\n  | OpStrictlyLeft\n  | OpStrictlyRight\n  | OpNotExtendsRight\n  | OpNotExtendsLeft\n  | OpAdjacent\n  deriving (Eq, Show)\n\n--\n-- | Operators for full text search operators\ndata FtsOperator\n  = FilterFts\n  | FilterFtsPlain\n  | FilterFtsPhrase\n  | FilterFtsWebsearch\n  deriving (Eq, Show)\n"
  },
  {
    "path": "src/PostgREST/ApiRequest.hs",
    "content": "{-|\nModule      : PostgREST.Request.ApiRequest\nDescription : PostgREST functions to translate HTTP request to a domain type called ApiRequest.\n-}\n{-# LANGUAGE LambdaCase     #-}\n{-# LANGUAGE NamedFieldPuns #-}\nmodule PostgREST.ApiRequest\n  ( ApiRequest(..)\n  , userApiRequest\n  , userPreferences\n  ) where\n\nimport qualified Data.CaseInsensitive as CI\nimport qualified Data.HashMap.Strict  as HM\nimport qualified Data.List.NonEmpty   as NonEmptyList\nimport qualified Data.Set             as S\nimport qualified Data.Text.Encoding   as T\n\nimport Data.List                 (lookup)\nimport Data.Ranged.Ranges        (emptyRange, rangeIntersection,\n                                  rangeIsEmpty)\nimport Network.HTTP.Types.Header (RequestHeaders, hCookie)\nimport Network.Wai               (Request (..))\nimport Network.Wai.Parse         (parseHttpAccept)\nimport Web.Cookie                (parseCookies)\n\nimport PostgREST.ApiRequest.Payload      (getPayload)\nimport PostgREST.ApiRequest.QueryParams  (QueryParams (..))\nimport PostgREST.ApiRequest.Types        (Action (..), DbAction (..),\n                                          InvokeMethod (..),\n                                          Mutation (..), Payload (..),\n                                          RequestBody, Resource (..))\nimport PostgREST.Config                  (AppConfig (..),\n                                          OpenAPIMode (..))\nimport PostgREST.Config.Database         (TimezoneNames)\nimport PostgREST.Error                   (ApiRequestError (..),\n                                          RangeError (..))\nimport PostgREST.MediaType               (MediaType (..))\nimport PostgREST.RangeQuery              (NonnegRange, allRange,\n                                          convertToLimitZeroRange,\n                                          hasLimitZero,\n                                          rangeRequested)\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier (..),\n                                          Schema)\n\nimport qualified PostgREST.ApiRequest.Preferences as Preferences\nimport qualified PostgREST.ApiRequest.QueryParams as QueryParams\nimport qualified PostgREST.MediaType              as MediaType\n\nimport Protolude\n\n{-|\n  Describes what the user wants to do. This data type is a\n  translation of the raw elements of an HTTP request into domain\n  specific language.  There is no guarantee that the intent is\n  sensible, it is up to a later stage of processing to determine\n  if it is an action we are able to perform.\n-}\ndata ApiRequest = ApiRequest {\n    iAction              :: Action                           -- ^ Action on the resource\n  , iRange               :: HM.HashMap Text NonnegRange      -- ^ Requested range of rows within response\n  , iTopLevelRange       :: NonnegRange                      -- ^ Requested range of rows from the top level\n  , iPayload             :: Maybe Payload                    -- ^ Data sent by client and used for mutation actions\n  , iPreferences         :: Preferences.Preferences          -- ^ Prefer header values\n  , iQueryParams         :: QueryParams.QueryParams\n  , iColumns             :: S.Set FieldName                  -- ^ parsed columns from &columns parameter and payload\n  , iHeaders             :: [(ByteString, ByteString)]       -- ^ HTTP request headers\n  , iCookies             :: [(ByteString, ByteString)]       -- ^ Request Cookies\n  , iPath                :: ByteString                       -- ^ Raw request path\n  , iMethod              :: ByteString                       -- ^ Raw request method\n  , iSchema              :: Schema                           -- ^ The request schema. Can vary depending on profile headers.\n  , iNegotiatedByProfile :: Bool                             -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/\n  , iAcceptMediaType     :: [MediaType]                      -- ^ The resolved media types in the Accept, considering quality(q) factors\n  , iContentMediaType    :: MediaType                        -- ^ The media type in the Content-Type header\n  }\n\n-- | Examines HTTP request and translates it into user intent.\nuserApiRequest :: AppConfig -> Preferences.Preferences -> Request -> RequestBody -> Either ApiRequestError ApiRequest\nuserApiRequest conf prefs req reqBody = do\n  resource <- getResource conf $ pathInfo req\n  (schema, negotiatedByProfile) <- getSchema conf hdrs method\n  act <- getAction resource schema method\n  qPrms <- first QueryParamError $ QueryParams.parse (actIsInvokeSafe act) $ rawQueryString req\n  (topLevelRange, ranges) <- getRanges method qPrms hdrs\n  (payload, columns) <- getPayload reqBody contentMediaType qPrms act\n  return $ ApiRequest {\n    iAction = act\n  , iRange = ranges\n  , iTopLevelRange = topLevelRange\n  , iPayload = payload\n  , iPreferences = prefs\n  , iQueryParams = qPrms\n  , iColumns = columns\n  , iHeaders = iHdrs\n  , iCookies = iCkies\n  , iPath = rawPathInfo req\n  , iMethod = method\n  , iSchema = schema\n  , iNegotiatedByProfile = negotiatedByProfile\n  , iAcceptMediaType = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader \"accept\"\n  , iContentMediaType = contentMediaType\n  }\n  where\n    method = requestMethod req\n    hdrs = requestHeaders req\n    lookupHeader    = flip lookup hdrs\n    iHdrs = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]\n    iCkies = maybe [] parseCookies $ lookupHeader \"Cookie\"\n    contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader \"content-type\"\n    actIsInvokeSafe x = case x of {ActDb (ActRoutine _  (InvRead _)) -> True; _ -> False}\n\n-- | Parses the Prefer header\nuserPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences\nuserPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req\n\ngetResource :: AppConfig -> [Text] -> Either ApiRequestError Resource\ngetResource AppConfig{configOpenApiMode, configDbRootSpec} = \\case\n  []             ->\n      case (configOpenApiMode,configDbRootSpec) of\n        (OADisabled,_) -> Left OpenAPIDisabled\n        (_, Just qi)   -> Right $ ResourceRoutine (qiName qi)\n        (_, Nothing)   -> Right ResourceSchema\n\n  [table]        -> Right $ ResourceRelation table\n  [\"rpc\", pName] -> Right $ ResourceRoutine pName\n  _              -> Left InvalidResourcePath\n\ngetAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action\ngetAction resource schema method =\n  case (resource, method) of\n    (ResourceRoutine rout, \"HEAD\")    -> Right . ActDb $ ActRoutine (qi rout) $ InvRead True\n    (ResourceRoutine rout, \"GET\")     -> Right . ActDb $ ActRoutine (qi rout) $ InvRead False\n    (ResourceRoutine rout, \"POST\")    -> Right . ActDb $ ActRoutine (qi rout) Inv\n    (ResourceRoutine rout, \"OPTIONS\") -> Right $ ActRoutineInfo (qi rout) $ InvRead True\n    (ResourceRoutine _, _)            -> Left $ InvalidRpcMethod method\n\n    (ResourceRelation rel, \"HEAD\")    -> Right . ActDb $ ActRelationRead (qi rel) True\n    (ResourceRelation rel, \"GET\")     -> Right . ActDb $ ActRelationRead (qi rel) False\n    (ResourceRelation rel, \"POST\")    -> Right . ActDb $ ActRelationMut  (qi rel) MutationCreate\n    (ResourceRelation rel, \"PUT\")     -> Right . ActDb $ ActRelationMut  (qi rel) MutationSingleUpsert\n    (ResourceRelation rel, \"PATCH\")   -> Right . ActDb $ ActRelationMut  (qi rel) MutationUpdate\n    (ResourceRelation rel, \"DELETE\")  -> Right . ActDb $ ActRelationMut  (qi rel) MutationDelete\n    (ResourceRelation rel, \"OPTIONS\") -> Right $ ActRelationInfo (qi rel)\n\n    (ResourceSchema, \"HEAD\")          -> Right . ActDb $ ActSchemaRead schema True\n    (ResourceSchema, \"GET\")           -> Right . ActDb $ ActSchemaRead schema False\n    (ResourceSchema, \"OPTIONS\")       -> Right ActSchemaInfo\n\n    _                                 -> Left $ UnsupportedMethod method\n  where\n    qi = QualifiedIdentifier schema\n\n\ngetSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)\ngetSchema AppConfig{configDbSchemas} hdrs method = do\n  case profile of\n    Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas\n           | otherwise                   -> Right (p, True)\n    Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated\n  where\n    defaultSchema = NonEmptyList.head configDbSchemas\n    profile = case method of\n      -- POST/PATCH/PUT/DELETE don't use the same header as per the spec\n      \"DELETE\" -> contentProfile\n      \"PATCH\"  -> contentProfile\n      \"POST\"   -> contentProfile\n      \"PUT\"    -> contentProfile\n      _        -> acceptProfile\n    contentProfile = T.decodeUtf8 <$> lookupHeader \"Content-Profile\"\n    acceptProfile = T.decodeUtf8 <$> lookupHeader \"Accept-Profile\"\n    lookupHeader    = flip lookup hdrs\n\ngetRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)\ngetRanges method QueryParams{qsRanges} hdrs\n  | isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)\n  | method == \"PUT\" && topLevelRange /= allRange = Left PutLimitNotAllowedError\n  | otherwise = Right (topLevelRange, ranges)\n  where\n    -- According to the RFC (https://www.rfc-editor.org/rfc/rfc9110.html#name-range),\n    -- the Range header must be ignored for all methods other than GET\n    headerRange = if method == \"GET\" then rangeRequested hdrs else allRange\n    limitRange = fromMaybe allRange (HM.lookup \"limit\" qsRanges)\n    headerAndLimitRange = rangeIntersection headerRange limitRange\n    -- Bypass all the ranges and send only the limit zero range (0 <= x <= -1) if\n    -- limit=0 is present in the query params (not allowed for the Range header)\n    ranges = HM.insert \"limit\" (convertToLimitZeroRange limitRange headerAndLimitRange) qsRanges\n    -- The only emptyRange allowed is the limit zero range\n    isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)\n    topLevelRange = fromMaybe allRange $ HM.lookup \"limit\" ranges -- if no limit is specified, get all the request rows\n"
  },
  {
    "path": "src/PostgREST/App.hs",
    "content": "{-|\nModule      : PostgREST.App\nDescription : PostgREST main application\n\nThis module is in charge of mapping HTTP requests to PostgreSQL queries.\nSome of its functionality includes:\n\n- Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION.\n- Producing HTTP Headers according to RFCs.\n- Content Negotiation\n-}\n{-# LANGUAGE RecordWildCards     #-}\n{-# LANGUAGE ScopedTypeVariables #-}\n{-# LANGUAGE ViewPatterns        #-}\nmodule PostgREST.App\n  ( postgrest\n  , run\n  ) where\n\n\nimport GHC.IO.Exception (IOErrorType (..))\nimport System.IO.Error  (ioeGetErrorType)\n\nimport Control.Monad.Except     (liftEither)\nimport Data.Either.Combinators  (mapLeft, whenLeft)\nimport Data.Maybe               (fromJust)\nimport Data.String              (IsString (..))\nimport Network.Wai.Handler.Warp (defaultSettings, setHost,\n                                 setOnException, setPort,\n                                 setServerName)\n\nimport qualified Data.Text.Encoding       as T\nimport qualified Network.Wai              as Wai\nimport qualified Network.Wai.Handler.Warp as Warp\n\nimport qualified PostgREST.Admin      as Admin\nimport qualified PostgREST.ApiRequest as ApiRequest\nimport qualified PostgREST.AppState   as AppState\nimport qualified PostgREST.Auth       as Auth\nimport qualified PostgREST.Cors       as Cors\nimport qualified PostgREST.Error      as Error\nimport qualified PostgREST.Listener   as Listener\nimport qualified PostgREST.Logger     as Logger\nimport qualified PostgREST.MainTx     as MainTx\nimport qualified PostgREST.Plan       as Plan\nimport qualified PostgREST.Query      as Query\nimport qualified PostgREST.Response   as Response\nimport qualified PostgREST.Unix       as Unix (installSignalHandlers)\n\nimport PostgREST.ApiRequest           (ApiRequest (..))\nimport PostgREST.AppState             (AppState)\nimport PostgREST.Auth.Types           (AuthResult (..))\nimport PostgREST.Config               (AppConfig (..), LogLevel (..))\nimport PostgREST.Error                (Error)\nimport PostgREST.Network              (resolveSocketToAddress)\nimport PostgREST.Observation          (Observation (..))\nimport PostgREST.Response.Performance (ServerTiming (..),\n                                       serverTimingHeader)\nimport PostgREST.SchemaCache          (SchemaCache (..))\nimport PostgREST.TimeIt               (timeItT)\nimport PostgREST.Version              (docsVersion, prettyVersion)\n\nimport qualified Data.ByteString.Char8     as BS\nimport qualified Data.List                 as L\nimport           Data.Streaming.Network    (bindPortTCP,\n                                            bindRandomPortTCP)\nimport qualified Data.Text                 as T\nimport qualified Network.HTTP.Types        as HTTP\nimport qualified Network.HTTP.Types.Header as HTTP (hVary)\nimport qualified Network.Socket            as NS\nimport           PostgREST.Unix            (createAndBindDomainSocket)\nimport           Protolude                 hiding (Handler)\n\ntype Handler = ExceptT Error\n\nrun :: AppState -> IO ()\nrun appState = do\n  conf@AppConfig{..} <- AppState.getConfig appState\n\n  AppState.schemaCacheLoader appState -- Loads the initial SchemaCache\n  (mainSocket, adminSocket) <- initSockets conf\n\n  Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)\n\n  Listener.runListener appState\n\n  Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)\n\n  let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)\n\n  do\n    address <- resolveSocketToAddress mainSocket\n    observer $ AppServerAddressObs address\n\n  Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app\n  where\n    observer = AppState.getObserver appState\n\n    onWarpException :: Maybe Wai.Request -> SomeException -> IO ()\n    onWarpException _ ex =\n      when (shouldDisplayException ex) $\n        observer $ WarpErrorObs $ show ex\n\n    -- Similar to wai defaultShouldDisplayException in\n    -- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258\n    -- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.\n    -- We want to reuse this to avoid flooding the logs for some transient failure cases.\n    shouldDisplayException :: SomeException -> Bool\n    shouldDisplayException se\n        | Just (_ :: Warp.InvalidRequest) <- fromException se = False\n        | Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False\n        | otherwise = True\n\nserverSettings :: AppConfig -> Warp.Settings\nserverSettings AppConfig{..} =\n  defaultSettings\n    & setHost (fromString $ toS configServerHost)\n    & setPort configServerPort\n    & setServerName (\"postgrest/\" <> prettyVersion)\n\n-- | PostgREST application\npostgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application\npostgrest logLevel appState connWorker =\n  traceHeaderMiddleware appState .\n  Cors.middleware appState .\n  Auth.middleware appState .\n  Logger.middleware logLevel Auth.getRole $\n    -- fromJust can be used, because the auth middleware will **always** add\n    -- some AuthResult to the vault.\n    \\req respond -> do\n      appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload\n      case fromJust $ Auth.getResult req of\n        Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err\n        Right authResult -> do\n          maybeSchemaCache <- AppState.getSchemaCache appState\n\n          let\n            eitherResponse :: IO (Either Error Wai.Response)\n            eitherResponse =\n              runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req\n\n          response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse\n          -- Launch the connWorker when the connection is down. The postgrest\n          -- function can respond successfully (with a stale schema cache) before\n          -- the connWorker is done. However, when there's an empty schema cache\n          -- postgrest responds with the error `PGRST002`; this means that the schema\n          -- cache is still loading, so we don't launch the connWorker here because\n          -- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704\n          -- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done\n          when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker\n          resp <- do\n            delay <- AppState.getNextDelay appState\n            return $ addRetryHint delay response\n          respond resp\n\npostgrestResponse\n  :: AppState.AppState\n  -> AppConfig\n  -> Maybe SchemaCache\n  -> AuthResult\n  -> Wai.Request\n  -> Handler IO Wai.Response\npostgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthResult{..} req = do\n  let observer = AppState.getObserver appState\n\n  sCache <-\n    case maybeSchemaCache of\n      Just sCache ->\n        return sCache\n      Nothing -> do\n        lift $ observer SchemaCacheEmptyObs\n        throwError Error.NoSchemaCacheError\n\n  body <- lift $ Wai.strictRequestBody req\n\n  let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing\n      timezones = dbTimezones sCache\n      prefs = ApiRequest.userPreferences conf req timezones\n\n  (parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body\n  (planTime, plan)                   <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache\n\n  let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest\n      tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache\n      obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s\n\n  (txTime, txResult) <- withTiming $ do\n    case tx of\n      MainTx.NoDbTx r -> pure r\n      MainTx.DbTx{..} -> do\n        dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)\n        let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes\n\n        -- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.\n        -- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.\n        -- This needs refactoring so only the below obsQuery is used.\n        lift $ whenLeft eitherResp $ obsQuery . Error.status\n        liftEither eitherResp\n\n  (respTime, resp) <- withTiming $ do\n    let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache\n        status' = either Error.status Response.pgrstStatus response\n\n    -- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)\n    lift $ obsQuery status'\n    liftEither response\n\n  return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp\n\n  where\n    toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response\n    toWaiResponse timing (Response.PgrstResponse st hdrs bod) =\n      Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod\n\n    serverTimingHeaders :: ServerTiming -> [HTTP.Header]\n    serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]\n\n    varyHeader :: HTTP.Header\n    varyHeader = (HTTP.hVary, \"Accept, Prefer, Range\")\n\n    varyHeaderPresent :: [HTTP.Header] -> Bool\n    varyHeaderPresent = any (\\(h, _v) -> h == HTTP.hVary)\n\n    withTiming :: Handler IO a -> Handler IO (Maybe Double, a)\n    withTiming f = if configServerTimingEnabled\n        then do\n          (t, r) <- timeItT f\n          pure (Just t, r)\n        else do\n          r <- f\n          pure (Nothing, r)\n\ntraceHeaderMiddleware :: AppState -> Wai.Middleware\ntraceHeaderMiddleware appState app req respond = do\n  conf <- AppState.getConfig appState\n\n  case configServerTraceHeader conf of\n    Nothing -> app req respond\n    Just hdr ->\n      let hdrVal = L.lookup hdr $ Wai.requestHeaders req in\n      app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))\n\naddRetryHint :: Int -> Wai.Response -> Wai.Response\naddRetryHint delay response = do\n  let h = (\"Retry-After\", BS.pack $ show delay)\n  Wai.mapResponseHeaders (\\hs -> if isServiceUnavailable response then h:hs else hs) response\n\nisServiceUnavailable :: Wai.Response -> Bool\nisServiceUnavailable response = Wai.responseStatus response == HTTP.status503\n\ntype AppSockets = (NS.Socket, Maybe NS.Socket)\n\ninitSockets :: AppConfig -> IO AppSockets\ninitSockets AppConfig{..} = do\n  let\n    cfg'usp = configServerUnixSocket\n    cfg'uspm = configServerUnixSocketMode\n    cfg'host = configServerHost\n    cfg'port = configServerPort\n    cfg'adminHost = configAdminServerHost\n    cfg'adminPort = configAdminServerPort\n\n  sock <- case cfg'usp of\n    -- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,\n    -- but we need to have runtime error if we try to use it in Windows, not compile time error\n    Just path -> createAndBindDomainSocket path cfg'uspm\n    Nothing -> do\n      (_, sock) <-\n        if cfg'port /= 0\n          then do\n            sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)\n            pure (cfg'port, sock)\n          else do\n            -- explicitly bind to a random port, returning bound port number\n            (num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)\n            pure (num, sock)\n      pure sock\n\n  adminSock <- case cfg'adminPort of\n    Just adminPort -> do\n      adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)\n      pure $ Just adminSock\n    Nothing -> pure Nothing\n\n  pure (sock, adminSock)\n\n"
  },
  {
    "path": "src/PostgREST/AppState.hs",
    "content": "{-# LANGUAGE LambdaCase      #-}\n{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\n\nmodule PostgREST.AppState\n  ( AppState\n  , destroy\n  , getConfig\n  , getSchemaCache\n  , getMainThreadId\n  , getPgVersion\n  , getNextDelay\n  , getNextListenerDelay\n  , getTime\n  , getJwtCacheState\n  , init\n  , initWithPool\n  , putNextListenerDelay\n  , putSchemaCache\n  , putPgVersion\n  , putIsListenerOn\n  , usePool\n  , readInDbConfig\n  , schemaCacheLoader\n  , getObserver\n  , isLoaded\n  , isPending\n  ) where\n\nimport qualified Data.ByteString.Char8      as BS\nimport           Data.Either.Combinators    (whenLeft)\nimport qualified Hasql.Pool                 as SQL\nimport qualified Hasql.Pool.Config          as SQL\nimport qualified Hasql.Session              as SQL\nimport qualified Hasql.Transaction.Sessions as SQL\nimport qualified Network.HTTP.Types.Status  as HTTP\nimport qualified PostgREST.Auth.JwtCache    as JwtCache\nimport qualified PostgREST.Error            as Error\nimport qualified PostgREST.Logger           as Logger\nimport qualified PostgREST.Metrics          as Metrics\nimport           PostgREST.Observation\nimport           PostgREST.TimeIt           (timeItT)\nimport           PostgREST.Version          (prettyVersion)\n\nimport Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,\n                           updateAction)\nimport Control.Debounce\nimport Control.Retry      (RetryPolicy, RetryStatus (..), capDelay,\n                           exponentialBackoff, retrying,\n                           rsPreviousDelay)\nimport Data.IORef         (IORef, atomicWriteIORef, newIORef,\n                           readIORef)\nimport Data.Time.Clock    (UTCTime, getCurrentTime)\n\nimport PostgREST.Auth.JwtCache           (JwtCacheState, update)\nimport PostgREST.Config                  (AppConfig (..),\n                                          addFallbackAppName,\n                                          readAppConfig)\nimport PostgREST.Config.Database         (queryDbSettings,\n                                          queryPgVersion,\n                                          queryRoleSettings)\nimport PostgREST.Config.PgVersion        (PgVersion (..),\n                                          minimumPgVersion)\nimport PostgREST.SchemaCache             (SchemaCache (..),\n                                          querySchemaCache,\n                                          showSummary)\nimport PostgREST.SchemaCache.Identifiers (quoteQi)\n\nimport Protolude\n\ndata AppState = AppState\n  -- | Database connection pool\n  { statePool              :: SQL.Pool\n  -- | Database server version\n  , statePgVersion         :: IORef PgVersion\n  -- | Schema cache\n  , stateSchemaCache       :: IORef (Maybe SchemaCache)\n  -- | The schema cache status\n  , stateSCacheStatus      :: SchemaCacheStatus\n  -- | State of the LISTEN channel\n  , stateIsListenerOn      :: IORef Bool\n  -- | starts the connection worker with a debounce\n  , debouncedSCacheLoader  :: IO ()\n  -- | Config that can change at runtime\n  , stateConf              :: IORef AppConfig\n  -- | Time used for verifying JWT expiration\n  , stateGetTime           :: IO UTCTime\n  -- | Used for killing the main thread in case a subthread fails\n  , stateMainThreadId      :: ThreadId\n  -- | Keeps track of the next delay for db connection retry\n  , stateNextDelay         :: IORef Int\n  -- | Keeps track of the next delay for the listener\n  , stateNextListenerDelay :: IORef Int\n  -- | Observation handler\n  , stateObserver          :: ObservationHandler\n  -- | JWT Cache\n  , stateJwtCache          :: JwtCache.JwtCacheState\n  , stateLogger            :: Logger.LoggerState\n  , stateMetrics           :: Metrics.MetricsState\n  }\n\n-- | Schema cache status.\n-- Empty means pending and full means loaded.\nnewtype SchemaCacheStatus = SchemaCacheStatus\n  { getSCStatusMVar :: MVar ()\n  }\n\ninit :: AppConfig -> IO AppState\ninit conf@AppConfig{configLogLevel, configDbPoolSize} = do\n  loggerState  <- Logger.init\n  metricsState <- Metrics.init configDbPoolSize\n  let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)\n\n  observer $ AppStartObs prettyVersion\n\n  pool <- initPool conf observer\n  initWithPool pool conf loggerState metricsState observer --{ stateSocketREST = sock, stateSocketAdmin = adminSock}\n\ninitWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState\ninitWithPool pool conf loggerState metricsState observer = do\n\n  appState <- AppState pool\n    <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step\n    <*> newIORef Nothing\n    <*> newSchemaCacheStatus\n    <*> newIORef False\n    <*> pure (pure ())\n    <*> newIORef conf\n    <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }\n    <*> myThreadId\n    <*> newIORef 0\n    <*> newIORef 1\n    <*> pure observer\n    <*> JwtCache.init conf observer\n    <*> pure loggerState\n    <*> pure metricsState\n\n  deb <-\n    let decisecond = 100000 in\n    mkDebounce defaultDebounceSettings\n       { debounceAction = retryingSchemaCacheLoad appState\n       , debounceFreq = decisecond\n       , debounceEdge = leadingEdge -- runs the worker at the start and the end\n       }\n\n  return appState { debouncedSCacheLoader = deb}\n\ndestroy :: AppState -> IO ()\ndestroy = destroyPool\n\ninitPool :: AppConfig -> ObservationHandler -> IO SQL.Pool\ninitPool AppConfig{..} observer = do\n  SQL.acquire $ SQL.settings\n    [ SQL.size configDbPoolSize\n    , SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout\n    , SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime\n    , SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime\n    , SQL.staticConnectionSettings (toUtf8 $ addFallbackAppName prettyVersion configDbUri)\n    , SQL.observationHandler $ observer . HasqlPoolObs\n    ]\n\n-- | Run an action with a database connection.\nusePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)\nusePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do\n  observer PoolRequest\n\n  res <- SQL.use statePool sess\n\n  observer PoolRequestFullfilled\n\n  whenLeft res (\\case\n    SQL.AcquisitionTimeoutUsageError ->\n      observer PoolAcqTimeoutObs\n    err@(SQL.ConnectionUsageError e) ->\n      let failureMessage = BS.unpack $ fromMaybe mempty e in\n      when ((\"FATAL:  password authentication failed\" `isInfixOf` failureMessage) || (\"no password supplied\" `isInfixOf` failureMessage)) $ do\n        observer $ ExitDBFatalError ServerAuthError err\n        killThread mainThreadId\n    err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do\n      case resultErr of\n        SQL.UnexpectedResult{} -> do\n          observer $ ExitDBFatalError ServerPgrstBug err\n          killThread mainThreadId\n        SQL.RowError{} -> do\n          observer $ ExitDBFatalError ServerPgrstBug err\n          killThread mainThreadId\n        SQL.UnexpectedAmountOfRows{} -> do\n          observer $ ExitDBFatalError ServerPgrstBug err\n          killThread mainThreadId\n        -- Check for a syntax error (42601 is the pg code) only for queries that don't have `WITH pgrst_source` as prefix.\n        -- This would mean the error is on our schema cache queries, so we treat it as fatal.\n        -- TODO have a better way to mark this as a schema cache query\n        SQL.ServerError \"42601\" _ _ _ _ ->\n          unless (\"WITH pgrst_source\" `BS.isPrefixOf` tpl) $ do\n            observer $ ExitDBFatalError ServerPgrstBug err\n            killThread mainThreadId\n        -- Check for a \"prepared statement <name> already exists\" error (Code 42P05: duplicate_prepared_statement).\n        -- This would mean that a connection pooler in transaction mode is being used\n        -- while prepared statements are enabled in the PostgREST configuration,\n        -- both of which are incompatible with each other.\n        SQL.ServerError \"42P05\" _ _ _ _ -> do\n          observer $ ExitDBFatalError ServerError42P05 err\n          killThread mainThreadId\n        -- Check for a \"transaction blocks not allowed in statement pooling mode\" error (Code 08P01: protocol_violation).\n        -- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.\n        SQL.ServerError \"08P01\" \"transaction blocks not allowed in statement pooling mode\" _ _ _ -> do\n          observer $ ExitDBFatalError ServerError08P01 err\n          killThread mainThreadId\n        SQL.ServerError{} ->\n          when (Error.status (Error.PgError False err) >= HTTP.status500) $\n            observer $ QueryErrorCodeHighObs err\n    err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->\n      -- An error on the client-side, usually indicates problems with connection\n        observer $ QueryErrorCodeHighObs err\n    )\n\n  return res\n\n-- | Flush the connection pool so that any future use of the pool will\n-- use connections freshly established after this call.\nflushPool :: AppState -> IO ()\nflushPool AppState{..} = SQL.release statePool\n\n-- | Destroy the pool on shutdown.\ndestroyPool :: AppState -> IO ()\ndestroyPool AppState{..} = SQL.release statePool\n\ngetPgVersion :: AppState -> IO PgVersion\ngetPgVersion = readIORef . statePgVersion\n\nputPgVersion :: AppState -> PgVersion -> IO ()\nputPgVersion = atomicWriteIORef . statePgVersion\n\ngetSchemaCache :: AppState -> IO (Maybe SchemaCache)\ngetSchemaCache = readIORef . stateSchemaCache\n\nputSchemaCache :: AppState -> Maybe SchemaCache -> IO ()\nputSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)\n\nschemaCacheLoader :: AppState -> IO ()\nschemaCacheLoader = debouncedSCacheLoader\n\ngetNextDelay :: AppState -> IO Int\ngetNextDelay = readIORef . stateNextDelay\n\ngetNextListenerDelay :: AppState -> IO Int\ngetNextListenerDelay = readIORef . stateNextListenerDelay\n\nputNextListenerDelay :: AppState -> Int -> IO ()\nputNextListenerDelay = atomicWriteIORef . stateNextListenerDelay\n\ngetConfig :: AppState -> IO AppConfig\ngetConfig = readIORef . stateConf\n\nputConfig :: AppState -> AppConfig -> IO ()\nputConfig = atomicWriteIORef . stateConf\n\ngetTime :: AppState -> IO UTCTime\ngetTime = stateGetTime\n\ngetJwtCacheState :: AppState -> JwtCacheState\ngetJwtCacheState = stateJwtCache\n\ngetMainThreadId :: AppState -> ThreadId\ngetMainThreadId = stateMainThreadId\n\nisConnEstablished :: AppState -> IO Bool\nisConnEstablished appState = do\n  AppConfig{..} <- getConfig appState\n  if configDbChannelEnabled then -- if the listener is enabled, we can be sure the connection is up\n    readIORef $ stateIsListenerOn appState\n  else -- otherwise the only way to check the connection is to make a query\n    isRight <$> usePool appState (SQL.sql \"SELECT 1\")\n\nputIsListenerOn :: AppState -> Bool -> IO ()\nputIsListenerOn = atomicWriteIORef . stateIsListenerOn\n\nisLoaded :: AppState -> IO Bool\nisLoaded x = do\n  scacheLoaded <- isSchemaCacheLoaded x\n  connEstablished <- isConnEstablished x\n  return $ scacheLoaded && connEstablished\n\nisPending :: AppState -> IO Bool\nisPending x = do\n  scacheLoaded <- isSchemaCacheLoaded x\n  connEstablished <- isConnEstablished x\n  return $ not scacheLoaded || not connEstablished\n\ngetObserver :: AppState -> ObservationHandler\ngetObserver = stateObserver\n\n-- | Try to load the schema cache and retry if it fails.\n--\n-- This is done by repeatedly: 1) flushing the pool, 2) querying the version and validating that the postgres version is supported by us, and 3) loading the schema cache.\n-- It's necessary to flush the pool:\n--\n-- + Because connections cache the pg catalog(see #2620)\n-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.\nretryingSchemaCacheLoad :: AppState -> IO ()\nretryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} =\n  void $ retrying retryPolicy shouldRetry (\\RetryStatus{rsIterNumber, rsPreviousDelay} -> do\n    when (rsIterNumber > 0) $ do\n      let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs\n      observer $ ConnectionRetryObs delay\n\n    flushPool appState\n\n    (,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)\n  )\n  where\n    qPgVersion :: IO (Maybe PgVersion)\n    qPgVersion = do\n      AppConfig{..} <- getConfig appState\n      pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established\n      case pgVersion of\n        Left e -> do\n          observer $ QueryPgVersionError e\n          unless configDbPoolAutomaticRecovery $ do\n            observer ExitDBNoRecoveryObs\n            killThread mainThreadId\n          return Nothing\n        Right actualPgVersion ->\n          if actualPgVersion < minimumPgVersion then do\n            observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion\n            killThread mainThreadId\n            return Nothing\n          else do\n            observer $ DBConnectedObs $ pgvFullName actualPgVersion\n            observer $ PoolInit configDbPoolSize\n            putPgVersion appState actualPgVersion\n            return $ Just actualPgVersion\n\n    qInDbConfig :: IO ()\n    qInDbConfig = do\n      AppConfig{..} <- getConfig appState\n      when configDbConfig $ readInDbConfig False appState\n\n    qSchemaCache :: IO (Maybe SchemaCache)\n    qSchemaCache = do\n      conf@AppConfig{..} <- getConfig appState\n      (resultTime, result) <-\n        let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in\n        timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)\n      case result of\n        Left e -> do\n          markSchemaCachePending appState\n          putSchemaCache appState Nothing\n          observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e\n          return Nothing\n\n        Right sCache -> do\n          -- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an\n          -- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.\n          markSchemaCachePending appState\n          putSchemaCache appState $ Just sCache\n          observer $ SchemaCacheQueriedObs resultTime\n          observer . uncurry SchemaCacheLoadedObs =<< timeItT (evaluate $ showSummary sCache)\n          markSchemaCacheLoaded appState\n          return $ Just sCache\n\n    shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool\n    shouldRetry _ (pgVer, sCache) = do\n      AppConfig{..} <- getConfig appState\n      let itShould = configDbPoolAutomaticRecovery && (isNothing pgVer || isNothing sCache)\n      return itShould\n\n    retryPolicy :: RetryPolicy\n    retryPolicy =\n      let delayMicroseconds = 32*oneSecondInUs {-32 seconds-} in\n      capDelay delayMicroseconds $ exponentialBackoff oneSecondInUs\n\n    oneSecondInUs = 1000000 -- one second in microseconds\n\nnewSchemaCacheStatus :: IO SchemaCacheStatus\nnewSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar\n\nmarkSchemaCachePending :: AppState -> IO ()\nmarkSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus\n\nmarkSchemaCacheLoaded :: AppState -> IO ()\nmarkSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus\n\nisSchemaCacheLoaded :: AppState -> IO Bool\nisSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus\n\n-- | Reads the in-db config and reads the config file again\n-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.\nreadInDbConfig :: Bool -> AppState -> IO ()\nreadInDbConfig startingUp appState@AppState{stateObserver=observer} = do\n  conf <- getConfig appState\n  pgVer <- getPgVersion appState\n  dbSettings <-\n    if configDbConfig conf then do\n      qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))\n      case qDbSettings of\n        Left e -> do\n          observer $ ConfigReadErrorObs e\n          pure mempty\n        Right x -> pure x\n    else\n      pure mempty\n  (roleSettings, roleIsolationLvl) <-\n    if configDbConfig conf then do\n      rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))\n      case rSettings of\n        Left e -> do\n          observer $ QueryRoleSettingsErrorObs e\n          pure (mempty, mempty)\n        Right x -> pure x\n    else\n      pure mempty\n  readAppConfig dbSettings (configFilePath conf) (Just $ configDbUri conf) roleSettings roleIsolationLvl >>= \\case\n    Left err   ->\n      if startingUp then\n        panic err -- die on invalid config if the program is starting up\n      else\n        observer $ ConfigInvalidObs err\n    Right newConf -> do\n      putConfig appState newConf\n      -- After the config has reloaded, jwt-secret might have changed, so\n      -- if it has changed, it is important to invalidate the jwt cache\n      -- entries, because they were cached using the old secret\n      update (getJwtCacheState appState) newConf\n\n      if startingUp then\n        pass\n      else\n        observer ConfigSucceededObs\n"
  },
  {
    "path": "src/PostgREST/Auth/Jwt.hs",
    "content": "{-|\nModule      : PostgREST.Auth.Jwt\nDescription : PostgREST JWT support functions.\n\nThis module provides functions to deal with JWT parsing and validation (http://jwt.io).\n-}\n{-# LANGUAGE DeriveGeneric         #-}\n{-# LANGUAGE FlexibleContexts      #-}\n{-# LANGUAGE ImpredicativeTypes    #-}\n{-# LANGUAGE LambdaCase            #-}\n{-# LANGUAGE NamedFieldPuns        #-}\n{-# LANGUAGE QuantifiedConstraints #-}\n\nmodule PostgREST.Auth.Jwt\n  ( parseAndDecodeClaims\n  , parseClaims) where\n\nimport qualified Data.Aeson                 as JSON\nimport qualified Data.ByteString            as BS\nimport qualified Data.ByteString.Internal   as BS\nimport qualified Data.ByteString.Lazy.Char8 as LBS\nimport qualified Data.Scientific            as Sci\nimport qualified Jose.Jwk                   as JWT\nimport qualified Jose.Jwt                   as JWT\n\nimport Control.Monad.Except    (liftEither)\nimport Data.Either.Combinators (mapLeft)\nimport Data.Text               ()\nimport Data.Time.Clock         (UTCTime, nominalDiffTimeToSeconds)\nimport Data.Time.Clock.POSIX   (utcTimeToPOSIXSeconds)\n\nimport PostgREST.Auth.Types    (AuthResult (..))\nimport PostgREST.Config        (AppConfig (..), audMatchesCfg)\nimport PostgREST.Config.JSPath (walkJSPath)\nimport PostgREST.Error         (Error (..), JwtClaimsError (..),\n                                JwtDecodeError (..), JwtError (..))\n\nimport Data.Aeson       ((.:?))\nimport Data.Aeson.Types (parseMaybe)\nimport Jose.Jwk         (JwkSet)\nimport Protolude        hiding (first)\n\nparseAndDecodeClaims :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JSON.Object\nparseAndDecodeClaims jwkSet token = parseToken jwkSet token >>= decodeClaims\n\ndecodeClaims :: MonadError Error m => JWT.JwtContent -> m JSON.Object\ndecodeClaims (JWT.Jws (_, claims)) = maybe (throwError (JwtErr $ JwtClaimsErr ParsingClaimsFailed)) pure (JSON.decodeStrict claims)\ndecodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType\n\nvalidateClaims :: MonadError Error m => UTCTime -> (Text -> Bool) -> JSON.Object -> m ()\nvalidateClaims time audMatches claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time audMatches claims)\n\ndata ValidAud = VAString Text | VAArray [Text] deriving Generic\ninstance JSON.FromJSON ValidAud where\n  parseJSON = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue }\n\ncheckForErrors :: (Applicative m, Monoid (m JwtClaimsError)) => UTCTime -> (Text -> Bool) -> JSON.Object -> m JwtClaimsError\ncheckForErrors time audMatches = mconcat\n  [\n    claim \"exp\" ExpClaimNotNumber $ inThePast JWTExpired\n  , claim \"nbf\" NbfClaimNotNumber $ inTheFuture JWTNotYetValid\n  , claim \"iat\" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture\n  , claim \"aud\" AudClaimNotStringOrArray $ checkValue (not . validAud) JWTNotInAudience\n  ]\n  where\n      allowedSkewSeconds = 30 :: Int64\n      sciToInt = fromMaybe 0 . Sci.toBoundedInteger\n      toSec = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds\n      now = toSec time\n\n      inTheFuture = checkTime ((now + allowedSkewSeconds) <)\n      inThePast = checkTime ((now - allowedSkewSeconds) >)\n\n      checkTime cond = checkValue (cond. sciToInt)\n\n      validAud = \\case\n        (VAString aud) -> audMatches aud\n        (VAArray auds) -> null auds || any audMatches auds\n\n      checkValue invalid msg val =\n        if invalid val then\n          pure msg\n        else\n          mempty\n\n      claim key parseError checkParsed = maybe (pure parseError) (maybe mempty checkParsed) . parseMaybe (.:? key)\n\n-- | Receives the JWT secret and audience (from config) and a JWT and returns a\n-- JSON object of JWT claims.\nparseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent\nparseToken _ \"\" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader\nparseToken secret tkn = do\n  tknWith3Parts <- hasThreeParts tkn\n  eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts\n  liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent\n  where\n      hasThreeParts token = case length $ BS.split (BS.c2w '.') token of\n        3 -> pure token\n        n -> throwError $ JwtErr $ JwtDecodeErr $ UnexpectedParts n\n\n      jwtDecodeError :: JWT.JwtError -> JwtError\n      -- The only errors we can get from JWT.decode function are:\n      --   BadAlgorithm\n      --   KeyError\n      --   BadCrypto\n      jwtDecodeError (JWT.KeyError m)     = JwtDecodeErr $ KeyError m\n      jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m\n      jwtDecodeError JWT.BadCrypto        = JwtDecodeErr BadCrypto\n      -- Control never reaches here, the decode function only returns the above three\n      jwtDecodeError _                    = JwtDecodeErr UnreachableDecodeError\n\nparseClaims :: (MonadError Error m, MonadIO m) => AppConfig -> UTCTime -> JSON.Object -> m AuthResult\nparseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims = do\n  validateClaims time (audMatchesCfg cfg) mclaims\n  -- role defaults to anon if not specified in jwt\n  role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $\n    unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole\n  pure AuthResult\n           { authClaims = mclaims\n           , authRole = role\n           }\n  where\n    unquoted :: JSON.Value -> BS.ByteString\n    unquoted (JSON.String t) = encodeUtf8 t\n    unquoted v               = LBS.toStrict $ JSON.encode v\n"
  },
  {
    "path": "src/PostgREST/Auth/JwtCache.hs",
    "content": "{-|\nModule      : PostgREST.Auth.JwtCache\nDescription : PostgREST JWT validation results Cache.\n\nThis module provides functions to deal with the JWT cache.\n-}\n{-# LANGUAGE ExistentialQuantification #-}\n{-# LANGUAGE FlexibleInstances         #-}\n{-# LANGUAGE LambdaCase                #-}\n{-# LANGUAGE MultiParamTypeClasses     #-}\n{-# LANGUAGE NamedFieldPuns            #-}\n{-# LANGUAGE StrictData                #-}\n\nmodule PostgREST.Auth.JwtCache\n  ( init\n  , update\n  , JwtCacheState\n  , lookupJwtCache\n  ) where\n\nimport qualified Data.Aeson        as JSON\nimport qualified Data.Aeson.KeyMap as KM\n\nimport PostgREST.Error (Error (..), JwtError (JwtSecretMissing))\n\nimport           Control.Concurrent.STM      (newTVarIO, readTVar,\n                                              writeTVar)\nimport           Control.Concurrent.STM.TVar (TVar)\nimport           Control.Monad.Error.Class   (liftEither)\nimport           Data.ByteString             hiding (all, init)\nimport           Data.IORef                  (IORef, newIORef,\n                                              readIORef, writeIORef)\nimport           Jose.Jwk                    (JwkSet)\nimport           PostgREST.Auth.Jwt          (parseAndDecodeClaims)\nimport           PostgREST.Cache.Sieve       (alwaysValid)\nimport qualified PostgREST.Cache.Sieve       as SC\nimport           PostgREST.Config            (AppConfig (..))\nimport           PostgREST.Observation       (Observation (JwtCacheEviction, JwtCacheLookup),\n                                              ObservationHandler)\nimport           Protolude\n\ndata JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)\n\nclass CacheVariant m v where\n  cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object\n\n{-|\nJwt caching can have three different configurations:\n* missing JWT Key (no caching and throw error when JWT token present in the request)\n* JWT cache turned off\n* JWT cache turned on\n\nAll three options are represented by JwtCache data type.\n\nHandling of reconfiguration is centralized in this module.\n-}\ndata JwtCache =\n  JwtNoJwks |\n  JwtNoCache JwkSet |\n  forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)\n\ninstance CacheVariant IO (Either Error JSON.Object) where\n  cached c = lift . SC.cached c >=> liftEither\n\ninstance CacheVariant (ExceptT Error IO) JSON.Object where\n  cached = SC.cached\n\ndecode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object\ndecode JwtNoJwks        = const $ throwError (JwtErr JwtSecretMissing)\ndecode (JwtNoCache key) = parseAndDecodeClaims key\ndecode (JwtCache _ _ c) = cached c\n\n-- | Reconfigure JWT caching and update JwtCacheState accordingly\nupdate :: JwtCacheState -> AppConfig -> IO ()\nupdate (JwtCacheState observationHandler jwtCacheState) config@AppConfig{configJWKS, configJwtCacheMaxEntries} =\n  let reinitialize =\n        newJwtCache config observationHandler\n          >>= writeIORef jwtCacheState\n  in\n  readIORef jwtCacheState >>= \\case\n    (JwtCache decodingKey maxSize _) ->\n      if configJWKS /= Just decodingKey || configJwtCacheMaxEntries <= 0 then\n        -- reinitialize if key changed or cache disabled\n        reinitialize\n      else\n        -- max size changed - set it and let the cache shrink itself if necessary\n        atomically $ writeTVar maxSize configJwtCacheMaxEntries\n\n    _ -> reinitialize\n\ninit :: AppConfig -> ObservationHandler -> IO JwtCacheState\ninit config = fmap (<$>) JwtCacheState <*> (newJwtCache config >=> newIORef)\n\n-- | Initialize JwtCacheState\nnewJwtCache :: AppConfig -> ObservationHandler -> IO JwtCache\nnewJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler = do\n  maybe (pure JwtNoJwks) initCache configJWKS\n  where\n    initCache key = if configJwtCacheMaxEntries <= 0 then pure (JwtNoCache key) else createCache key configJwtCacheMaxEntries\n\n    createCache key maxSize = do\n          maxSizeTVar <- newTVarIO maxSize\n          JwtCache key maxSizeTVar <$>\n            notCachingErrors (readTVar maxSizeTVar) key\n\n    notCachingErrors :: STM Int -> JwkSet -> IO (SC.Cache (ExceptT Error IO) ByteString JSON.Object)\n    notCachingErrors maxSize key = SC.cacheIO (SC.CacheConfig maxSize\n            (parseAndDecodeClaims key)\n            (lift . observationHandler . JwtCacheLookup) -- lookup metrics\n            (const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics\n            alwaysValid) -- no invalidation for now\n\nlookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object\nlookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode\n"
  },
  {
    "path": "src/PostgREST/Auth/Types.hs",
    "content": "module PostgREST.Auth.Types\n  ( AuthResult (..) )\n  where\n\nimport qualified Data.Aeson        as JSON\nimport qualified Data.Aeson.KeyMap as KM\nimport qualified Data.ByteString   as BS\n\n-- |\n-- Parse and store result for JWT Claims. Can be accessed in\n-- db through GUCs (for RLS etc)\ndata AuthResult = AuthResult\n  { authClaims :: KM.KeyMap JSON.Value\n  , authRole   :: BS.ByteString\n  }\n"
  },
  {
    "path": "src/PostgREST/Auth.hs",
    "content": "{-# LANGUAGE RecordWildCards #-}\n{-|\nModule      : PostgREST.Auth\nDescription : PostgREST authentication functions.\n\nThis module provides functions to deal with the JWT authentication (http://jwt.io).\nIt also can be used to define other authentication functions,\nin the future Oauth, LDAP and similar integrations can be coded here.\n\nAuthentication should always be implemented in an external service.\nIn the test suite there is an example of simple login function that can be used for a\nvery simple authentication system inside the PostgreSQL database.\n-}\nmodule PostgREST.Auth\n  ( getResult\n  , getJwtDur\n  , getRole\n  , middleware\n  ) where\n\nimport qualified Data.ByteString                 as BS\nimport qualified Data.Vault.Lazy                 as Vault\nimport qualified Network.HTTP.Types.Header       as HTTP\nimport qualified Network.Wai                     as Wai\nimport qualified Network.Wai.Middleware.HttpAuth as Wai\n\nimport Data.List        (lookup)\nimport PostgREST.TimeIt (timeItT)\nimport System.IO.Unsafe (unsafePerformIO)\n\nimport PostgREST.AppState      (AppState, getConfig, getJwtCacheState,\n                                getTime)\nimport PostgREST.Auth.Jwt      (parseClaims)\nimport PostgREST.Auth.JwtCache (lookupJwtCache)\nimport PostgREST.Auth.Types    (AuthResult (..))\nimport PostgREST.Config        (AppConfig (..))\nimport PostgREST.Error         (Error (..))\n\nimport Protolude\n\n-- | Validate authorization header\n--   Parse and store JWT claims for future use in the request.\nmiddleware :: AppState -> Wai.Middleware\nmiddleware appState app req respond = do\n  conf@AppConfig{..} <- getConfig appState\n  time <- getTime appState\n\n  let token  = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)\n      parseJwt = runExceptT $ lookupJwtCache jwtCacheState token >>= parseClaims conf time\n      jwtCacheState = getJwtCacheState appState\n\n  -- If ServerTimingEnabled -> calculate JWT validation time\n  req' <- if configServerTimingEnabled then do\n      (dur, authResult) <- timeItT parseJwt\n      pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }\n    else do\n      authResult <- parseJwt\n      pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }\n\n  app req' respond\n\nauthResultKey :: Vault.Key (Either Error AuthResult)\nauthResultKey = unsafePerformIO Vault.newKey\n{-# NOINLINE authResultKey #-}\n\ngetResult :: Wai.Request -> Maybe (Either Error AuthResult)\ngetResult = Vault.lookup authResultKey . Wai.vault\n\njwtDurKey :: Vault.Key Double\njwtDurKey = unsafePerformIO Vault.newKey\n{-# NOINLINE jwtDurKey #-}\n\ngetJwtDur :: Wai.Request -> Maybe Double\ngetJwtDur =  Vault.lookup jwtDurKey . Wai.vault\n\ngetRole :: Wai.Request -> Maybe BS.ByteString\ngetRole req = authRole <$> (rightToMaybe =<< getResult req)\n"
  },
  {
    "path": "src/PostgREST/CLI.hs",
    "content": "{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\nmodule PostgREST.CLI\n  ( main\n  , CLI (..)\n  , Command (..)\n  , readCLIShowHelp\n  ) where\n\nimport qualified Data.Aeson                 as JSON\nimport qualified Data.ByteString.Char8      as BS\nimport qualified Data.ByteString.Lazy       as LBS\nimport qualified Hasql.Transaction.Sessions as SQL\nimport qualified Options.Applicative        as O\n\nimport PostgREST.AppState    (AppState)\nimport PostgREST.Config      (AppConfig (..))\nimport PostgREST.Observation (Observation (..))\nimport PostgREST.SchemaCache (querySchemaCache)\nimport PostgREST.Version     (prettyVersion)\n\nimport qualified PostgREST.App      as App\nimport qualified PostgREST.AppState as AppState\nimport qualified PostgREST.Client   as Client\nimport qualified PostgREST.Config   as Config\n\nimport Protolude\n\n\nmain :: CLI -> IO ()\nmain CLI{cliCommand, cliPath} = do\n  conf <-\n    either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty\n  case cliCommand of\n    Client adminCmd -> runClientCommand conf adminCmd\n    Run runCmd      -> runAppCommand conf runCmd\n\n-- | Run command using http-client to communicate with an already running postgrest\nrunClientCommand :: AppConfig -> ClientCommand -> IO ()\nrunClientCommand conf CmdReady = Client.ready conf\n\n-- | Run postgrest with command\nrunAppCommand :: AppConfig -> RunCommand -> IO ()\nrunAppCommand conf@AppConfig{..} runCmd = do\n  -- Per https://github.com/PostgREST/postgrest/issues/268, we want to\n  -- explicitly close the connections to PostgreSQL on shutdown.\n  -- 'AppState.destroy' takes care of that.\n  bracket\n    (AppState.init conf)\n    AppState.destroy\n    (\\appState -> case runCmd of\n      CmdDumpConfig -> do\n        when configDbConfig $ AppState.readInDbConfig True appState\n        putStr . Config.toText =<< AppState.getConfig appState\n      CmdDumpSchema -> do\n        when configDbConfig $ AppState.readInDbConfig True appState\n        putStrLn =<< dumpSchema appState\n      CmdRun -> App.run appState)\n\n-- | Dump SchemaCache schema to JSON\ndumpSchema :: AppState -> IO LBS.ByteString\ndumpSchema appState = do\n  conf@AppConfig{..} <- AppState.getConfig appState\n  result <-\n    let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in\n    AppState.usePool appState\n      (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)\n  case result of\n    Left e -> do\n      let observer = AppState.getObserver appState\n      observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e\n      exitFailure\n    Right sCache -> return $ JSON.encode sCache\n\n-- | Command line interface options\ndata CLI = CLI\n  { cliCommand :: Command\n  , cliPath    :: Maybe FilePath\n  }\n\ndata Command\n  = Client ClientCommand\n  | Run RunCommand\n\ndata ClientCommand\n  = CmdReady\n\ndata RunCommand\n  = CmdRun\n  | CmdDumpConfig\n  | CmdDumpSchema\n\n-- | Read command line interface options. Also prints help.\nreadCLIShowHelp :: IO CLI\nreadCLIShowHelp =\n  O.customExecParser prefs opts\n  where\n    prefs = O.prefs $ O.showHelpOnError <> O.showHelpOnEmpty\n    opts = O.info parser $ O.fullDesc <> progDesc\n    parser = O.helper <*> versionFlag <*> exampleParser <*> cliParser\n\n    progDesc =\n      O.progDesc $\n        \"PostgREST \"\n        <> BS.unpack prettyVersion\n        <> \" / create a REST API to an existing Postgres database\"\n\n    versionFlag =\n      O.infoOption (\"PostgREST \" <> BS.unpack prettyVersion) $\n        O.long \"version\"\n        <> O.short 'v'\n        <> O.help \"Show the version information\"\n\n    exampleParser =\n      O.infoOption Config.exampleConfigFile $\n        O.long \"example\"\n        <> O.short 'e'\n        <> O.help \"Show an example configuration file\"\n\n    cliParser :: O.Parser CLI\n    cliParser =\n      CLI\n        <$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag)\n        <*> O.optional configFileOption\n\n    configFileOption =\n      O.strArgument $\n        O.metavar \"FILENAME\"\n        <> O.help \"Path to configuration file\"\n\n    dumpConfigFlag =\n      O.flag (Run CmdRun) (Run CmdDumpConfig) $\n        O.long \"dump-config\"\n        <> O.help \"Dump loaded configuration and exit\"\n\n    dumpSchemaFlag =\n      O.flag (Run CmdRun) (Run CmdDumpSchema) $\n        O.long \"dump-schema\"\n        <> O.help \"Dump loaded schema as JSON and exit (for debugging, output structure is unstable)\"\n\n    readyFlag =\n      O.flag (Run CmdRun) (Client CmdReady) $\n        O.long \"ready\"\n        <> O.help \"Checks the health of PostgREST by doing a request on the admin server /ready endpoint\"\n"
  },
  {
    "path": "src/PostgREST/Cache/Sieve.hs",
    "content": "{-|\nModule      : PostgREST.Cache.Sieve\nDescription : PostgREST cache implementation based on Sieve algorithm.\n\nThis module provides implementation of a mutable cache on Sieve algorithm.\n-}\n{-# LANGUAGE DataKinds       #-}\n{-# LANGUAGE GADTs           #-}\n{-# LANGUAGE LambdaCase      #-}\n{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE PolyKinds       #-}\n{-# LANGUAGE RecordWildCards #-}\n{-# LANGUAGE RecursiveDo     #-}\n{-# LANGUAGE StrictData      #-}\n{-# LANGUAGE TupleSections   #-}\n\nmodule PostgREST.Cache.Sieve (\n      Cache\n    , CacheConfig (..)\n    , Discard (..)\n    , alwaysValid\n    , cache\n    , cacheIO\n    , cached\n)\nwhere\n\nimport           Control.Concurrent.STM\nimport           Control.Monad.Extra    (whileM)\nimport           Data.Some\nimport qualified Focus                  as F\nimport           Protolude              hiding (elem, head)\nimport qualified StmHamt.SizedHamt      as SH\n\ndata ListNode k v (b :: Bool) = ListNode {\n        nextPtr        :: NodePtr k v,\n        prevNextPtrPtr :: NodePtrPtr k v,\n        elem           :: NodeElem k v b\n    }\n\ndata NodeElem :: Type -> Type -> Bool -> Type where\n    Head :: {\n            entries          :: SH.SizedHamt (HamtEntry k v),\n            finger           :: NodePtrPtr k v\n        } -> NodeElem k v False\n    Entry :: Hashable k => {\n            visited :: TVar Bool,\n            ekey :: k,\n            entryValue :: v\n        } -> NodeElem k v True\n\ntype HamtEntry k v = ListNode k v True\ntype AnyNode k v = Some (ListNode k v)\ntype NodePtr k v = TVar (AnyNode k v)\ntype NodePtrPtr k v = TVar (NodePtr k v)\n\ndata Discard m v = Refresh (m ()) | Invalid (m v)\n\ndata Cache m k v = (MonadIO m, Hashable k) => Cache (ListNode k v False) (CacheConfig m k v)\n\ndata CacheConfig m k v = CacheConfig {\n        maxSize          :: STM Int,\n        load             :: k -> m v,\n        requestListener  :: Bool -> m (),\n        evictionListener :: k -> v -> m (),\n        validator        :: m (k -> v -> Maybe (Discard m v))\n}\n\nalwaysValid :: Applicative m => m (k -> v -> Maybe (Discard m v))\nalwaysValid = pure (const . const Nothing)\n\ncacheIO :: (MonadIO m, Hashable k) => CacheConfig m k v -> IO (Cache m k v)\ncacheIO = atomically . cache\n\ncache :: (MonadIO m, Hashable k) => CacheConfig m k v -> STM (Cache m k v)\ncache cacheConfig = mdo\n    tail <- newTVar (Some head)\n    entries <- SH.new\n    finger <- newTVar tail\n    head <- ListNode tail <$> newTVar tail <*> pure Head {..}\n    pure $ Cache head cacheConfig\n\ncached :: Cache m k v -> k -> m v\ncached (Cache head@ListNode{prevNextPtrPtr=neck, elem=Head{..}} CacheConfig{..}) k = do\n    checkValid <- validator\n    tryMaybe\n        -- Fast path: lookup value, update stats and return the value if found and valid\n        ((liftIO . atomically) (lookup checkValid) >>= notify (requestListener . isJust) >>= validate)\n        -- Slow path: load/calculate value and insert it (if still not found)\n        (do\n            value <- load k\n            whileM (not <$> tryInsert value)\n            pure value)\n    where\n        tryMaybe f notFound = f >>= maybe notFound pure\n\n        notify = ((<$) <*>)\n\n        validate = fmap join . traverse (\\case\n            -- valid value\n            (Right v) -> pure $ Just v\n            -- refresh value\n            (Left (Refresh act)) -> act $> Nothing\n            -- discard value and return alt result\n            (Left (Invalid res)) -> Just <$> res)\n\n        lookup checkValid = SH.focus focus (ekey . elem) k entries\n            where\n                focus = F.Focus\n                    -- not found\n                    (pure (Nothing, F.Leave))\n                    -- found\n                    -- check entry validity\n                    (\\e@ListNode{elem=Entry{visited, entryValue}} ->\n                        maybe\n                            -- entry valid\n                            (mark visited True $> (Just $ Right entryValue, F.Leave))\n                            -- entry invalid\n                            -- remove it\n                            ((removeEntry e $>) . (, F.Remove) . Just . Left)\n                            (checkValid k entryValue)\n                    )\n\n        mark t b = whenM ((/= b) <$> readTVar t) (writeTVar t b)\n\n        -- perform a single entry eviction and possibly insertion atomically\n        -- returning False if could not insert\n        -- (either because entry currently pointed by the finger was visited\n        --  or because after this entry eviction the cache is still full)\n        -- so that other threads don't have to wait when visiting entries.\n        -- First check if entry is still not in the cache - this time inside transaction.\n        --\n        -- Execute evictionListener if an entry was evicted\n        tryInsert value = do\n            (result, evicted) <- liftIO . atomically $ do\n                -- Use SH.focus to performa a single lookup instead of 2\n                -- we cannot modify Hamt from inside focus\n                -- so if there is any entry to remove\n                -- we need to delete it after\n                (res, evictedKey) <- SH.focus focus (ekey . elem) k entries\n                case evictedKey of\n                    (Just Entry{ekey=entryKey, entryValue}) -> do\n                        SH.focus F.delete (ekey . elem) entryKey entries\n                        pure (res, evictionListener entryKey entryValue)\n                    Nothing -> pure (res, pure ())\n\n            evicted $> result\n            where\n                focus = F.Focus (do\n                    (hasSpace, evictedKey) <- evictionStep\n                    if hasSpace then do\n                        entry <- newLinkedEntry value\n                        -- done, maybe evicted, insert entry\n                        pure ((True, evictedKey), F.Set entry)\n                    else\n                        -- not done, maybe evicted, don't modify entries\n                        pure ((False, evictedKey), F.Leave))\n                    -- Entry found case\n                    (\\ListNode{elem=Entry{visited}} -> do\n                        -- mark as visited\n                        mark visited True\n                        -- done, no evictions, don't modify entries\n                        pure ((True, Nothing), F.Leave))\n\n        -- if the cache is full precoesses a single node\n        -- removing it if it is marked as unvisited\n        -- or clearing visited mark\n        -- returns True if there is space in the cache\n        -- puts evictionListener in state if an entry was evicted\n        evictionStep = do\n            currDiff <- liftA2 (-) (SH.size entries) (max 1 <$> maxSize)\n            if currDiff >= 0 then do\n                -- no space in the cache\n                -- need to evict an entry\n                (nextFinger, evictedKey) <- readTVar finger >>= evict\n                writeTVar finger nextFinger\n                -- return if enough space and evicted key if any\n                pure (isJust evictedKey && currDiff == 0, evictedKey)\n            else\n                -- there is space in the cache\n                pure (True, Nothing)\n\n        evict :: TVar (Some (ListNode k v)) -> STM (NodePtr k v, Maybe (NodeElem k v True))\n        evict = readTVar >=> \\case\n            (Some e@ListNode{nextPtr, prevNextPtrPtr, elem=elem@Entry{visited}}) -> do\n                ifM (readTVar visited)\n\n                    (writeTVar visited False $> (nextPtr, Nothing))\n\n                    (unlinkEntry e *> fmap (, Just elem) (readTVar prevNextPtrPtr))\n            -- skip head\n            (Some ListNode{nextPtr, elem=Head{}}) -> evict nextPtr\n\n        unlinkEntry :: HamtEntry k v -> STM ()\n        unlinkEntry (ListNode{nextPtr, prevNextPtrPtr=currPrev}) = do\n            nextEntry <- readTVar nextPtr\n            withSome nextEntry $ \\e -> do\n                prevNextPtr <- readTVar currPrev\n                writeTVar (prevNextPtrPtr e) prevNextPtr\n                writeTVar prevNextPtr nextEntry\n\n        newLinkedEntry v = do\n            oldNeckNextPtr <- readTVar neck\n            newNeckNextPtr <- newTVar (Some head)\n            newNeck <- ListNode newNeckNextPtr <$>\n                newTVar oldNeckNextPtr <*>\n                (Entry <$> newTVar False <*> pure k <*> pure v)\n            -- update pointers\n            writeTVar oldNeckNextPtr (Some newNeck)\n            writeTVar neck newNeckNextPtr\n            -- return HAMT entry\n            pure newNeck\n\n        removeEntry = fmap (*>) unlinkEntry <*> adjustFinger\n\n        adjustFinger ListNode{nextPtr, prevNextPtrPtr} =\n            whenM ((nextPtr ==) <$> readTVar finger) $\n                readTVar prevNextPtrPtr >>= writeTVar finger\n"
  },
  {
    "path": "src/PostgREST/Client.hs",
    "content": "{-|\nModule      : PostgREST.Client\nDescription : PostgREST HTTP client\n-}\n{-# LANGUAGE NamedFieldPuns #-}\nmodule PostgREST.Client\n  ( ready\n  ) where\n\nimport qualified Data.Text                 as T\nimport qualified Network.HTTP.Client       as HC\nimport qualified Network.HTTP.Types.Status as HTTP\n\nimport Network.HTTP.Client (HttpException (..))\nimport System.IO           (hFlush)\n\nimport PostgREST.Config  (AppConfig (..))\nimport PostgREST.Network (isSpecialHostName)\n\nimport Protolude\n\ndata PgrstClientError\n  = NoAdminServer\n  | NoSpecialHostNamesAllowed Text\n  | PostgRESTNotReady         Text\n  | HTTPConnectionRefused     Text\n  | HTTPExceptionInvalidURL   Text\n\n-- | This is invoked by the CLI \"--ready\" flag.\n--   The http-client sends and a request to /ready endpoint\n--   and exits with success or failure.\nready :: AppConfig -> IO ()\nready AppConfig{configAdminServerHost, configAdminServerPort} = do\n\n  client <- HC.newManager HC.defaultManagerSettings\n  readyURL <- getURL\n  req <- HC.parseRequest (T.unpack readyURL) `catch` handleHttpException\n  resp <- HC.httpLbs req client `catch` handleHttpException\n\n  let status = HC.responseStatus resp\n\n  if status >= HTTP.status200 && status < HTTP.status300\n    then printAndExitWithSuccess $ \"OK: \" <> readyURL\n    else printAndExitWithFailure $ clientErrorMsg (PostgRESTNotReady readyURL)\n        where\n          getURL :: IO Text\n          getURL =\n            -- Here, we have three cases:\n            -- 1. If the admin port config is not defined, we exit\n            --      with \"no admin server error\"\n            -- 2. Otherwise, if admin server is running, then we check if\n            --      postgrest server-host is configured with special hostname like \"*4\",\n            --      if it is, we fail with \"no special hostname allowed with \"--ready\".\n            --      The reason for this is that we can't know the actual address.\n            -- 3. Finally, if we know the \"actual\" hostname and the port, then we\n            --      construct the URL and return it.\n            case configAdminServerPort of\n              Nothing   -> printAndExitWithFailure $ clientErrorMsg NoAdminServer\n              Just port ->\n                if isSpecialHostName configAdminServerHost\n                  then printAndExitWithFailure $ clientErrorMsg (NoSpecialHostNamesAllowed configAdminServerHost)\n                  else return $ makeReadyUrl port\n\n          -- NOTE: http-client automatically resolves hostnames\n          makeReadyUrl :: Int -> Text\n          makeReadyUrl p = \"http://\" <> wrapIfIpv6 configAdminServerHost <> \":\" <> (T.pack . show) p <> \"/ready\"\n            where\n              -- IPv6 needs to wrapped in [], it has ':' as separator\n              wrapIfIpv6 :: Text -> Text\n              wrapIfIpv6 s\n                | T.any (== ':') s = \"[\" <> s <> \"]\"\n                | otherwise = s\n\n-- | Handle HTTP exception for \"http-client\" requests\nhandleHttpException :: HttpException -> IO a\nhandleHttpException (HttpExceptionRequest req _) = do\n  let url = show (HC.getUri req)\n  printAndExitWithFailure $ clientErrorMsg (HTTPConnectionRefused $ T.pack url)\nhandleHttpException (InvalidUrlException url _) = do\n  printAndExitWithFailure $ clientErrorMsg (HTTPExceptionInvalidURL $ T.pack url)\n\n-- | Print the message on stdout and exit with success\nprintAndExitWithSuccess :: Text -> IO a\nprintAndExitWithSuccess msg = putStrLn (T.unpack msg) >> hFlush stdout >> exitSuccess\n\n-- | Print the message on stderr and exit with failure\nprintAndExitWithFailure :: Text -> IO a\nprintAndExitWithFailure msg = hPutStrLn stderr (T.unpack msg) >> hFlush stderr >> exitWith (ExitFailure 1)\n\n-- | Pgrst client error to error message\nclientErrorMsg :: PgrstClientError -> Text\nclientErrorMsg err = \"ERROR: \" <>\n  case err of\n    NoAdminServer -> \"Admin server is not running. Please check admin-server-port config.\"\n    NoSpecialHostNamesAllowed host ->\n      \"The `--ready` flag cannot be used when server-host is configured as \\\"\" <> host <> \"\\\". \"\n      <> \"Please update your server-host config to \\\"localhost\\\".\"\n    PostgRESTNotReady url -> url\n    HTTPConnectionRefused url -> \"connection refused to \" <> url\n    HTTPExceptionInvalidURL url -> \"invalid url - \" <> url\n"
  },
  {
    "path": "src/PostgREST/Config/Database.hs",
    "content": "{-# LANGUAGE QuasiQuotes #-}\n\nmodule PostgREST.Config.Database\n  ( pgVersionStatement\n  , queryDbSettings\n  , queryPgVersion\n  , queryRoleSettings\n  , RoleSettings\n  , RoleIsolationLvl\n  , TimezoneNames\n  , toIsolationLevel\n  ) where\n\nimport Control.Arrow ((***))\n\nimport PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)\n\nimport qualified Data.HashMap.Strict as HM\n\nimport qualified Hasql.Decoders             as HD\nimport qualified Hasql.Encoders             as HE\nimport           Hasql.Session              (Session, statement)\nimport qualified Hasql.Statement            as SQL\nimport qualified Hasql.Transaction          as SQL\nimport qualified Hasql.Transaction.Sessions as SQL\n\nimport NeatInterpolation (trimming)\n\nimport Protolude\n\ntype RoleSettings     = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))\ntype RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel\ntype TimezoneNames    = Set Text -- cache timezone names for prefer timezone=\n\ntoIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel\ntoIsolationLevel a = case a of\n  \"repeatable read\" -> SQL.RepeatableRead\n  \"serializable\"    -> SQL.Serializable\n  _                 -> SQL.ReadCommitted\n\nprefix :: Text\nprefix = \"pgrst.\"\n\n-- | In-db settings names\ndbSettingsNames :: [Text]\ndbSettingsNames =\n  (prefix <>) <$>\n  [\"db_aggregates_enabled\"\n  ,\"client_error_verbosity\"\n  ,\"db_anon_role\"\n  ,\"db_pre_config\"\n  ,\"db_extra_search_path\"\n  ,\"db_max_rows\"\n  ,\"db_plan_enabled\"\n  ,\"db_pre_request\"\n  ,\"db_prepared_statements\"\n  ,\"db_root_spec\"\n  ,\"db_schemas\"\n  ,\"db_tx_end\"\n  ,\"db_hoisted_tx_settings\"\n  ,\"jwt_aud\"\n  ,\"jwt_role_claim_key\"\n  ,\"jwt_secret\"\n  ,\"jwt_secret_is_base64\"\n  ,\"jwt_cache_max_lifetime\"\n  ,\"openapi_mode\"\n  ,\"openapi_security_active\"\n  ,\"openapi_server_proxy_uri\"\n  ,\"server_cors_allowed_origins\"\n  ,\"server_trace_header\"\n  ,\"server_timing_enabled\"\n  ]\n\nqueryPgVersion :: Bool -> Session PgVersion\nqueryPgVersion prepared = statement mempty $ pgVersionStatement prepared\n\npgVersionStatement :: Bool -> SQL.Statement () PgVersion\npgVersionStatement = SQL.Statement sql HE.noParams versionRow\n  where\n    sql = \"SELECT current_setting('server_version_num')::integer, current_setting('server_version'), version()\"\n    versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text <*> column HD.text\n\n-- | Query the in-database configuration. The settings have the following priorities:\n--\n-- 1. Role + with database-specific settings:\n--    ALTER ROLE authenticator IN DATABASE postgres SET <prefix>jwt_aud = 'val';\n-- 2. Role + with settings:\n--    ALTER ROLE authenticator SET <prefix>jwt_aud = 'overridden';\n-- 3. pre-config function:\n--    CREATE FUNCTION pre_config() .. PERFORM set_config(<prefix>jwt_aud, 'pre_config_aud'..)\n--\n-- The example above will result in <prefix>jwt_aud = 'val'\n-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'\nqueryDbSettings :: Maybe Text -> Bool -> Session [(Text, Text)]\nqueryDbSettings preConfFunc prepared =\n  let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in\n  transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared\n  where\n    sql = encodeUtf8 [trimming|\n      WITH\n      role_setting AS (\n        SELECT setdatabase as database,\n               unnest(setconfig) as setting\n        FROM pg_catalog.pg_db_role_setting\n        WHERE setrole = CURRENT_USER::regrole::oid\n          AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))\n      ),\n      kv_settings AS (\n        SELECT database,\n               substr(setting, 1, strpos(setting, '=') - 1) as k,\n               substr(setting, strpos(setting, '=') + 1) as v\n        FROM role_setting\n        ${preConfigF}\n      )\n      SELECT DISTINCT ON (key)\n             replace(k, '${prefix}', '') AS key,\n             v AS value\n      FROM kv_settings\n      WHERE k = ANY($$1) AND v IS NOT NULL\n      ORDER BY key, database DESC NULLS LAST;\n    |]\n    preConfigF = case preConfFunc of\n      Nothing   -> mempty\n      Just func -> [trimming|\n          UNION\n          SELECT\n            null as database,\n            x as k,\n            current_setting(x, true) as v\n          FROM unnest($$1) x\n          JOIN ${func}() _ ON TRUE\n      |]::Text\n    decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text\n\nqueryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)\nqueryRoleSettings pgVer prepared =\n  let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in\n  transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared\n  where\n    sql = encodeUtf8 [trimming|\n      with\n      role_setting as (\n        select r.rolname, unnest(r.rolconfig) as setting\n        from pg_auth_members m\n        join pg_roles r on r.oid = m.roleid\n        where member = current_user::regrole::oid\n      ),\n      kv_settings AS (\n        SELECT\n          rolname,\n          substr(setting, 1, strpos(setting, '=') - 1) as key,\n          lower(substr(setting, strpos(setting, '=') + 1)) as value\n        FROM role_setting\n      ),\n      iso_setting AS (\n        SELECT rolname, value\n        FROM kv_settings\n        WHERE key = 'default_transaction_isolation'\n      )\n      select\n        kv.rolname,\n        i.value as iso_lvl,\n        coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings\n      from kv_settings kv\n      join pg_settings ps on ps.name = kv.key and (ps.context = 'user' ${hasParameterPrivilege})\n      left join iso_setting i on i.rolname = kv.rolname\n      group by kv.rolname, i.value;\n    |]\n\n    hasParameterPrivilege\n      | pgVer >= pgVersion150 = \"or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')\"\n      | otherwise             = \"\"\n\n    processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)\n    processRows rs =\n      let\n        rowsWRoleSettings = [ (x, z) | (x, _, z) <- rs ]\n        rowsWIsolation    = [ (x, y) | (x, Just y, _) <- rs ]\n      in\n      ( HM.fromList $ bimap encodeUtf8 (HM.fromList . ((encodeUtf8 *** encodeUtf8) <$>)) <$> rowsWRoleSettings\n      , HM.fromList $ (encodeUtf8 *** toIsolationLevel) <$> rowsWIsolation\n      )\n\n    rows :: HD.Result [(Text, Maybe Text, [(Text, Text)])]\n    rows = HD.rowList $ (,,) <$> column HD.text <*> nullableColumn HD.text <*> compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text)\n\ncolumn :: HD.Value a -> HD.Row a\ncolumn = HD.column . HD.nonNullable\n\nnullableColumn :: HD.Value a -> HD.Row (Maybe a)\nnullableColumn = HD.column . HD.nullable\n\ncompositeField :: HD.Value a -> HD.Composite a\ncompositeField = HD.field . HD.nonNullable\n\ncompositeArrayColumn :: HD.Composite a -> HD.Row [a]\ncompositeArrayColumn = arrayColumn . HD.composite\n\narrayColumn :: HD.Value a -> HD.Row [a]\narrayColumn = column . HD.listArray . HD.nonNullable\n\nparam :: HE.Value a -> HE.Params a\nparam = HE.param . HE.nonNullable\n\narrayParam :: HE.Value a -> HE.Params [a]\narrayParam = param . HE.foldableArray . HE.nonNullable\n"
  },
  {
    "path": "src/PostgREST/Config/JSPath.hs",
    "content": "{-# OPTIONS_GHC -Wno-unused-do-bind #-}\n{-# LANGUAGE LambdaCase #-}\nmodule PostgREST.Config.JSPath\n  ( JSPath\n  , JSPathExp(..)\n  , FilterExp(..)\n  , dumpJSPath\n  , pRoleClaimKey\n  , walkJSPath\n  ) where\n\nimport qualified Data.Aeson                    as JSON\nimport qualified Data.Aeson.Key                as K\nimport qualified Data.Aeson.KeyMap             as KM\nimport qualified Data.Text                     as T\nimport qualified Data.Vector                   as V\nimport qualified Text.ParserCombinators.Parsec as P\n\nimport Data.Either.Combinators       (mapLeft)\nimport Text.ParserCombinators.Parsec ((<?>))\nimport Text.Read                     (read)\n\nimport Protolude\n\n\n-- | full jspath, e.g. .property[0].attr.detail[?(@ == \"role1\")]\ntype JSPath = [JSPathExp]\n\n-- NOTE: We only accept one JSPFilter expr (at the end of input)\n-- | jspath expression\ndata JSPathExp\n  = JSPKey Text                      -- .property or .\"property-dash\"\n  | JSPIdx Int                       -- [0]\n  | JSPSlice (Maybe Int) (Maybe Int) -- [0:5] or [0:] or [:5] or [:]\n  | JSPFilter FilterExp              -- [?(@ == \"match\")]\n\ndata FilterExp\n  = EqualsCond Text\n  | NotEqualsCond Text\n  | StartsWithCond Text\n  | EndsWithCond Text\n  | ContainsCond Text\n\ndumpJSPath :: JSPathExp -> Text\n-- TODO: this needs to be quoted properly for special chars\ndumpJSPath (JSPKey k) = \".\" <> show k\ndumpJSPath (JSPIdx i) = \"[\" <> show i <> \"]\"\ndumpJSPath (JSPSlice s e) = \"[\" <> maybe \"\" show s <> \":\" <> maybe \"\" show e <> \"]\"\ndumpJSPath (JSPFilter cond) = \"[?(@\" <> expr <> \")]\"\n  where\n    expr =\n      case cond of\n        EqualsCond text     -> \" == \" <> show text\n        NotEqualsCond text  -> \" != \" <> show text\n        StartsWithCond text -> \" ^== \" <> show text\n        EndsWithCond text   -> \" ==^ \" <> show text\n        ContainsCond text   -> \" *== \" <> show text\n\n-- | Evaluate JSPath on a JSON\nwalkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value\nwalkJSPath x                      []                = x\nwalkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest\nwalkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest\nwalkJSPath (Just (JSON.String str)) (JSPSlice start end:rest) =\n  let\n    len = T.length str\n\n    norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive\n    norm = fmap (\\i -> max 0 $ min len $ if i < 0 then len + i else i)\n\n    s = fromMaybe 0 $ norm start -- normalized start index\n    e = fromMaybe len $ norm end -- normalized end index\n    slicedString = if s >= e then T.empty else T.take (e-s) $ T.drop s str\n  in\n    walkJSPath (Just $ JSON.String slicedString) rest\n\nwalkJSPath (Just (JSON.Array ar)) (JSPFilter jspFilter:rest) = case jspFilter of\n    EqualsCond txt     -> walkJSPath (findFirstMatch (==) txt ar) rest\n    NotEqualsCond txt  -> walkJSPath (findFirstMatch (/=) txt ar) rest\n    StartsWithCond txt -> walkJSPath (findFirstMatch T.isPrefixOf txt ar) rest\n    EndsWithCond txt   -> walkJSPath (findFirstMatch T.isSuffixOf txt ar) rest\n    ContainsCond txt   -> walkJSPath (findFirstMatch T.isInfixOf txt ar) rest\n  where\n    findFirstMatch matchWith pattern = find (\\case\n      JSON.String txt -> pattern `matchWith` txt\n      _               -> False)\nwalkJSPath _                      _                 = Nothing\n\n-- Used for the config value \"role-claim-key\"\npRoleClaimKey :: Text -> Either Text JSPath\npRoleClaimKey selStr =\n  mapLeft show $ P.parse pJSPath (\"failed to parse role-claim-key value (\" <> toS selStr <> \")\") (toS selStr)\n\npJSPath :: P.Parser JSPath\npJSPath = P.many1 pJSPathExp <* P.eof\n\npJSPathExp :: P.Parser JSPathExp\npJSPathExp = P.try pJSPKey <|> P.try pJSPFilter <|> P.try pJSPIdx <|> pJSPSlice\n\npJSPKey :: P.Parser JSPathExp\npJSPKey = do\n  P.char '.'\n  val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf \"_$@\") <|> pQuotedValue\n  return (JSPKey val) <?> \"pJSPKey: JSPath attribute key\"\n\npJSPIdx :: P.Parser JSPathExp\npJSPIdx = do\n  P.char '['\n  num <- read <$> P.many1 P.digit\n  P.char ']'\n  return (JSPIdx num) <?> \"pJSPIdx: JSPath array index\"\n\npJSPSlice :: P.Parser JSPathExp\npJSPSlice = do\n  P.char '['\n  startSign <- P.optionMaybe $ P.char '-'\n  startIndex <- P.optionMaybe (read <$> P.many1 P.digit)\n  P.char ':'\n  endSign <- P.optionMaybe $ P.char '-'\n  endIndex <- P.optionMaybe (read <$> P.many1 P.digit)\n  P.char ']'\n  let start' = if isJust startSign then ((-1) *) <$> startIndex else startIndex\n      end'   = if isJust endSign   then ((-1) *) <$> endIndex   else endIndex\n  return (JSPSlice start' end') <?> \"pJSPSlice: JSPath string slice\"\n\npJSPFilter :: P.Parser JSPathExp\npJSPFilter = do\n  P.try $ P.string \"[?(\"\n  condition <- pFilterConditionParser\n  P.char ')'\n  P.char ']'\n  return (JSPFilter condition) <?> \"pJSPFilter: JSPath filter exp\"\n\npFilterConditionParser :: P.Parser FilterExp\npFilterConditionParser = do\n  P.char '@'\n  P.spaces\n  filt <- matchOperator\n  P.spaces\n  filt <$> pQuotedValue\n    where\n      matchOperator =\n        P.try (P.string \"==^\" $> EndsWithCond)\n        <|> P.try (P.string \"==\" $> EqualsCond)\n        <|> P.try (P.string \"!=\" $> NotEqualsCond)\n        <|> P.try (P.string \"^==\" $> StartsWithCond)\n        <|> P.try (P.string \"*==\" $> ContainsCond)\n\npQuotedValue :: P.Parser Text\npQuotedValue = toS <$> (P.char '\"' *> P.many (P.noneOf \"\\\"\") <* P.char '\"')\n"
  },
  {
    "path": "src/PostgREST/Config/PgVersion.hs",
    "content": "{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric  #-}\nmodule PostgREST.Config.PgVersion\n  ( PgVersion(..)\n  , minimumPgVersion\n  , pgVersion140\n  , pgVersion150\n  , pgVersion170\n  ) where\n\nimport qualified Data.Aeson as JSON\n\nimport Protolude\n\n\ndata PgVersion = PgVersion\n  { pgvNum      :: Int32\n  , pgvName     :: Text\n  , pgvFullName :: Text\n  }\n  deriving (Eq, Generic, JSON.ToJSON)\n\ninstance Ord PgVersion where\n  (PgVersion v1 _ _) `compare` (PgVersion v2 _ _) = v1 `compare` v2\n\n-- | Tells the minimum PostgreSQL version required by this version of PostgREST\nminimumPgVersion :: PgVersion\nminimumPgVersion = pgVersion130\n\npgVersion130 :: PgVersion\npgVersion130 = PgVersion 130000 \"13.0\" \"13.0\"\n\npgVersion140 :: PgVersion\npgVersion140 = PgVersion 140000 \"14.0\" \"14.0\"\n\npgVersion150 :: PgVersion\npgVersion150 = PgVersion 150000 \"15.0\" \"15.0\"\n\npgVersion170 :: PgVersion\npgVersion170 = PgVersion 170000 \"17.0\" \"17.0\"\n"
  },
  {
    "path": "src/PostgREST/Config/Proxy.hs",
    "content": "{-|\nModule      : PostgREST.Private.ProxyUri\nDescription : Proxy Uri validator\n-}\nmodule PostgREST.Config.Proxy\n  ( Proxy(..)\n  , isMalformedProxyUri\n  , toURI\n  ) where\n\nimport qualified Data.Text as T\n\nimport Data.Maybe  (fromJust)\nimport Network.URI (URI (..), URIAuth (..), isAbsoluteURI, parseURI)\n\nimport Protolude hiding (Proxy)\n\ndata Proxy = Proxy\n  { proxyScheme :: Text\n  , proxyHost   :: Text\n  , proxyPort   :: Integer\n  , proxyPath   :: Text\n  }\n\n{-|\n  Test whether a proxy uri is malformed or not.\n  A valid proxy uri should be an absolute uri without query and user info,\n  only http(s) schemes are valid, port number range is 1-65535.\n\n  For example\n  http://postgrest.com/openapi.json\n  https://postgrest.com:8080/openapi.json\n-}\nisMalformedProxyUri :: Text -> Bool\nisMalformedProxyUri uri\n  | isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri\n  | otherwise = True\n\ntoURI :: Text -> URI\ntoURI uri = fromJust $ parseURI (toS uri)\n\nisUriValid:: URI -> Bool\nisUriValid = fAnd [isSchemeValid, isQueryValid, isAuthorityValid]\n\nfAnd :: [a -> Bool] -> a -> Bool\nfAnd fs x = all ($ x) fs\n\nisSchemeValid :: URI -> Bool\nisSchemeValid URI {uriScheme = s}\n  | T.toLower (T.pack s) == \"https:\" = True\n  | T.toLower (T.pack s) == \"http:\" = True\n  | otherwise = False\n\nisQueryValid :: URI -> Bool\nisQueryValid URI {uriQuery = \"\"} = True\nisQueryValid _                   = False\n\nisAuthorityValid :: URI -> Bool\nisAuthorityValid URI {uriAuthority = a}\n  | isJust a = fAnd [isUserInfoValid, isHostValid, isPortValid] $ fromJust a\n  | otherwise = False\n\nisUserInfoValid :: URIAuth -> Bool\nisUserInfoValid URIAuth {uriUserInfo = \"\"} = True\nisUserInfoValid _                          = False\n\nisHostValid :: URIAuth -> Bool\nisHostValid URIAuth {uriRegName = \"\"} = False\nisHostValid _                         = True\n\nisPortValid :: URIAuth -> Bool\nisPortValid URIAuth {uriPort = \"\"} = True\nisPortValid URIAuth {uriPort = (':':p)} =\n  case readMaybe p of\n    Just i  -> i > (0 :: Integer) && i < 65536\n    Nothing -> False\nisPortValid _ = False\n"
  },
  {
    "path": "src/PostgREST/Config.hs",
    "content": "{-|\nModule      : PostgREST.Config\nDescription : Manages PostgREST configuration type and parser.\n\n-}\n{-# LANGUAGE FlexibleContexts      #-}\n{-# LANGUAGE FlexibleInstances     #-}\n{-# LANGUAGE LambdaCase            #-}\n{-# LANGUAGE MultiParamTypeClasses #-}\n{-# LANGUAGE RecordWildCards       #-}\n{-# OPTIONS_GHC -fno-warn-type-defaults #-}\n\nmodule PostgREST.Config\n  ( AppConfig (..)\n  , Environment\n  , JSPath\n  , JSPathExp(..)\n  , FilterExp(..)\n  , LogLevel(..)\n  , OpenAPIMode(..)\n  , Proxy(..)\n  , toText\n  , isMalformedProxyUri\n  , readAppConfig\n  , readPGRSTEnvironment\n  , toURI\n  , parseSecret\n  , addFallbackAppName\n  , addTargetSessionAttrs\n  , exampleConfigFile\n  , audMatchesCfg\n  , Verbosity (..)\n  ) where\n\nimport qualified Data.Aeson             as JSON\nimport qualified Data.ByteString        as BS\nimport qualified Data.ByteString.Base64 as B64\nimport qualified Data.CaseInsensitive   as CI\nimport qualified Data.Configurator      as C\nimport qualified Data.Map.Strict        as M\nimport qualified Data.String            as S\nimport qualified Data.Text              as T\nimport qualified Data.Text.Encoding     as T\nimport qualified Jose.Jwa               as JWT\nimport qualified Jose.Jwk               as JWT\n\nimport Control.Monad           (fail)\nimport Data.Either.Combinators (mapLeft)\nimport Data.List               (lookup)\nimport Data.List.NonEmpty      (fromList, toList)\nimport Data.Maybe              (fromJust)\nimport Data.Scientific         (floatingOrInteger)\nimport Jose.Jwk                (Jwk, JwkSet)\nimport Network.URI             (escapeURIString, isURI,\n                                isUnescapedInURIComponent)\nimport Numeric                 (readOct, showOct)\nimport System.Environment      (getEnvironment)\nimport System.Posix.Types      (FileMode)\n\nimport PostgREST.Config.Database         (RoleIsolationLvl,\n                                          RoleSettings)\nimport PostgREST.Config.JSPath           (FilterExp (..), JSPath,\n                                          JSPathExp (..), dumpJSPath,\n                                          pRoleClaimKey)\nimport PostgREST.Config.Proxy            (Proxy (..),\n                                          isMalformedProxyUri, toURI)\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),\n                                          toQi)\n\nimport Protolude hiding (Proxy, toList)\n\naudMatchesCfg :: AppConfig -> Text -> Bool\naudMatchesCfg =  maybe (const True) (==) . configJwtAudience\n\ndata AppConfig = AppConfig\n  { configAppSettings              :: [(Text, Text)]\n  , configClientErrorVerbosity     :: Verbosity\n  , configDbAggregates             :: Bool\n  , configDbAnonRole               :: Maybe BS.ByteString\n  , configDbChannel                :: Text\n  , configDbChannelEnabled         :: Bool\n  , configDbExtraSearchPath        :: [Text]\n  , configDbHoistedTxSettings      :: [Text]\n  , configDbMaxRows                :: Maybe Integer\n  , configDbPlanEnabled            :: Bool\n  , configDbPoolSize               :: Int\n  , configDbPoolAcquisitionTimeout :: Int\n  , configDbPoolMaxLifetime        :: Int\n  , configDbPoolMaxIdletime        :: Int\n  , configDbPoolAutomaticRecovery  :: Bool\n  , configDbPreRequest             :: Maybe QualifiedIdentifier\n  , configDbPreparedStatements     :: Bool\n  , configDbRootSpec               :: Maybe QualifiedIdentifier\n  , configDbSchemas                :: NonEmpty Text\n  , configDbConfig                 :: Bool\n  , configDbPreConfig              :: Maybe QualifiedIdentifier\n  , configDbTxAllowOverride        :: Bool\n  , configDbTxRollbackAll          :: Bool\n  , configDbUri                    :: Text\n  , configFilePath                 :: Maybe FilePath\n  , configJWKS                     :: Maybe JwkSet\n  , configJwtAudience              :: Maybe Text\n  , configJwtRoleClaimKey          :: JSPath\n  , configJwtSecret                :: Maybe BS.ByteString\n  , configJwtSecretIsBase64        :: Bool\n  , configJwtCacheMaxEntries       :: Int\n  , configLogLevel                 :: LogLevel\n  , configLogQuery                 :: Bool\n  , configOpenApiMode              :: OpenAPIMode\n  , configOpenApiSecurityActive    :: Bool\n  , configOpenApiServerProxyUri    :: Maybe Text\n  , configServerCorsAllowedOrigins :: Maybe [Text]\n  , configServerHost               :: Text\n  , configServerPort               :: Int\n  , configServerTraceHeader        :: Maybe (CI.CI BS.ByteString)\n  , configServerTimingEnabled      :: Bool\n  , configServerUnixSocket         :: Maybe FilePath\n  , configServerUnixSocketMode     :: FileMode\n  , configAdminServerHost          :: Text\n  , configAdminServerPort          :: Maybe Int\n  , configRoleSettings             :: RoleSettings\n  , configRoleIsoLvl               :: RoleIsolationLvl\n  , configInternalSCQuerySleep     :: Maybe Int32\n  , configInternalSCLoadSleep      :: Maybe Int32\n  , configInternalSCRelLoadSleep   :: Maybe Int32\n  }\n\ndata LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug\n  deriving (Eq, Ord)\n\ndumpLogLevel :: LogLevel -> Text\ndumpLogLevel = \\case\n  LogCrit  -> \"crit\"\n  LogError -> \"error\"\n  LogWarn  -> \"warn\"\n  LogInfo  -> \"info\"\n  LogDebug -> \"debug\"\n\ndata Verbosity\n  = Minimal\n  | Verbose\n\ndumpClientErrorVerbosity :: Verbosity -> Text\ndumpClientErrorVerbosity = \\case\n  Minimal -> \"minimal\"\n  Verbose -> \"verbose\"\n\ndata OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled\n  deriving Eq\n\ndumpOpenApiMode :: OpenAPIMode -> Text\ndumpOpenApiMode = \\case\n  OAFollowPriv -> \"follow-privileges\"\n  OAIgnorePriv -> \"ignore-privileges\"\n  OADisabled   -> \"disabled\"\n\n-- | Dump the config\ntoText :: AppConfig -> Text\ntoText conf =\n  unlines $ (\\(k, v) -> k <> \" = \" <> v) <$> pgrstSettings ++ appSettings\n  where\n    -- apply conf to all pgrst settings\n    pgrstSettings = (\\(k, v) -> (k, v conf)) <$>\n      [(\"client-error-verbosity\",    q . dumpClientErrorVerbosity . configClientErrorVerbosity)\n      ,(\"db-aggregates-enabled\",         T.toLower . show . configDbAggregates)\n      ,(\"db-anon-role\",              q . T.decodeUtf8 . fromMaybe \"\" . configDbAnonRole)\n      ,(\"db-channel\",                q . configDbChannel)\n      ,(\"db-channel-enabled\",            T.toLower . show . configDbChannelEnabled)\n      ,(\"db-extra-search-path\",      q . T.intercalate \",\" . configDbExtraSearchPath)\n      ,(\"db-hoisted-tx-settings\",    q . T.intercalate \",\" . configDbHoistedTxSettings)\n      ,(\"db-max-rows\",                   maybe \"\\\"\\\"\" show . configDbMaxRows)\n      ,(\"db-plan-enabled\",               T.toLower . show . configDbPlanEnabled)\n      ,(\"db-pool\",                       show . configDbPoolSize)\n      ,(\"db-pool-acquisition-timeout\",   show . configDbPoolAcquisitionTimeout)\n      ,(\"db-pool-max-lifetime\",          show . configDbPoolMaxLifetime)\n      ,(\"db-pool-max-idletime\",          show . configDbPoolMaxIdletime)\n      ,(\"db-pool-automatic-recovery\",    T.toLower . show . configDbPoolAutomaticRecovery)\n      ,(\"db-pre-request\",            q . maybe mempty dumpQi . configDbPreRequest)\n      ,(\"db-prepared-statements\",        T.toLower . show . configDbPreparedStatements)\n      ,(\"db-root-spec\",              q . maybe mempty dumpQi . configDbRootSpec)\n      ,(\"db-schemas\",                q . T.intercalate \",\" . toList . configDbSchemas)\n      ,(\"db-config\",                     T.toLower . show . configDbConfig)\n      ,(\"db-pre-config\",             q . maybe mempty dumpQi . configDbPreConfig)\n      ,(\"db-tx-end\",                 q . showTxEnd)\n      ,(\"db-uri\",                    q . configDbUri)\n      ,(\"jwt-aud\",                   q . fromMaybe mempty . configJwtAudience)\n      ,(\"jwt-role-claim-key\",        q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)\n      ,(\"jwt-secret\",                q . T.decodeUtf8 . showJwtSecret)\n      ,(\"jwt-secret-is-base64\",          T.toLower . show . configJwtSecretIsBase64)\n      ,(\"jwt-cache-max-entries\",         show . configJwtCacheMaxEntries)\n      ,(\"log-level\",                 q . dumpLogLevel . configLogLevel)\n      ,(\"log-query\",                     T.toLower . show . configLogQuery)\n      ,(\"openapi-mode\",              q . dumpOpenApiMode . configOpenApiMode)\n      ,(\"openapi-security-active\",       T.toLower . show . configOpenApiSecurityActive)\n      ,(\"openapi-server-proxy-uri\",  q . fromMaybe mempty . configOpenApiServerProxyUri)\n      ,(\"server-cors-allowed-origins\",      q . maybe \"\" (T.intercalate \",\") . configServerCorsAllowedOrigins)\n      ,(\"server-host\",               q . configServerHost)\n      ,(\"server-port\",                   show . configServerPort)\n      ,(\"server-trace-header\",       q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)\n      ,(\"server-timing-enabled\",         T.toLower . show . configServerTimingEnabled)\n      ,(\"server-unix-socket\",        q . maybe mempty T.pack . configServerUnixSocket)\n      ,(\"server-unix-socket-mode\",   q . T.pack . showSocketMode)\n      ,(\"admin-server-host\",         q . configAdminServerHost)\n      ,(\"admin-server-port\",             maybe \"\\\"\\\"\" show . configAdminServerPort)\n      ]\n\n    -- quote all app.settings\n    appSettings = second q <$> configAppSettings conf\n\n    -- quote strings and replace \" with \\\"\n    q s = \"\\\"\" <> T.replace \"\\\"\" \"\\\\\\\"\" s <> \"\\\"\"\n\n    dumpQi :: QualifiedIdentifier -> Text\n    dumpQi (QualifiedIdentifier s i) =\n      (if T.null s then mempty else s <> \".\") <> i\n\n    showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of\n      ( False, False ) -> \"commit\"\n      ( False, True  ) -> \"commit-allow-override\"\n      ( True , False ) -> \"rollback\"\n      ( True , True  ) -> \"rollback-allow-override\"\n    showJwtSecret c\n      | configJwtSecretIsBase64 c = B64.encode secret\n      | otherwise                 = secret\n      where\n        secret = fromMaybe mempty $ configJwtSecret c\n    showSocketMode c = showOct (configServerUnixSocketMode c) mempty\n\n-- This class is needed for the polymorphism of overrideFromDbOrEnvironment\n-- because C.required and C.optional have different signatures\nclass JustIfMaybe a b where\n  justIfMaybe :: a -> b\n\ninstance JustIfMaybe a a where\n  justIfMaybe = identity\n\ninstance JustIfMaybe a (Maybe a) where\n  justIfMaybe = Just\n\n-- | Reads and parses the config and overrides its parameters from env vars,\n-- files or db settings.\nreadAppConfig :: [(Text, Text)] -> Maybe FilePath -> Maybe Text -> RoleSettings -> RoleIsolationLvl -> IO (Either Text AppConfig)\nreadAppConfig dbSettings optPath prevDbUri roleSettings roleIsolationLvl = do\n  env <- readPGRSTEnvironment\n  -- if no filename provided, start with an empty map to read config from environment\n  conf <- maybe (return $ Right M.empty) loadConfig optPath\n\n  case C.runParser (parser optPath env dbSettings roleSettings roleIsolationLvl) =<< mapLeft show conf of\n    Left err ->\n      return . Left $ \"Error in config \" <> err\n    Right parsedConfig ->\n      mapLeft show <$> decodeLoadFiles parsedConfig\n  where\n    -- Both C.ParseError and IOError are shown here\n    loadConfig :: FilePath -> IO (Either SomeException C.Config)\n    loadConfig = try . C.load\n\n    decodeLoadFiles :: AppConfig -> IO (Either IOException AppConfig)\n    decodeLoadFiles parsedConfig = try $\n      decodeJWKS =<<\n      decodeSecret =<<\n      readSecretFile =<<\n      readDbUriFile prevDbUri parsedConfig\n\nparser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig\nparser optPath env dbSettings roleSettings roleIsolationLvl =\n  AppConfig\n    <$> parseAppSettings \"app.settings\"\n    <*> parseErrorVerbosity \"client-error-verbosity\"\n    <*> (fromMaybe False <$> optBool \"db-aggregates-enabled\")\n    <*> (fmap encodeUtf8 <$> optString \"db-anon-role\")\n    <*> (fromMaybe \"pgrst\" <$> optString \"db-channel\")\n    <*> (fromMaybe True <$> optBool \"db-channel-enabled\")\n    <*> (maybe [\"public\"] splitOnCommasEmptyable <$> optStringEmptyable \"db-extra-search-path\")\n    <*> (maybe defaultHoistedAllowList splitOnCommas <$> optString \"db-hoisted-tx-settings\")\n    <*> optWithAlias (optInt \"db-max-rows\")\n                     (optInt \"max-rows\")\n    <*> (fromMaybe False <$> optBool \"db-plan-enabled\")\n    <*> (fromMaybe 10 <$> optInt \"db-pool\")\n    <*> (fromMaybe 10 <$> optInt \"db-pool-acquisition-timeout\")\n    <*> (fromMaybe 1800 <$> optInt \"db-pool-max-lifetime\")\n    <*> (fromMaybe 30 <$> optWithAlias (optInt \"db-pool-timeout\")\n                                       (optInt \"db-pool-max-idletime\"))\n    <*> (fromMaybe True <$> optBool \"db-pool-automatic-recovery\")\n    <*> (fmap toQi <$> optWithAlias (optString \"db-pre-request\")\n                                    (optString \"pre-request\"))\n    <*> (fromMaybe True <$> optBool \"db-prepared-statements\")\n    <*> (fmap toQi <$> optWithAlias (optString \"db-root-spec\")\n                                    (optString \"root-spec\"))\n    <*> parseDbSchemas \"db-schemas\" \"db-schema\"\n    <*> (fromMaybe True <$> optBool \"db-config\")\n    <*> (fmap toQi <$> optString \"db-pre-config\")\n    <*> parseTxEnd \"db-tx-end\" snd\n    <*> parseTxEnd \"db-tx-end\" fst\n    <*> (fromMaybe \"postgresql://\" <$> optString \"db-uri\")\n    <*> pure optPath\n    <*> pure Nothing\n    <*> optStringOrURI \"jwt-aud\"\n    <*> parseRoleClaimKey \"jwt-role-claim-key\" \"role-claim-key\"\n    <*> (fmap encodeUtf8 <$> optString \"jwt-secret\")\n    <*> (fromMaybe False <$> optWithAlias\n          (optBool \"jwt-secret-is-base64\")\n          (optBool \"secret-is-base64\"))\n    <*> (fromMaybe 1000 <$> optInt \"jwt-cache-max-entries\")\n    <*> parseLogLevel \"log-level\"\n    <*> (fromMaybe False <$> optBool \"log-query\")\n    <*> parseOpenAPIMode \"openapi-mode\"\n    <*> (fromMaybe False <$> optBool \"openapi-security-active\")\n    <*> parseOpenAPIServerProxyURI \"openapi-server-proxy-uri\"\n    <*> parseCORSAllowedOrigins \"server-cors-allowed-origins\"\n    <*> (defaultServerHost <$> optString \"server-host\")\n    <*> parseServerPort \"server-port\"\n    <*> (fmap (CI.mk . encodeUtf8) <$> optString \"server-trace-header\")\n    <*> (fromMaybe False <$> optBool \"server-timing-enabled\")\n    <*> (fmap T.unpack <$> optString \"server-unix-socket\")\n    <*> parseSocketFileMode \"server-unix-socket-mode\"\n    <*> (defaultServerHost <$> optWithAlias (optString \"admin-server-host\")\n                                            (optString \"server-host\"))\n    <*> parseAdminServerPort \"admin-server-port\"\n    <*> pure roleSettings\n    <*> pure roleIsolationLvl\n    <*> optInt \"internal-schema-cache-query-sleep\"\n    <*> optInt \"internal-schema-cache-load-sleep\"\n    <*> optInt \"internal-schema-cache-relationship-load-sleep\"\n  where\n    parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity\n    parseErrorVerbosity k =\n      optString k >>= \\case\n        Nothing        -> pure Verbose -- default\n        Just \"minimal\" -> pure Minimal\n        Just \"verbose\" -> pure Verbose\n        Just _         -> fail \"Invalid client-error-verbosity. Check your configuration.\"\n\n    parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]\n    parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value\n      where\n        addFromEnv f = M.toList $ M.union fromEnv $ M.fromList f\n        fromEnv = M.mapKeys fromJust $ M.filterWithKey (\\k _ -> isJust k) $ M.mapKeys normalize env\n        normalize k = (\"app.settings.\" <>) <$> T.stripPrefix \"PGRST_APP_SETTINGS_\" (toS k)\n\n    parseServerPort :: C.Key -> C.Parser C.Config Int\n    parseServerPort k = fromMaybe 3000 <$> optInt k\n\n    parseAdminServerPort :: C.Key -> C.Parser C.Config (Maybe Int)\n    parseAdminServerPort k = do\n      serverPort <- parseServerPort \"server-port\"\n      optInt k >>= \\case\n        Nothing -> pure Nothing\n        Just asp | asp == serverPort -> fail \"admin-server-port cannot be the same as server-port\"\n                 | otherwise         -> pure $ Just asp\n\n    parseDbSchemas :: C.Key -> C.Key -> C.Parser C.Config (NonEmpty Text)\n    parseDbSchemas k al =\n      optWithAlias (optString k) (optString al) >>= \\case\n        Nothing  -> pure $ fromList [\"public\"]\n        Just s\n          | \"pg_catalog\"         `elem` schemas -> fail (errMsg \"pg_catalog\")\n          | \"information_schema\" `elem` schemas -> fail (errMsg \"information_schema\")\n          | otherwise -> pure $ fromList schemas\n          where\n            schemas = splitOnCommas s\n            errMsg x = (\"db-schemas does not allow schema: '\" <> x <> \"'\")\n\n    parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode\n    parseSocketFileMode k =\n      optString k >>= \\case\n        Nothing -> pure 432 -- return default 660 mode if no value was provided\n        Just fileModeText ->\n          case readOct $ T.unpack fileModeText of\n            []              ->\n              fail \"Invalid server-unix-socket-mode: not an octal\"\n            (fileMode, _):_ ->\n              if fileMode < 384 || fileMode > 511\n                then fail \"Invalid server-unix-socket-mode: needs to be between 600 and 777\"\n                else pure fileMode\n\n    parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode\n    parseOpenAPIMode k =\n      optString k >>= \\case\n        Nothing                  -> pure OAFollowPriv\n        Just \"follow-privileges\" -> pure OAFollowPriv\n        Just \"ignore-privileges\" -> pure OAIgnorePriv\n        Just \"disabled\"          -> pure OADisabled\n        Just _                   -> fail \"Invalid openapi-mode. Check your configuration.\"\n\n    parseOpenAPIServerProxyURI :: C.Key -> C.Parser C.Config (Maybe Text)\n    parseOpenAPIServerProxyURI k =\n      optString k >>= \\case\n        Nothing                            -> pure Nothing\n        Just val | isMalformedProxyUri val -> fail \"Malformed proxy uri, a correct example: https://example.com:8443/basePath\"\n                 | otherwise               -> pure $ Just val\n\n    parseLogLevel :: C.Key -> C.Parser C.Config LogLevel\n    parseLogLevel k =\n      optString k >>= \\case\n        Nothing      -> pure LogError\n        Just \"crit\"  -> pure LogCrit\n        Just \"error\" -> pure LogError\n        Just \"warn\"  -> pure LogWarn\n        Just \"info\"  -> pure LogInfo\n        Just \"debug\" -> pure LogDebug\n        Just _       -> fail \"Invalid logging level. Check your configuration.\"\n\n    parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool\n    parseTxEnd k f =\n      optString k >>= \\case\n        --                                          RollbackAll AllowOverride\n        Nothing                        -> pure $ f (False,      False)\n        Just \"commit\"                  -> pure $ f (False,      False)\n        Just \"commit-allow-override\"   -> pure $ f (False,      True)\n        Just \"rollback\"                -> pure $ f (True,       False)\n        Just \"rollback-allow-override\" -> pure $ f (True,       True)\n        Just _                         -> fail \"Invalid transaction termination. Check your configuration.\"\n\n    parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath\n    parseRoleClaimKey k al =\n      optWithAlias (optString k) (optString al) >>= \\case\n        Nothing  -> pure [JSPKey \"role\"]\n        Just rck -> either (fail . show) pure $ pRoleClaimKey rck\n\n    parseCORSAllowedOrigins k =\n      optString k >>= \\case\n        Nothing   -> pure Nothing\n        Just orig -> pure $ Just (T.strip <$> T.splitOn \",\" orig)\n\n    optWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a)\n    optWithAlias orig alias =\n      orig >>= \\case\n        Just v  -> pure $ Just v\n        Nothing -> alias\n\n    optString :: C.Key -> C.Parser C.Config (Maybe Text)\n    optString k = mfilter (/= \"\") <$> overrideFromDbOrEnvironment C.optional k coerceText\n\n    optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)\n    optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText\n\n    optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)\n    optStringOrURI k = do\n      stringOrURI <- mfilter (/= \"\") <$> overrideFromDbOrEnvironment C.optional k coerceText\n      -- If the string contains ':' then it should\n      -- be a valid URI according to RFC 3986\n      case stringOrURI of\n        Just s  -> if T.isInfixOf \":\" s then validateURI s else return (Just s)\n        Nothing -> return Nothing\n      where\n        validateURI :: Text -> C.Parser C.Config (Maybe Text)\n        validateURI s = if isURI (T.unpack s)\n                          then return $ Just s\n                          else fail \"jwt-aud should be a string or a valid URI\"\n\n    optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)\n    optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt\n\n    optBool :: C.Key -> C.Parser C.Config (Maybe Bool)\n    optBool k = join <$> overrideFromDbOrEnvironment C.optional k coerceBool\n\n    overrideFromDbOrEnvironment :: JustIfMaybe a b =>\n                               (C.Key -> C.Parser C.Value a -> C.Parser C.Config b) ->\n                               C.Key -> (C.Value -> a) -> C.Parser C.Config b\n    overrideFromDbOrEnvironment necessity key coercion =\n      case dbConf <|> M.lookup envVarName env of\n        Just dbOrEnvVal -> pure $ justIfMaybe $ coercion $ C.String dbOrEnvVal\n        Nothing         -> necessity key (coercion <$> C.value)\n      where\n        dashToUnderscore '-' = '_'\n        dashToUnderscore c   = c\n        envVarName = \"PGRST_\" <> (toUpper . dashToUnderscore <$> toS key)\n        dbConf = lookup (T.pack $ dashToUnderscore <$> toS key) dbSettings\n\n    coerceText :: C.Value -> Text\n    coerceText (C.String s) = s\n    coerceText v            = show v\n\n    coerceInt :: (Read i, Integral i) => C.Value -> Maybe i\n    coerceInt (C.Number x) = rightToMaybe $ floatingOrInteger x\n    coerceInt (C.String x) = readMaybe x\n    coerceInt _            = Nothing\n\n    coerceBool :: C.Value -> Maybe Bool\n    coerceBool (C.Bool b)   = Just b\n    coerceBool (C.String s) =\n      -- parse all kinds of text: True, true, TRUE, \"true\", ...\n      case readMaybe $ T.toTitle $ T.filter isAlpha $ toS s of\n        Just b  -> Just b\n        -- numeric instead?\n        Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)\n    coerceBool _            = Nothing\n\n    splitOnCommas :: Text -> [Text]\n    splitOnCommas s = T.strip <$> T.splitOn \",\" s\n\n    splitOnCommasEmptyable :: Text -> [Text]\n    splitOnCommasEmptyable \"\" = []\n    splitOnCommasEmptyable s  = T.strip <$> T.splitOn \",\" s\n\n    defaultHoistedAllowList = [\"statement_timeout\",\"plan_filter.statement_cost_limit\",\"default_transaction_isolation\"]\n\n    defaultServerHost :: Maybe Text -> Text\n    defaultServerHost = fromMaybe \"!4\"\n\n-- | Read the JWT secret from a file if configJwtSecret is actually a\n-- filepath(has @ as its prefix). To check if the JWT secret is provided is\n-- in fact a file path, it must be decoded as 'Text' to be processed.\nreadSecretFile :: AppConfig -> IO AppConfig\nreadSecretFile conf =\n  maybe (return conf) readSecret maybeFilename\n  where\n    maybeFilename = T.stripPrefix \"@\" . decodeUtf8 =<< configJwtSecret conf\n    readSecret filename = do\n      jwtSecret <- chomp <$> BS.readFile (toS filename)\n      return $ conf { configJwtSecret = Just jwtSecret }\n    chomp bs = fromMaybe bs (BS.stripSuffix \"\\n\" bs)\n\ndecodeSecret :: AppConfig -> IO AppConfig\ndecodeSecret conf@AppConfig{..} =\n  case (configJwtSecretIsBase64, configJwtSecret) of\n    (True, Just secret) ->\n      either fail (return . updateSecret) $ decodeB64 secret\n    _ -> return conf\n  where\n    updateSecret bs = conf { configJwtSecret = Just bs }\n    decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8\n    replaceUrlChars = T.replace \"_\" \"/\" . T.replace \"-\" \"+\" . T.replace \".\" \"=\"\n\n-- | Parse `jwt-secret` configuration option and turn into a JWKS.\n--\n-- There are three ways to specify `jwt-secret`: text secret, JSON Web Key\n-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JwkSet\n-- with one key and the last is converted as is.\ndecodeJWKS :: AppConfig -> IO AppConfig\ndecodeJWKS conf = do\n  jwks <- case configJwtSecret conf of\n    Just s  -> either fail (pure . Just) $ parseSecret s\n    Nothing -> pure Nothing\n  return $ conf { configJWKS = jwks }\n\nparseSecret :: ByteString -> Either [Char] JwkSet\nparseSecret bytes =\n  case maybeJWKSet of\n    Just jwk -> Right jwk\n    Nothing  -> maybe validateSecret (\\jwk' -> Right $ JWT.JwkSet [jwk']) maybeJWK\n  where\n    maybeJWKSet = JSON.decodeStrict bytes :: Maybe JwkSet\n    maybeJWK = JSON.decodeStrict bytes :: Maybe Jwk\n    secret = JWT.JwkSet [JWT.SymmetricJwk bytes Nothing (Just JWT.Sig) (Just $ JWT.Signed JWT.HS256)]\n    validateSecret\n      | BS.length bytes < 32 = Left \"The JWT secret must be at least 32 characters long.\"\n      | otherwise = Right secret\n\n-- | Read database uri from a separate file if `db-uri` is a filepath.\nreadDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig\nreadDbUriFile maybeDbUri conf =\n  case maybeDbUri of\n    Just prevDbUri ->\n      pure $ conf { configDbUri = prevDbUri }\n    Nothing ->\n      case T.stripPrefix \"@\" $ configDbUri conf of\n        Nothing -> return conf\n        Just filename -> do\n          dbUri <- T.strip <$> readFile (toS filename)\n          return $ conf { configDbUri = dbUri }\n\ntype Environment = M.Map [Char] Text\n\n-- | Read environment variables that start with PGRST_\nreadPGRSTEnvironment :: IO Environment\nreadPGRSTEnvironment =\n  M.map T.pack . M.fromList . filter (isPrefixOf \"PGRST_\" . fst) <$> getEnvironment\n\ndata PGConnString = PGURI | PGKeyVal\n\n-- Uses same logic as libpq recognized_connection_string\n-- https://github.com/postgres/postgres/blob/5eafacd2797dc0b04a0bde25fbf26bf79903e7c2/src/interfaces/libpq/fe-connect.c#L5923-L5936\npgConnString :: Text -> Maybe PGConnString\npgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.isPrefixOf` conn = Just PGURI\n                  | \"=\" `T.isInfixOf` conn                                                      = Just PGKeyVal\n                  | otherwise                                                                   = Nothing\n  where\n    uriDesignator = \"postgresql://\"\n    shortUriDesignator = \"postgres://\"\n\n-- | Adds a `fallback_application_name` value to the connection string. This allows querying the PostgREST version on pg_stat_activity.\n--\n-- >>> let ver = \"11.1.0 (5a04ec7)\"::ByteString\n-- >>> let strangeVer = \"11'1&0@#$%,.:\\\"[]{}?+^()=asdfqwer\"::ByteString\n--\n-- >>> addFallbackAppName ver \"postgres://user:pass@host:5432/postgres\"\n-- \"postgres://user:pass@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- >>> addFallbackAppName ver \"postgres://user:pass@host:5432/postgres?\"\n-- \"postgres://user:pass@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- >>> addFallbackAppName ver \"postgres:///postgres?host=server&port=5432\"\n-- \"postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- >>> addFallbackAppName ver \"postgresql://\"\n-- \"postgresql://?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- >>> addFallbackAppName strangeVer \"postgres:///postgres?host=server&port=5432\"\n-- \"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\"\n--\n-- >>> addFallbackAppName ver \"postgres://user:invalid_chars[]#@host:5432/postgres\"\n-- \"postgres://user:invalid_chars[]#@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- >>> addFallbackAppName ver \"host=localhost port=5432 dbname=postgres\"\n-- \"host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11.1.0 (5a04ec7)'\"\n--\n-- >>> addFallbackAppName strangeVer \"host=localhost port=5432 dbname=postgres\"\n-- \"host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11\\\\'1&0@#$%,.:\\\"[]{}?+^()=asdfqwer'\"\n--\n-- works with passwords containing `?`\n-- >>> addFallbackAppName ver \"postgres://admin2:?pass?special?@localhost:5432/postgres\"\n-- \"postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- addFallbackAppName ver \"postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass\"\n-- \"postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\n--\n-- addFallbackAppName ver \"postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass\"\n-- \"postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29\"\naddFallbackAppName :: ByteString -> Text -> Text\naddFallbackAppName version dbUri = addConnStringOption dbUri \"fallback_application_name\" pgrstVer\n  where\n    pgrstVer = \"PostgREST \" <> T.decodeUtf8 version\n\n-- | Adds `target_session_attrs=read-write` to the connection string. This allows using PostgREST listener when multiple hosts are specified in the connection string.\n--\n-- >>> addTargetSessionAttrs \"postgres:///postgres?host=/dir/0kN/socket_replica_24378,/dir/0kN/socket\"\n-- \"postgres:///postgres?host=/dir/0kN/socket_replica_24378,/dir/0kN/socket&target_session_attrs=read-write\"\n--\n-- >>> addTargetSessionAttrs \"postgresql://host1:123,host2:456/somedb\"\n-- \"postgresql://host1:123,host2:456/somedb?target_session_attrs=read-write\"\n--\n-- >>> addTargetSessionAttrs \"postgresql://host1:123,host2:456/somedb?fallback_application_name=foo\"\n-- \"postgresql://host1:123,host2:456/somedb?fallback_application_name=foo&target_session_attrs=read-write\"\n--\n-- adds target_session_attrs despite one existing\n-- >>> addTargetSessionAttrs \"postgresql://host1:123,host2:456/somedb?target_session_attrs=read-only\"\n-- \"postgresql://host1:123,host2:456/somedb?target_session_attrs=read-only&target_session_attrs=read-write\"\n--\n-- >>> addTargetSessionAttrs \"host=localhost port=5432 dbname=postgres\"\n-- \"host=localhost port=5432 dbname=postgres target_session_attrs='read-write'\"\naddTargetSessionAttrs :: Text -> Text\naddTargetSessionAttrs dbUri = addConnStringOption dbUri \"target_session_attrs\" \"read-write\"\n\naddConnStringOption :: Text -> Text -> Text -> Text\naddConnStringOption dbUri key val = dbUri <>\n  case pgConnString dbUri of\n    Nothing  -> mempty\n    Just PGKeyVal -> \" \" <> keyValFmt\n    Just PGURI    -> case lookAtOptions dbUri of\n      (_, \"\")  -> \"?\" <> uriFmt\n      (_, \"?\") -> uriFmt\n      (_, _)   -> \"&\" <> uriFmt\n  where\n    uriFmt = key <> \"=\" <> toS (escapeURIString isUnescapedInURIComponent $ toS val)\n    keyValFmt = key <> \"=\" <> \"'\" <> T.replace \"'\" \"\\\\'\" val <> \"'\"\n    lookAtOptions x =  T.breakOn \"?\" . snd $ T.breakOnEnd \"@\" x -- start from after `@` to not mess passwords that include `?`, see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS\n\n-- | Example config file displayed on postgrest \"--example\" flag\nexampleConfigFile :: [Char]\nexampleConfigFile = S.unlines\n  [ \"## Admin server used for checks. It's disabled by default unless a port is specified.\"\n  , \"# admin-server-port = 3001\"\n  , \"\"\n  , \"# PostgREST error json verbosity config\"\n  , \"# client-error-verbosity = \\\"verbose\\\"\"\n  , \"\"\n  , \"## The database role to use when no client authentication is provided\"\n  , \"# db-anon-role = \\\"anon\\\"\"\n  , \"\"\n  , \"## Notification channel for reloading the schema cache\"\n  , \"db-channel = \\\"pgrst\\\"\"\n  , \"\"\n  , \"## Enable or disable the notification channel\"\n  , \"db-channel-enabled = true\"\n  , \"\"\n  , \"## Enable in-database configuration\"\n  , \"db-config = true\"\n  , \"\"\n  , \"## Function for in-database configuration\"\n  , \"## db-pre-config = \\\"postgrest.pre_config\\\"\"\n  , \"\"\n  , \"## Extra schemas to add to the search_path of every request\"\n  , \"db-extra-search-path = \\\"public\\\"\"\n  , \"\"\n  , \"## Limit rows in response\"\n  , \"# db-max-rows = 1000\"\n  , \"\"\n  , \"## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header\"\n  , \"# db-plan-enabled = false\"\n  , \"\"\n  , \"## Number of open connections in the pool\"\n  , \"db-pool = 10\"\n  , \"\"\n  , \"## Time in seconds to wait to acquire a slot from the connection pool\"\n  , \"# db-pool-acquisition-timeout = 10\"\n  , \"\"\n  , \"## Time in seconds after which to recycle pool connections\"\n  , \"# db-pool-max-lifetime = 1800\"\n  ,  \"\"\n  ,  \"## Time in seconds after which to recycle unused pool connections\"\n  ,  \"# db-pool-max-idletime = 30\"\n  ,  \"\"\n  , \"## Allow automatic database connection retrying\"\n  , \"# db-pool-automatic-recovery = true\"\n  , \"\"\n  , \"## Stored proc to exec immediately after auth\"\n  , \"# db-pre-request = \\\"stored_proc_name\\\"\"\n  , \"\"\n  , \"## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler.\"\n  , \"## When disabled, statements will be parametrized but won't be prepared.\"\n  , \"db-prepared-statements = true\"\n  , \"\"\n  , \"## The name of which database schema to expose to REST clients\"\n  , \"db-schemas = \\\"public\\\"\"\n  , \"\"\n  , \"## How to terminate database transactions\"\n  , \"## Possible values are:\"\n  , \"## commit (default)\"\n  , \"##   Transaction is always committed, this can not be overridden\"\n  , \"## commit-allow-override\"\n  , \"##   Transaction is committed, but can be overridden with Prefer tx=rollback header\"\n  , \"## rollback\"\n  , \"##   Transaction is always rolled back, this can not be overridden\"\n  , \"## rollback-allow-override\"\n  , \"##   Transaction is rolled back, but can be overridden with Prefer tx=commit header\"\n  , \"db-tx-end = \\\"commit\\\"\"\n  , \"\"\n  , \"## The standard connection URI format, documented at\"\n  , \"## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING\"\n  , \"db-uri = \\\"postgresql://\\\"\"\n  , \"\"\n  , \"# jwt-aud = \\\"your_audience_claim\\\"\"\n  , \"\"\n  , \"## Jspath to the role claim key\"\n  ,  \"jwt-role-claim-key = \\\".role\\\"\"\n  ,  \"\"\n  ,  \"## Choose a secret, JSON Web Key (or set) to enable JWT auth\"\n  ,  \"## (use \\\"@filename\\\" to load from separate file)\"\n  ,  \"# jwt-secret = \\\"secret_with_at_least_32_characters\\\"\"\n  ,  \"jwt-secret-is-base64 = false\"\n  , \"\"\n  , \"## Enables JWT Cache and sets its max size, disables caching with 0\"\n  , \"# jwt-cache-max-entries = 0\"\n  , \"\"\n  , \"## Logging level, the admitted values are: crit, error, warn, info and debug.\"\n  , \"log-level = \\\"error\\\"\"\n  , \"\"\n  , \"## Log the SQL query at the current log-level.\"\n  , \"log-query = false\"\n  , \"\"\n  , \"## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.\"\n  , \"## Admitted values: follow-privileges, ignore-privileges, disabled\"\n  , \"openapi-mode = \\\"follow-privileges\\\"\"\n  , \"\"\n  , \"## Base url for the OpenAPI output\"\n  , \"openapi-server-proxy-uri = \\\"\\\"\"\n  , \"\"\n  , \"## Configurable CORS origins\"\n  , \"# server-cors-allowed-origins = \\\"\\\"\"\n  , \"\"\n  , \"server-host = \\\"!4\\\"\"\n  , \"server-port = 3000\"\n  , \"\"\n  , \"## Allow getting the request-response timing information through the `Server-Timing` header\"\n  , \"server-timing-enabled = false\"\n  , \"\"\n  , \"## Unix socket location\"\n  , \"## if specified it takes precedence over server-port\"\n  , \"# server-unix-socket = \\\"/tmp/pgrst.sock\\\"\"\n  , \"\"\n  , \"## Unix socket file mode\"\n  , \"## When none is provided, 660 is applied by default\"\n  , \"# server-unix-socket-mode = \\\"660\\\"\"\n  ]\n"
  },
  {
    "path": "src/PostgREST/Cors.hs",
    "content": "{-|\nModule      : PostgREST.Cors\nDescription : Wai Middleware to set cors policy.\n-}\n\n{-# LANGUAGE TupleSections #-}\n\nmodule PostgREST.Cors (middleware) where\n\nimport qualified Data.ByteString.Char8       as BS\nimport qualified Data.CaseInsensitive        as CI\nimport qualified Data.Text.Encoding          as T\nimport qualified Network.Wai                 as Wai\nimport qualified Network.Wai.Middleware.Cors as Wai\n\nimport Data.List (lookup)\n\nimport PostgREST.AppState (AppState, getConfig)\nimport PostgREST.Config   (AppConfig (..))\n\nimport Protolude\n\nmiddleware :: AppState -> Wai.Middleware\nmiddleware appState app req res = do\n  conf <- getConfig appState\n  Wai.cors (corsPolicy $ configServerCorsAllowedOrigins conf) app req res\n\n-- | CORS policy to be used in by Wai Cors middleware\ncorsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy\ncorsPolicy corsAllowedOrigins req = case lookup \"origin\" headers of\n  Just _ ->\n    Just Wai.CorsResourcePolicy\n    { Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins\n    , Wai.corsMethods = [\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\", \"OPTIONS\"]\n    , Wai.corsRequestHeaders = \"Authorization\" : accHeaders\n    , Wai.corsExposedHeaders = Just\n      [ \"Content-Encoding\", \"Content-Location\", \"Content-Range\", \"Content-Type\"\n      , \"Date\", \"Location\", \"Server\", \"Transfer-Encoding\", \"Range-Unit\"]\n    , Wai.corsMaxAge = Just $ 60*60*24\n    , Wai.corsVaryOrigin = False\n    , Wai.corsRequireOrigin = False\n    , Wai.corsIgnoreFailures = True\n    }\n  Nothing -> Nothing\n  where\n    headers = Wai.requestHeaders req\n    accHeaders = case lookup \"access-control-request-headers\" headers of\n      Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs\n       -- Impossible case, Middleware.Cors will not evaluate this when\n       -- the Access-Control-Request-Headers header is not set.\n      Nothing   -> []\n"
  },
  {
    "path": "src/PostgREST/Error/Types.hs",
    "content": "{-|\nModule      : PostgREST.Error.Types\nDescription : PostgREST Error Data Types\n-}\nmodule PostgREST.Error.Types\n  ( ApiRequestError(..)\n  , QPError(..)\n  , RangeError(..)\n  , RaiseError(..)\n  , SchemaCacheError(..)\n  , PgError(..)\n  , Error(..)\n  , JwtError (..)\n  , JwtDecodeError(..)\n  , JwtClaimsError(..)\n  , PgRaiseErrMessage(..)\n  , PgRaiseErrDetails(..)\n  ) where\n\nimport qualified Hasql.Pool as SQL\n\nimport PostgREST.MediaType                (MediaType (..))\nimport PostgREST.SchemaCache              (SchemaCache (..))\nimport PostgREST.SchemaCache.Identifiers  (QualifiedIdentifier (..))\nimport PostgREST.SchemaCache.Relationship (Relationship (..),\n                                           RelationshipsMap)\nimport PostgREST.SchemaCache.Routine      (Routine (..))\nimport Protolude\n\ndata Error\n  = ApiRequestErr ApiRequestError\n  | SchemaCacheErr SchemaCacheError\n  | JwtErr JwtError\n  | NoSchemaCacheError\n  | PgErr PgError\n  deriving Show\n\n-- API REQUEST ERRORS: PGRST1XX\ndata ApiRequestError\n  = AggregatesNotAllowed\n  | MediaTypeError [ByteString]\n  | InvalidBody ByteString\n  | InvalidFilters\n  | InvalidPreferences [ByteString]\n  | InvalidRange RangeError\n  | InvalidRpcMethod ByteString\n  | NotEmbedded Text\n  | NotImplemented Text\n  | PutLimitNotAllowedError\n  | QueryParamError QPError\n  | RelatedOrderNotToOne Text Text\n  | UnacceptableFilter Text\n  | UnacceptableSchema Text [Text]\n  | UnsupportedMethod ByteString\n  | GucHeadersError\n  | GucStatusError\n  | PutMatchingPkError\n  | SingularityError Integer\n  | PGRSTParseError RaiseError\n  | MaxAffectedViolationError Integer\n  | InvalidResourcePath\n  | OpenAPIDisabled\n  | MaxAffectedRpcViolation\n  deriving Show\n\ndata QPError = QPError Text Text\n  deriving Show\n\ndata RaiseError\n  = MsgParseError ByteString\n  | DetParseError ByteString\n  | NoDetail\n  deriving Show\n\ndata RangeError\n  = NegativeLimit\n  | LowerGTUpper\n  | OutOfBounds Text Text\n  deriving Show\n\n-- SCHEMA CACHE ERRORS: PGRST2XX\ndata SchemaCacheError\n  = AmbiguousRelBetween Text Text [Relationship]\n  | AmbiguousRpc [Routine]\n  | NoRelBetween Text Text (Maybe Text) Text RelationshipsMap\n  | NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]\n  | ColumnNotFound Text Text\n  | TableNotFound Text Text SchemaCache\n  deriving Show\n\n-- JWT ERRORS: PGRST3XX\ndata JwtError\n  = JwtDecodeErr JwtDecodeError\n  | JwtSecretMissing\n  | JwtTokenRequired\n  | JwtClaimsErr JwtClaimsError\n  deriving Show\n\ndata JwtDecodeError\n  = EmptyAuthHeader\n  | UnexpectedParts Int\n  | KeyError Text\n  | BadAlgorithm Text\n  | BadCrypto\n  | UnsupportedTokenType\n  | UnreachableDecodeError\n  deriving Show\n\ndata JwtClaimsError\n  = JWTExpired\n  | JWTNotYetValid\n  | JWTIssuedAtFuture\n  | JWTNotInAudience\n  | ParsingClaimsFailed\n  | ExpClaimNotNumber\n  | NbfClaimNotNumber\n  | IatClaimNotNumber\n  | AudClaimNotStringOrArray\n  deriving Show\n\n-- PG ERRORS\ntype Authenticated = Bool\ndata PgError = PgError Authenticated SQL.UsageError\n  deriving Show\n\n-- For parsing byteString to JSON Object, used for allowing full response control\ndata PgRaiseErrMessage = PgRaiseErrMessage {\n  getCode    :: Text,\n  getMessage :: Text,\n  getDetails :: Maybe Text,\n  getHint    :: Maybe Text\n}\n\ndata PgRaiseErrDetails = PgRaiseErrDetails {\n  getStatus     :: Int,\n  getStatusText :: Maybe Text,\n  getHeaders    :: Map Text Text\n}\n"
  },
  {
    "path": "src/PostgREST/Error.hs",
    "content": "{-|\nModule      : PostgREST.Error\nDescription : PostgREST error HTTP responses\n-}\n{-# OPTIONS_GHC -fno-warn-orphans #-}\n{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\n\nmodule PostgREST.Error\n  ( errorResponseFor\n  , ApiRequestError(..)\n  , QPError(..)\n  , RangeError(..)\n  , SchemaCacheError(..)\n  , PgError(..)\n  , Error(..)\n  , JwtError (..)\n  , JwtDecodeError(..)\n  , JwtClaimsError(..)\n  , errorPayload\n  , status\n  ) where\n\nimport qualified Data.Aeson                as JSON\nimport qualified Data.ByteString.Char8     as BS\nimport qualified Data.ByteString.Lazy      as LBS\nimport qualified Data.CaseInsensitive      as CI\nimport qualified Data.FuzzySet             as Fuzzy\nimport qualified Data.HashMap.Strict       as HM\nimport qualified Data.Map.Internal         as M\nimport qualified Data.Text                 as T\nimport qualified Data.Text.Encoding        as T\nimport qualified Hasql.Pool                as SQL\nimport qualified Hasql.Session             as SQL\nimport qualified Network.HTTP.Types.Status as HTTP\n\nimport Data.Aeson  ((.:), (.:?), (.=))\nimport Network.Wai (Response, responseLBS)\n\nimport Network.HTTP.Types.Header (Header)\n\nimport           PostgREST.MediaType (MediaType (..))\nimport qualified PostgREST.MediaType as MediaType\n\nimport PostgREST.Config                   (Verbosity (..))\nimport PostgREST.SchemaCache              (SchemaCache (SchemaCache, dbTablesFuzzyIndex))\nimport PostgREST.SchemaCache.Identifiers  (QualifiedIdentifier (..),\n                                           Schema)\nimport PostgREST.SchemaCache.Relationship (Cardinality (..),\n                                           Junction (..),\n                                           Relationship (..),\n                                           RelationshipsMap)\nimport PostgREST.SchemaCache.Routine      (Routine (..),\n                                           RoutineParam (..))\n\nimport PostgREST.Error.Types\n\nimport Protolude\n\n-- | Encode Error to ByteString\nerrorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString\nerrorPayload verb = JSON.encode . toJsonPgrstError verb\n  where\n    toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value\n    toJsonPgrstError Verbose err = JSON.object [\n        \"code\"    .= code err\n      , \"message\" .= message err\n      , \"details\" .= details err\n      , \"hint\"    .= hint err\n      ]\n    toJsonPgrstError Minimal err = JSON.object [\n        \"code\"    .= code err\n      , \"message\" .= message err\n      ]\n\n-- | Create HTTP response from Error\nerrorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response\nerrorResponseFor verb err =\n  let\n    baseHeader = MediaType.toContentType MTApplicationJSON\n    cLHeader body = (,) \"Content-Length\" (show $ LBS.length body) :: Header\n    pSHeader code' = (\"Proxy-Status\", \"PostgREST; error=\" <> T.encodeUtf8 code')\n  in\n  responseLBS (status err) (baseHeader : cLHeader (errorPayload verb err) : pSHeader (code err) : headers err) $ errorPayload verb err\n\nclass ErrorHeaders a where\n  status  :: a -> HTTP.Status\n  headers :: a -> [Header]\n\nclass ErrorBody a where\n  code    :: a -> Text\n  message :: a -> Text\n  details :: a -> Maybe JSON.Value\n  hint    :: a -> Maybe JSON.Value\n\ninstance ErrorHeaders ApiRequestError where\n  status AggregatesNotAllowed{}      = HTTP.status400\n  status MediaTypeError{}            = HTTP.status406\n  status InvalidBody{}               = HTTP.status400\n  status InvalidFilters              = HTTP.status405\n  status InvalidPreferences{}        = HTTP.status400\n  status InvalidRpcMethod{}          = HTTP.status405\n  status InvalidRange{}              = HTTP.status416\n\n  status NotEmbedded{}               = HTTP.status400\n  status NotImplemented{}            = HTTP.status400\n  status PutLimitNotAllowedError     = HTTP.status400\n  status QueryParamError{}           = HTTP.status400\n  status RelatedOrderNotToOne{}      = HTTP.status400\n  status UnacceptableFilter{}        = HTTP.status400\n  status UnacceptableSchema{}        = HTTP.status406\n  status UnsupportedMethod{}         = HTTP.status405\n  status GucHeadersError             = HTTP.status500\n  status GucStatusError              = HTTP.status500\n  status PutMatchingPkError          = HTTP.status400\n  status SingularityError{}          = HTTP.status406\n  status PGRSTParseError{}           = HTTP.status500\n  status MaxAffectedViolationError{} = HTTP.status400\n  status InvalidResourcePath         = HTTP.status404\n  status OpenAPIDisabled             = HTTP.status404\n  status MaxAffectedRpcViolation     = HTTP.status400\n\n  headers _ = mempty\n\n-- Error codes:\n--\n-- Error codes are grouped by common modules or characteristics\n-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence\n-- Keep the \"PGRST\" prefix in every code for an easier search/grep\n-- They are grouped as following:\n--\n-- PGRST0xx -> Connection Error\n-- PGRST1xx -> ApiRequest Error\n-- PGRST2xx -> SchemaCache Error\n-- PGRST3xx -> JWT authentication Error\n-- PGRSTXxx -> Internal Hasql Error\n\ninstance ErrorBody ApiRequestError where\n  -- CODE: Text\n  code QueryParamError{}           = \"PGRST100\"\n  code InvalidRpcMethod{}          = \"PGRST101\"\n  code InvalidBody{}               = \"PGRST102\"\n  code InvalidRange{}              = \"PGRST103\"\n  -- code ParseRequestError           = \"PGRST104\" -- no longer used\n  code InvalidFilters              = \"PGRST105\"\n  code UnacceptableSchema{}        = \"PGRST106\"\n  code MediaTypeError{}            = \"PGRST107\"\n  code NotEmbedded{}               = \"PGRST108\"\n  -- code LimitNoOrderError           = \"PGRST109\" -- no longer used\n  -- code OffLimitsChangesError       = \"PGRST110\" -- no longer used\n  code GucHeadersError             = \"PGRST111\"\n  code GucStatusError              = \"PGRST112\"\n  -- code BinaryFieldError            = \"PGRST113\" -- no longer used\n  code PutLimitNotAllowedError     = \"PGRST114\"\n  code PutMatchingPkError          = \"PGRST115\"\n  code SingularityError{}          = \"PGRST116\"\n  code UnsupportedMethod{}         = \"PGRST117\"\n  code RelatedOrderNotToOne{}      = \"PGRST118\"\n  -- code SpreadNotToOne              = \"PGRST109\" -- no longer used\n  code UnacceptableFilter{}        = \"PGRST120\"\n  code PGRSTParseError{}           = \"PGRST121\"\n  code InvalidPreferences{}        = \"PGRST122\"\n  code AggregatesNotAllowed        = \"PGRST123\"\n  code MaxAffectedViolationError{} = \"PGRST124\"\n  code InvalidResourcePath         = \"PGRST125\"\n  code OpenAPIDisabled             = \"PGRST126\"\n  code NotImplemented{}            = \"PGRST127\"\n  code MaxAffectedRpcViolation     = \"PGRST128\"\n\n  -- MESSAGE: Text\n  message (QueryParamError (QPError msg _)) = msg\n  message (InvalidRpcMethod method)    = \"Cannot use the \" <> T.decodeUtf8 method <> \" method on RPC\"\n  message (InvalidBody errorMessage)   = T.decodeUtf8 errorMessage\n  message (InvalidRange _)             = \"Requested range not satisfiable\"\n  message InvalidFilters               = \"Filters must include all and only primary key columns with 'eq' operators\"\n  message (UnacceptableSchema sch _)   = \"Invalid schema: \" <> sch\n  message (MediaTypeError cts)         = \"None of these media types are available: \" <> T.intercalate \", \" (map T.decodeUtf8 cts)\n  message (NotEmbedded resource)       = \"'\" <> resource <> \"' is not an embedded resource in this request\"\n  message GucHeadersError              = \"response.headers guc must be a JSON array composed of objects with a single key and a string value\"\n  message GucStatusError               = \"response.status guc must be a valid status code\"\n  message PutLimitNotAllowedError      = \"limit/offset querystring parameters are not allowed for PUT\"\n  message PutMatchingPkError           = \"Payload values do not match URL in primary key column(s)\"\n  message (SingularityError _)         = \"Cannot coerce the result to a single JSON object\"\n  message (UnsupportedMethod method)   = \"Unsupported HTTP method: \" <> T.decodeUtf8 method\n  message (RelatedOrderNotToOne _ target) = \"A related order on '\" <> target <> \"' is not possible\"\n  message (UnacceptableFilter target)    = \"Bad operator on the '\" <> target <> \"' embedded resource\"\n  message (PGRSTParseError _)            = \"Could not parse JSON in the \\\"RAISE SQLSTATE 'PGRST'\\\" error\"\n  message (InvalidPreferences _)         = \"Invalid preferences given with handling=strict\"\n  message AggregatesNotAllowed           = \"Use of aggregate functions is not allowed\"\n  message (MaxAffectedViolationError _)  = \"Query result exceeds max-affected preference constraint\"\n  message InvalidResourcePath            = \"Invalid path specified in request URL\"\n  message OpenAPIDisabled                = \"Root endpoint metadata is disabled\"\n  message (NotImplemented _)             = \"Feature not implemented\"\n  message MaxAffectedRpcViolation        = \"Function must return SETOF or TABLE when max-affected preference is used with handling=strict\"\n\n  -- DETAILS: Maybe JSON.Value\n  details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets\n  details (InvalidRange rangeError) = Just $\n    case rangeError of\n       NegativeLimit           -> \"Limit should be greater than or equal to zero.\"\n       LowerGTUpper            -> \"The lower boundary must be lower than or equal to the upper boundary in the Range header.\"\n       OutOfBounds lower total -> JSON.String $ \"An offset of \" <> lower <> \" was requested, but there are only \" <> total <> \" rows.\"\n  details (SingularityError n) = Just $ JSON.String $ T.unwords [\"The result contains\", show n, \"rows\"]\n  details (RelatedOrderNotToOne origin target) = Just $ JSON.String $ \"'\" <> origin <> \"' and '\" <> target <> \"' do not form a many-to-one or one-to-one relationship\"\n  details (UnacceptableFilter _)      = Just \"Only is null or not is null filters are allowed on embedded resources\"\n  details (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorDetails raiseErr\n  details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 (\"Invalid preferences: \" <> BS.intercalate \", \" prefs)\n  details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords [\"The query affects\", show n, \"rows\"]\n  details (NotImplemented details') = Just $ JSON.String details'\n\n  details _ = Nothing\n\n  -- HINT: Maybe JSON.Value\n  hint (NotEmbedded resource) = Just $ JSON.String $ \"Verify that '\" <> resource <> \"' is included in the 'select' query parameter.\"\n  hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr\n  hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ \"Only the following schemas are exposed: \"  <> T.intercalate \", \" schemas\n\n  hint _ = Nothing\n\ninstance ErrorHeaders SchemaCacheError where\n  status AmbiguousRelBetween{} = HTTP.status300\n  status AmbiguousRpc{}        = HTTP.status300\n  status NoRelBetween{}        = HTTP.status400\n  status NoRpc{}               = HTTP.status404\n  status ColumnNotFound{}      = HTTP.status400\n  status TableNotFound{}       = HTTP.status404\n\n  headers _ = mempty\n\ninstance ErrorBody SchemaCacheError where\n  code NoRelBetween{}        = \"PGRST200\"\n  code AmbiguousRelBetween{} = \"PGRST201\"\n  code NoRpc{}               = \"PGRST202\"\n  code AmbiguousRpc{}        = \"PGRST203\"\n  code ColumnNotFound{}      = \"PGRST204\"\n  code TableNotFound{}       = \"PGRST205\"\n\n  message (NoRelBetween parent child _ _ _)  = \"Could not find a relationship between '\" <> parent <> \"' and '\" <> child <> \"' in the schema cache\"\n  message (AmbiguousRelBetween parent child _) = \"Could not embed because more than one relationship was found for '\" <> parent <> \"' and '\" <> child <> \"'\"\n  message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = \"Could not find the function \" <> func <> (if onlySingleParams then \"\" else fmtPrms prmsMsg) <> \" in the schema cache\"\n      where\n        onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]\n        func = schema <> \".\" <> procName\n        prms = T.intercalate \", \" argumentKeys\n        prmsMsg = \"(\" <> prms <> \")\"\n        fmtPrms p = if null argumentKeys then \" without parameters\" else p\n  message (AmbiguousRpc procs) = \"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]\n  message (ColumnNotFound rel col) = \"Could not find the '\" <> col <> \"' column of '\" <> rel <> \"' in the schema cache\"\n  message (TableNotFound schemaName relName _) = \"Could not find the table '\" <> schemaName <> \".\" <> relName <> \"' in the schema cache\"\n\n  details (NoRelBetween parent child embedHint schema _) = Just $ JSON.String $ \"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.\"\n  details (AmbiguousRelBetween _ _ rels)       = Just $ JSON.toJSONList (compressedRel <$> rels)\n  details (NoRpc schema procName argumentKeys contentType isInvPost _ _) =\n      Just $ JSON.String $ \"Searched for the function \" <> func <>\n        (case (isInvPost, contentType) of\n           (True, MTTextPlain)       -> \" with a single unnamed text parameter\"\n           (True, MTTextXML)         -> \" with a single unnamed xml parameter\"\n           (True, MTOctetStream)     -> \" with a single unnamed bytea parameter\"\n           (True, MTApplicationJSON) -> fmtPrms prmsDet <> \" or with a single unnamed json/jsonb parameter\"\n           _                         -> fmtPrms prmsDet\n        ) <> \", but no matches were found in the schema cache.\"\n      where\n        func = schema <> \".\" <> procName\n        prms = T.intercalate \", \" argumentKeys\n        prmsDet = \" with parameter\" <> (if length argumentKeys > 1 then \"s \" else \" \") <> prms\n        fmtPrms p = if null argumentKeys then \" without parameters\" else p\n\n  details _ = Nothing\n\n  hint (NoRelBetween parent child _ schema allRels) = JSON.String <$> noRelBetweenHint parent child schema allRels\n  hint (AmbiguousRelBetween _ child rels)   = Just $ JSON.String $ \"Try changing '\" <> child <> \"' to one of the following: \" <> relHint rels <> \". Find the desired relationship in the 'details' key.\"\n  -- The hint will be null in the case of single unnamed parameter functions\n  hint (NoRpc schema procName argumentKeys contentType isInvPost allProcs overloadedProcs) =\n    if onlySingleParams\n      then Nothing\n      else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs\n      where\n        onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]\n  hint (AmbiguousRpc _)      = Just \"Try renaming the parameters or the function itself in the database so function overloading can be resolved\"\n  hint (TableNotFound schemaName relName schemaCache) = JSON.String <$> tableNotFoundHint schemaName relName schemaCache\n\n  hint _ = Nothing\n\n-- |\n-- If no relationship is found then:\n--\n-- Looks for parent suggestions if parent not found\n-- Looks for child suggestions if parent is found but child is not\n-- Gives no suggestions if both are found (it means that there is a problem with the embed hint)\n--\n-- >>> :set -Wno-missing-fields\n-- >>> let qi t = QualifiedIdentifier \"api\" t\n-- >>> let rel ft = Relationship{relForeignTable = qi ft}\n-- >>> let rels = HM.fromList [((qi \"films\", \"api\"), [rel \"directors\", rel \"roles\", rel \"actors\"])]\n--\n-- >>> noRelBetweenHint \"film\" \"directors\" \"api\" rels\n-- Just \"Perhaps you meant 'films' instead of 'film'.\"\n--\n-- >>> noRelBetweenHint \"films\" \"role\" \"api\" rels\n-- Just \"Perhaps you meant 'roles' instead of 'role'.\"\n--\n-- >>> noRelBetweenHint \"films\" \"role\" \"api\" rels\n-- Just \"Perhaps you meant 'roles' instead of 'role'.\"\n--\n-- >>> noRelBetweenHint \"films\" \"actors\" \"api\" rels\n-- Nothing\n--\n-- >>> noRelBetweenHint \"noclosealternative\" \"roles\" \"api\" rels\n-- Nothing\n--\n-- >>> noRelBetweenHint \"films\" \"noclosealternative\" \"api\" rels\n-- Nothing\n--\n-- >>> noRelBetweenHint \"films\" \"noclosealternative\" \"noclosealternative\" rels\n-- Nothing\n--\nnoRelBetweenHint :: Text -> Text -> Schema -> RelationshipsMap -> Maybe Text\nnoRelBetweenHint parent child schema allRels = (\"Perhaps you meant '\" <>) <$>\n  if isJust findParent\n    then (<> \"' instead of '\" <> child <> \"'.\") <$> suggestChild\n    else (<> \"' instead of '\" <> parent <> \"'.\") <$> suggestParent\n  where\n    findParent = HM.lookup (QualifiedIdentifier schema parent, schema) allRels\n    fuzzySetOfParents  = Fuzzy.fromList [qiName (fst p) | p <- HM.keys allRels, snd p == schema]\n    fuzzySetOfChildren = Fuzzy.fromList [qiName (relForeignTable c) | c <- fromMaybe [] findParent]\n    suggestParent = Fuzzy.getOne fuzzySetOfParents parent\n    -- Do not give suggestion if the child is found in the relations (weight = 1.0)\n    suggestChild  = headMay [snd k | k <- Fuzzy.get fuzzySetOfChildren child, fst k < 1.0]\n\n-- |\n-- If no function is found with the given name, it does a fuzzy search to all the functions\n-- in the same schema and shows the best match as hint.\n--\n-- >>> :set -Wno-missing-fields\n-- >>> let procs = [(QualifiedIdentifier \"api\" \"test\"), (QualifiedIdentifier \"api\" \"another\"), (QualifiedIdentifier \"private\" \"other\")]\n--\n-- >>> noRpcHint \"api\" \"testt\" [\"val\", \"param\", \"name\"] procs []\n-- Just \"Perhaps you meant to call the function api.test\"\n--\n-- >>> noRpcHint \"api\" \"other\" [] procs []\n-- Nothing\n--\n-- >>> noRpcHint \"api\" \"noclosealternative\" [] procs []\n-- Nothing\n--\n-- If a function is found with the given name, but no params match, then it does a fuzzy search\n-- to all the overloaded functions' params using the form \"param1, param2, param3, ...\"\n-- and shows the best match as hint.\n--\n-- >>> let procsDesc = [Function {pdParams = [RoutineParam {ppName=\"val\"}, RoutineParam {ppName=\"param\"}, RoutineParam {ppName=\"name\"}]}, Function {pdParams = [RoutineParam {ppName=\"id\"}, RoutineParam {ppName=\"attr\"}]}]\n--\n-- >>> noRpcHint \"api\" \"test\" [\"vall\", \"pqaram\", \"nam\"] procs procsDesc\n-- Just \"Perhaps you meant to call the function api.test(name, param, val)\"\n--\n-- >>> noRpcHint \"api\" \"test\" [\"val\", \"param\"] procs procsDesc\n-- Just \"Perhaps you meant to call the function api.test(name, param, val)\"\n--\n-- >>> noRpcHint \"api\" \"test\" [\"id\", \"attrs\"] procs procsDesc\n-- Just \"Perhaps you meant to call the function api.test(attr, id)\"\n--\n-- >>> noRpcHint \"api\" \"test\" [\"id\"] procs procsDesc\n-- Just \"Perhaps you meant to call the function api.test(attr, id)\"\n--\n-- >>> noRpcHint \"api\" \"test\" [\"noclosealternative\"] procs procsDesc\n-- Nothing\n--\nnoRpcHint :: Text -> Text -> [Text] -> [QualifiedIdentifier] -> [Routine] -> Maybe Text\nnoRpcHint schema procName params allProcs overloadedProcs =\n  fmap ((\"Perhaps you meant to call the function \" <> schema <> \".\") <>) possibleProcs\n  where\n    fuzzySetOfProcs  = Fuzzy.fromList [qiName k | k <- allProcs, qiSchema k == schema]\n    fuzzySetOfParams = Fuzzy.fromList $ listToText <$> [[ppName prm | prm <- pdParams ov] | ov <- overloadedProcs]\n    -- Cannot do a fuzzy search like: Fuzzy.getOne [[Text]] [Text], where [[Text]] is the list of params for each\n    -- overloaded function and [Text] the given params. This converts those lists to text to make fuzzy search possible.\n    -- E.g. [\"val\", \"param\", \"name\"] into \"(name, param, val)\"\n    listToText       = (\"(\" <>) . (<> \")\") . T.intercalate \", \" . sort\n    possibleProcs\n      | null overloadedProcs = getFuzzyHint HintProcedure fuzzySetOfProcs procName\n      | otherwise            = (procName <>) <$> getFuzzyHint HintParams fuzzySetOfParams (listToText params)\n\n-- |\n-- Do a fuzzy search in all tables in the same schema and return closest result\ntableNotFoundHint :: Text -> Text -> SchemaCache -> Maybe Text\ntableNotFoundHint schema tblName SchemaCache{dbTablesFuzzyIndex}\n  = fmap (\\tbl -> \"Perhaps you meant the table '\" <> schema <> \".\" <> tbl <> \"'\") perhapsTable\n    where\n      perhapsTable = (\\fuzzySet -> getFuzzyHint HintTable fuzzySet tblName) =<< HM.lookup schema dbTablesFuzzyIndex\n\ndata HintType\n  = HintTable\n  | HintProcedure\n  | HintParams\n\n-- | Get hint using Fuzzy Search with at least 0.75 similarity score\ngetFuzzyHint :: HintType -> Fuzzy.FuzzySet -> Text -> Maybe Text\ngetFuzzyHint hintType =\n  let minScore = 0.75 :: Double -- used for table and procedure name hints\n  in case hintType of\n    HintTable     -> Fuzzy.getOneWithMinScore minScore\n    HintProcedure -> Fuzzy.getOneWithMinScore minScore\n    HintParams    -> Fuzzy.getOne -- For params, we stick to `getOne` which defaults to 0.33 min score, not a security risk to reveal params\n\ncompressedRel :: Relationship -> JSON.Value\n-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed\ncompressedRel ComputedRelationship{} = JSON.object mempty\ncompressedRel Relationship{..} =\n  let\n    fmtEls els = \"(\" <> T.intercalate \", \" els <> \")\"\n  in\n  JSON.object $\n    (\"embedding\" .= (qiName relTable <> \" with \" <> qiName relForeignTable :: Text))\n    : case relCardinality of\n        M2M Junction{..} -> [\n            \"cardinality\" .= (\"many-to-many\" :: Text)\n          , \"relationship\" .= (qiName junTable <> \" using \" <> junConstraint1 <> fmtEls (snd <$> junColsSource) <> \" and \" <> junConstraint2 <> fmtEls (snd <$> junColsTarget))\n          ]\n        M2O cons relColumns -> [\n            \"cardinality\" .= (\"many-to-one\" :: Text)\n          , \"relationship\" .= (cons <> \" using \" <> qiName relTable <> fmtEls (fst <$> relColumns) <> \" and \" <> qiName relForeignTable <> fmtEls (snd <$> relColumns))\n          ]\n        O2O cons relColumns _ -> [\n            \"cardinality\" .= (\"one-to-one\" :: Text)\n          , \"relationship\" .= (cons <> \" using \" <> qiName relTable <> fmtEls (fst <$> relColumns) <> \" and \" <> qiName relForeignTable <> fmtEls (snd <$> relColumns))\n          ]\n        O2M cons relColumns -> [\n            \"cardinality\" .= (\"one-to-many\" :: Text)\n          , \"relationship\" .= (cons <> \" using \" <> qiName relTable <> fmtEls (fst <$> relColumns) <> \" and \" <> qiName relForeignTable <> fmtEls (snd <$> relColumns))\n          ]\n\nrelHint :: [Relationship] -> Text\nrelHint rels = T.intercalate \", \" (hintList <$> rels)\n  where\n    hintList Relationship{..} =\n      let buildHint rel = \"'\" <> qiName relForeignTable <> \"!\" <> rel <> \"'\" in\n      case relCardinality of\n        M2M Junction{..} -> buildHint (qiName junTable)\n        M2O cons _       -> buildHint cons\n        O2O cons _ _     -> buildHint cons\n        O2M cons _       -> buildHint cons\n    -- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed\n    hintList ComputedRelationship{} = mempty\n\npgrstParseErrorDetails :: RaiseError -> Text\npgrstParseErrorDetails err = case err of\n  MsgParseError m -> \"Invalid JSON value for MESSAGE: '\" <> T.decodeUtf8 m <> \"'\"\n  DetParseError d -> \"Invalid JSON value for DETAIL: '\" <> T.decodeUtf8 d <> \"'\"\n  NoDetail        -> \"DETAIL is missing in the RAISE statement\"\n\npgrstParseErrorHint :: RaiseError -> Text\npgrstParseErrorHint err = case err of\n  MsgParseError _ -> \"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'.\"\n  _               -> \"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'.\"\n\ninstance ErrorHeaders PgError where\n  status (PgError authed usageError) = pgErrorStatus authed usageError\n\n  headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError \"PGRST\" m d _ _p))))) =\n    case parseRaisePGRST m d of\n      Right (_, r) -> map intoHeader (M.toList $ getHeaders r)\n      Left e       -> headers e\n    where\n      intoHeader (k,v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v)\n\n  headers err =\n    if status err == HTTP.status401\n       then [(\"WWW-Authenticate\", \"Bearer\") :: Header]\n       else mempty\n\ninstance ErrorBody PgError where\n  code    (PgError _ usageError) = code usageError\n  message (PgError _ usageError) = message usageError\n  details (PgError _ usageError) = details usageError\n  hint    (PgError _ usageError) = hint usageError\n\ninstance ErrorBody SQL.UsageError where\n  code    (SQL.ConnectionUsageError _)                   = \"PGRST000\"\n  code    (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e\n  code    SQL.AcquisitionTimeoutUsageError               = \"PGRST003\"\n\n  message (SQL.ConnectionUsageError _) = \"Database connection error. Retrying the connection.\"\n  message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e\n  message SQL.AcquisitionTimeoutUsageError = \"Timed out acquiring connection from connection pool.\"\n\n  details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e\n  details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e\n  details SQL.AcquisitionTimeoutUsageError               = Nothing\n\n  hint    (SQL.ConnectionUsageError _)                   = Nothing\n  hint    (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e\n  hint    SQL.AcquisitionTimeoutUsageError               = Nothing\n\ninstance ErrorBody SQL.CommandError where\n  -- Special error raised with code PGRST, to allow full response control\n  code (SQL.ResultError (SQL.ServerError \"PGRST\" m d _ _)) =\n    case parseRaisePGRST m d of\n      Right (r, _) -> getCode r\n      Left e       -> code e\n  code (SQL.ResultError (SQL.ServerError c _ _ _ _)) = T.decodeUtf8 c\n\n  code (SQL.ResultError _) = \"PGRSTX00\" -- Internal Error\n\n  code (SQL.ClientError _) = \"PGRST001\"\n\n  message (SQL.ResultError (SQL.ServerError \"PGRST\" m d _ _)) =\n    case parseRaisePGRST m d of\n      Right (r, _) -> getMessage r\n      Left e       -> message e\n  message (SQL.ResultError (SQL.ServerError _ m _ _ _)) = T.decodeUtf8 m\n  message (SQL.ResultError resultError) = show resultError -- We never really return this error, because we kill pgrst thread early in App.hs\n  message (SQL.ClientError _) = \"Database client error. Retrying the connection.\"\n\n  details (SQL.ResultError (SQL.ServerError \"PGRST\" m d _ _)) =\n    case parseRaisePGRST m d of\n      Right (r, _) -> JSON.String <$> getDetails r\n      Left e       -> details e\n  details (SQL.ResultError (SQL.ServerError _ _ d _ _)) = JSON.String . T.decodeUtf8 <$> d\n  details (SQL.ClientError d) = JSON.String . T.decodeUtf8 <$> d\n\n  details _ = Nothing\n\n  hint (SQL.ResultError (SQL.ServerError \"PGRST\" m d _ _p)) =\n    case parseRaisePGRST m d of\n      Right (r, _) -> JSON.String <$> getHint r\n      Left e       -> hint e\n  hint (SQL.ResultError (SQL.ServerError _ _ _ h _)) = JSON.String . T.decodeUtf8 <$> h\n\n  hint _                   = Nothing\n\n\npgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status\npgErrorStatus _      (SQL.ConnectionUsageError _) = HTTP.status503\npgErrorStatus _      SQL.AcquisitionTimeoutUsageError = HTTP.status504\npgErrorStatus _      (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _)))      = HTTP.status503\npgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =\n  case rError of\n    (SQL.ServerError c m d _ _) ->\n      case BS.unpack c of\n        '0':'8':_ -> HTTP.status503 -- pg connection err\n        '0':'9':_ -> HTTP.status500 -- triggered action exception\n        '0':'L':_ -> HTTP.status403 -- invalid grantor\n        '0':'P':_ -> HTTP.status403 -- invalid role specification\n        \"23503\"   -> HTTP.status409 -- foreign_key_violation\n        \"23505\"   -> HTTP.status409 -- unique_violation\n        \"25006\"   -> HTTP.status405 -- read_only_sql_transaction\n        \"21000\"   -> -- cardinality_violation\n          if BS.isSuffixOf \"requires a WHERE clause\" m\n            then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error\n            else HTTP.status500 -- generic function or view server error, e.g. \"more than one row returned by a subquery used as an expression\"\n        \"22023\"   -> -- invalid_parameter_value. Catch nonexistent role error, see https://github.com/PostgREST/postgrest/issues/3601\n          if BS.isPrefixOf \"role\" m && BS.isSuffixOf \"does not exist\" m\n            then HTTP.status401 -- role in jwt does not exist\n            else HTTP.status400\n        '2':'5':_ -> HTTP.status500 -- invalid tx state\n        '2':'8':_ -> HTTP.status403 -- invalid auth specification\n        '2':'D':_ -> HTTP.status500 -- invalid tx termination\n        '3':'8':_ -> HTTP.status500 -- external routine exception\n        '3':'9':_ -> HTTP.status500 -- external routine invocation\n        '3':'B':_ -> HTTP.status500 -- savepoint exception\n        '4':'0':_ -> HTTP.status500 -- tx rollback\n        \"53400\"   -> HTTP.status500 -- config limit exceeded\n        '5':'3':_ -> HTTP.status503 -- insufficient resources\n        '5':'4':_ -> HTTP.status500 -- too complex\n        '5':'5':_ -> HTTP.status500 -- obj not on prereq state\n        \"57P01\"   -> HTTP.status503 -- terminating connection due to administrator command\n        '5':'7':_ -> HTTP.status500 -- operator intervention\n        '5':'8':_ -> HTTP.status500 -- system error\n        'F':'0':_ -> HTTP.status500 -- conf file error\n        'H':'V':_ -> HTTP.status500 -- foreign data wrapper error\n        \"P0001\"   -> HTTP.status400 -- default code for \"raise\"\n        'P':'0':_ -> HTTP.status500 -- PL/pgSQL Error\n        'X':'X':_ -> HTTP.status500 -- internal Error\n        \"42883\"-> if BS.isPrefixOf \"function xmlagg(\" m\n          then HTTP.status406\n          else HTTP.status404 -- undefined function\n        \"42P01\"   -> HTTP.status404 -- undefined table\n        \"42P17\"   -> HTTP.status500 -- infinite recursion\n        \"42501\"   -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege\n        'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)\n        \"PGRST\"   ->\n          case parseRaisePGRST m d of\n            Right (_, r) -> maybe (toEnum $ getStatus r) (HTTP.mkStatus (getStatus r) . T.encodeUtf8) (getStatusText r)\n            Left e       -> status e\n        _         -> HTTP.status400\n\n    _                       -> HTTP.status500\n\n\ninstance ErrorHeaders Error where\n  status (ApiRequestErr err)  = status err\n  status (SchemaCacheErr err) = status err\n  status (JwtErr err)         = status err\n  status NoSchemaCacheError   = HTTP.status503\n  status (PgErr err)          = status err\n\n  headers (ApiRequestErr err)  = headers err\n  headers (SchemaCacheErr err) = headers err\n  headers (JwtErr err)         = headers err\n  headers (PgErr err)          = headers err\n  headers NoSchemaCacheError   = mempty\n\ninstance ErrorBody Error where\n  code (ApiRequestErr err)  = code err\n  code (SchemaCacheErr err) = code err\n  code (JwtErr err)         = code err\n  code NoSchemaCacheError   = \"PGRST002\"\n  code (PgErr err)          = code err\n\n  message (ApiRequestErr err) = message err\n  message (SchemaCacheErr err)  = message err\n  message (JwtErr err)          = message err\n  message NoSchemaCacheError    = \"Could not query the database for the schema cache. Retrying.\"\n  message (PgErr err)           = message err\n\n  details (ApiRequestErr err)  = details err\n  details (SchemaCacheErr err) = details err\n  details (JwtErr err)         = details err\n  details NoSchemaCacheError   = Nothing\n  details (PgErr err)          = details err\n\n  hint (ApiRequestErr err)  = hint err\n  hint (SchemaCacheErr err) = hint err\n  hint (JwtErr err)         = hint err\n  hint NoSchemaCacheError   = Nothing\n  hint (PgErr err)          = hint err\n\ninstance ErrorHeaders JwtError where\n  status JwtDecodeErr{}   = HTTP.unauthorized401\n  status JwtSecretMissing = HTTP.status500\n  status JwtTokenRequired = HTTP.unauthorized401\n  status JwtClaimsErr{}   = HTTP.unauthorized401\n\n  headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]\n  headers JwtTokenRequired   = [requiredTokenHeader]\n  headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]\n  headers _                  = mempty\n\ninstance ErrorBody JwtError where\n  code JwtSecretMissing = \"PGRST300\"\n  code (JwtDecodeErr _) = \"PGRST301\"\n  code JwtTokenRequired = \"PGRST302\"\n  code (JwtClaimsErr _) = \"PGRST303\"\n\n  message JwtSecretMissing = \"Server lacks JWT secret\"\n  message (JwtDecodeErr e) = case e of\n    EmptyAuthHeader        -> \"Empty JWT is sent in Authorization header\"\n    UnexpectedParts n      -> \"Expected 3 parts in JWT; got \" <> show n\n    KeyError _             -> \"No suitable key or wrong key type\"\n    BadAlgorithm _         -> \"Wrong or unsupported encoding algorithm\"\n    BadCrypto              -> \"JWT cryptographic operation failed\"\n    UnsupportedTokenType   -> \"Unsupported token type\"\n    UnreachableDecodeError -> \"JWT couldn't be decoded\"\n  message JwtTokenRequired = \"Anonymous access is disabled\"\n  message (JwtClaimsErr e) = case e of\n    JWTExpired               -> \"JWT expired\"\n    JWTNotYetValid           -> \"JWT not yet valid\"\n    JWTIssuedAtFuture        -> \"JWT issued at future\"\n    JWTNotInAudience         -> \"JWT not in audience\"\n    ParsingClaimsFailed      -> \"Parsing claims failed\"\n    ExpClaimNotNumber        -> \"The JWT 'exp' claim must be a number\"\n    NbfClaimNotNumber        -> \"The JWT 'nbf' claim must be a number\"\n    IatClaimNotNumber        -> \"The JWT 'iat' claim must be a number\"\n    AudClaimNotStringOrArray -> \"The JWT 'aud' claim must be a string or an array of strings\"\n\n  details (JwtDecodeErr jde) = case jde of\n    KeyError dets     -> Just $ JSON.String dets\n    BadAlgorithm dets -> Just $ JSON.String dets\n    _                 -> Nothing\n  details _ = Nothing\n\n  hint _    = Nothing\n\ninvalidTokenHeader :: Text -> Header\ninvalidTokenHeader m =\n  (\"WWW-Authenticate\", \"Bearer error=\\\"invalid_token\\\", \" <> \"error_description=\" <> encodeUtf8 (show m))\n\nrequiredTokenHeader :: Header\nrequiredTokenHeader = (\"WWW-Authenticate\", \"Bearer\")\n\n-- For parsing byteString to JSON Object, used for allowing full response control\n\ninstance JSON.FromJSON PgRaiseErrMessage where\n  parseJSON (JSON.Object m) =\n    PgRaiseErrMessage\n      <$> m .: \"code\"\n      <*> m .: \"message\"\n      <*> m .:? \"details\"\n      <*> m .:? \"hint\"\n\n  parseJSON _ = mzero\n\ninstance JSON.FromJSON PgRaiseErrDetails where\n  parseJSON (JSON.Object d) =\n    PgRaiseErrDetails\n      <$> d .: \"status\"\n      <*> d .:? \"status_text\"\n      <*> d .: \"headers\"\n\n  parseJSON _ = mzero\n\nparseRaisePGRST :: ByteString -> Maybe ByteString -> Either ApiRequestError (PgRaiseErrMessage, PgRaiseErrDetails)\nparseRaisePGRST m d = do\n  msgJson <- maybeToRight (PGRSTParseError $ MsgParseError m) (JSON.decodeStrict m)\n  det <- maybeToRight (PGRSTParseError NoDetail) d\n  detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)\n  return (msgJson, detJson)\n"
  },
  {
    "path": "src/PostgREST/Listener.hs",
    "content": "{-# LANGUAGE LambdaCase      #-}\n{-# LANGUAGE MultiWayIf      #-}\n{-# LANGUAGE RecordWildCards #-}\n\nmodule PostgREST.Listener (runListener) where\n\nimport qualified Data.ByteString.Char8 as BS\n\nimport qualified Hasql.Connection      as SQL\nimport qualified Hasql.Notifications   as SQL\nimport           PostgREST.AppState    (AppState, getConfig)\nimport           PostgREST.Config      (AppConfig (..))\nimport           PostgREST.Observation (Observation (..))\nimport           PostgREST.Version     (prettyVersion)\n\nimport qualified PostgREST.AppState as AppState\nimport qualified PostgREST.Config   as Config\n\nimport           Control.Arrow              ((&&&))\nimport           Data.Bitraversable         (bisequence)\nimport           Data.Either.Combinators    (whenRight)\nimport qualified Data.Text                  as T\nimport qualified Database.PostgreSQL.LibPQ  as LibPQ\nimport qualified Hasql.Session              as SQL\nimport           PostgREST.Config.Database  (queryPgVersion)\nimport           PostgREST.Config.PgVersion (pgvFullName)\nimport           Protolude\n\n-- | Starts the Listener in a thread\nrunListener :: AppState -> IO ()\nrunListener appState = do\n  AppConfig{..} <- getConfig appState\n  when configDbChannelEnabled $\n    void . forkIO . void $ retryingListen appState\n\n-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.\n-- | This function never returns (but can throw) and return type enforces that.\nretryingListen :: AppState -> IO Void\nretryingListen appState = do\n  AppConfig{..} <- AppState.getConfig appState\n  let\n    dbChannel = toS configDbChannel\n    onError err = do\n      AppState.putIsListenerOn appState False\n      observer $ DBListenFail dbChannel (Right err)\n      when (isDbListenerBug err) $\n        observer DBListenBugHint\n      unless configDbPoolAutomaticRecovery $\n        killThread mainThreadId\n\n      -- retry the listener\n      delay <- AppState.getNextListenerDelay appState\n      observer $ DBListenRetry delay\n      threadDelay (delay * oneSecondInMicro)\n      unless (delay == maxDelay) $\n        AppState.putNextListenerDelay appState (delay * 2)\n      -- loop running the listener\n      retryingListen appState\n\n  -- Execute the listener with with error handling\n  handle onError $ do\n    -- Make sure we don't leak connections on errors\n    bracket\n      -- acquire connection\n      (SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))\n      -- release connection\n      (`whenRight` releaseConnection) $\n      -- use connection\n      \\case\n        Right db -> do\n          SQL.listen db $ SQL.toPgIdentifier dbChannel\n          (pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)\n          pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)\n\n          AppState.putIsListenerOn appState True\n\n          delay <- AppState.getNextListenerDelay appState\n          when (delay > 1) $ do -- if we did a retry\n            -- assume we lost notifications, refresh the schema cache\n            AppState.schemaCacheLoader appState\n            -- reset the delay\n            AppState.putNextListenerDelay appState 1\n\n          observer $ DBListenStart pqHost pqPort pgFullName dbChannel\n\n          -- wait for notifications\n          -- this will never return, in case of an error it will throw and be caught by onError\n          forever $ SQL.waitForNotifications handleNotification db\n\n        Left err -> do\n          observer $ DBListenFail dbChannel (Left err)\n          exitFailure\n  where\n    observer = AppState.getObserver appState\n    mainThreadId = AppState.getMainThreadId appState\n    oneSecondInMicro = 1000000\n    maxDelay = 32\n\n    handleNotification channel msg =\n      if | BS.null msg            -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader\n         | msg == \"reload schema\" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader\n         | msg == \"reload config\" -> observer (DBListenerGotConfigMsg channel) >> AppState.readInDbConfig False appState\n         | otherwise              -> pure () -- Do nothing if anything else than an empty message is sent\n\n    cacheReloader =\n      AppState.schemaCacheLoader appState\n\n    releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release\n\n    isDbListenerBug e = \"could not access status of transaction\" `T.isInfixOf` show e\n"
  },
  {
    "path": "src/PostgREST/Logger.hs",
    "content": "{-# LANGUAGE LambdaCase      #-}\n{-# LANGUAGE RecordWildCards #-}\n{-# LANGUAGE RecursiveDo     #-}\n{-|\nModule      : PostgREST.Logger\nDescription : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.\n-}\n-- TODO log with buffering enabled to not lose throughput on logging levels higher than LogError\nmodule PostgREST.Logger\n  ( middleware\n  , observationLogger\n  , init\n  , LoggerState\n  ) where\n\nimport           Control.AutoUpdate                (defaultUpdateSettings,\n                                                    mkAutoUpdate,\n                                                    updateAction)\nimport           Control.Debounce\nimport qualified Data.ByteString.Char8             as BS\nimport qualified Data.Text.Encoding                as T\nimport qualified Hasql.Decoders                    as HD\nimport qualified Hasql.DynamicStatements.Snippet   as SQL hiding (sql)\nimport qualified Hasql.DynamicStatements.Statement as SQL\nimport qualified Hasql.Statement                   as SQL\n\nimport Data.Time (ZonedTime, defaultTimeLocale, formatTime,\n                  getZonedTime)\n\nimport qualified Network.Wai                          as Wai\nimport qualified Network.Wai.Middleware.RequestLogger as Wai\n\nimport Network.HTTP.Types.Status (Status, status400, status500)\nimport System.IO.Unsafe          (unsafePerformIO)\n\nimport PostgREST.Config      (LogLevel (..), Verbosity (..))\nimport PostgREST.Observation\nimport PostgREST.Query       (MainQuery (..))\n\nimport qualified Data.ByteString.Lazy       as LBS\nimport qualified Data.Text                  as T\nimport qualified Hasql.Connection           as SQL\nimport qualified Hasql.Pool                 as SQL\nimport qualified Hasql.Pool.Observation     as SQL\nimport           Numeric                    (showFFloat)\nimport           PostgREST.Config.PgVersion (pgvName)\nimport qualified PostgREST.Error            as Error\nimport           Protolude\n\ndata LoggerState = LoggerState\n  { stateGetZTime               :: IO ZonedTime  -- ^ Time with time zone used for logs\n  , stateLogDebouncePoolTimeout :: IO ()         -- ^ Logs with a debounce\n  }\n\ninit :: IO LoggerState\ninit = mdo\n  let\n    oneSecond = 1000000\n    loggerState = LoggerState zTime debouncePoolTimeout\n  zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }\n  debouncePoolTimeout <- mkDebounce defaultDebounceSettings\n          { debounceAction = logWithZTime loggerState $ observationMessages PoolAcqTimeoutObs\n          , debounceFreq = 5*oneSecond\n          , debounceEdge = leadingEdge -- logs at the start and the end\n          }\n  pure loggerState\n\n-- TODO stop using this middleware to reuse the same \"observer\" pattern for all our logs\nmiddleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware\nmiddleware logLevel getAuthRole =\n    unsafePerformIO $\n      Wai.mkRequestLogger Wai.defaultRequestLoggerSettings\n      { Wai.outputFormat =\n         Wai.ApacheWithSettings $\n           Wai.defaultApacheSettings &\n           Wai.setApacheRequestFilter (\\_ res -> shouldLogResponse logLevel $ Wai.responseStatus res) &\n           Wai.setApacheUserGetter getAuthRole\n      , Wai.autoFlush = True\n      , Wai.destination = Wai.Handle stdout\n      }\n\nshouldLogResponse :: LogLevel -> Status -> Bool\nshouldLogResponse logLevel = case logLevel of\n  LogCrit  -> const False\n  LogError -> (>= status500)\n  LogWarn  -> (>= status400)\n  LogInfo  -> const True\n  LogDebug -> const True\n\n-- All observations are logged except some that depend on the log-level\nobservationLogger :: LoggerState -> LogLevel -> ObservationHandler\nobservationLogger loggerState logLevel obs = case obs of\n  PoolAcqTimeoutObs -> do\n    when (logLevel >= LogError) $\n      stateLogDebouncePoolTimeout loggerState\n  o@(QueryErrorCodeHighObs _) -> do\n    when (logLevel >= LogError) $ do\n      logWithZTime loggerState $ observationMessages o\n  o@SchemaCacheEmptyObs ->\n    when (logLevel >= LogError) $ do\n    logWithZTime loggerState $ observationMessages o\n  o@(HasqlPoolObs _) -> do\n    when (logLevel >= LogDebug) $ do\n      logWithZTime loggerState $ observationMessages o\n  o@(QueryObs _ status) -> do\n    when (shouldLogResponse logLevel status) $\n      logWithZTime loggerState $ observationMessages o\n  o@PoolRequest ->\n    when (logLevel >= LogDebug) $ do\n      logWithZTime loggerState $ observationMessages o\n  o@PoolRequestFullfilled ->\n    when (logLevel >= LogDebug) $ do\n      logWithZTime loggerState $ observationMessages o\n  o@JwtCacheEviction ->\n    when (logLevel >= LogDebug) $ do\n      logWithZTime loggerState $ observationMessages o\n  o@(JwtCacheLookup _) ->\n    when (logLevel >= LogDebug) $ do\n      logWithZTime loggerState $ observationMessages o\n  o ->\n    logWithZTime loggerState $ observationMessages o\n\nlogWithZTime :: LoggerState -> [Text] -> IO ()\nlogWithZTime loggerState txts = do\n  zTime <- stateGetZTime loggerState\n  traverse_ (hPutStrLn stderr . (toS (formatTime defaultTimeLocale \"%d/%b/%Y:%T %z: \" zTime) <>)) txts\n\n-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert\n-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql\nrenderSnippet :: SQL.Snippet -> ByteString\nrenderSnippet snippet =\n  let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder prepared\n      decoder = HD.noResult -- unused\n      prepared = False  -- unused\n  in\n    sql\n\nobservationMessages :: Observation -> [Text]\nobservationMessages = \\case\n  AdminStartObs address ->\n    pure $ \"Admin server listening on \" <> address\n  AppStartObs ver ->\n    pure $ \"Starting PostgREST \" <> T.decodeUtf8 ver <> \"...\"\n  AppServerAddressObs address ->\n    pure $ \"API server listening on \" <> address\n  DBConnectedObs ver ->\n    pure $ \"Successfully connected to \" <> ver\n  ExitUnsupportedPgVersion pgVer minPgVer ->\n    pure $ \"Cannot run in this PostgreSQL version (\" <> pgvName pgVer <> \"), PostgREST needs at least \" <> pgvName minPgVer\n  ExitDBNoRecoveryObs ->\n    pure \"Automatic recovery disabled, exiting.\"\n  ExitDBFatalError ServerAuthError usageErr ->\n    pure $ \"Failed to establish a connection. \" <> jsonMessage usageErr\n  ExitDBFatalError ServerPgrstBug usageErr ->\n    pure $ \"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. \" <> jsonMessage usageErr\n  ExitDBFatalError ServerError42P05 usageErr ->\n    pure $ \"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. \" <> jsonMessage usageErr\n  ExitDBFatalError ServerError08P01 usageErr ->\n    pure $ \"Connection poolers in statement mode are not supported.\" <> jsonMessage usageErr\n  SchemaCacheEmptyObs ->\n    pure $ T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError\n  SchemaCacheErrorObs dbSchemas extraPaths usageErr ->\n    pure $ \"Failed to load the schema cache using \"\n      <> \"db-schemas=\" <> T.intercalate \",\" (toList dbSchemas)\n      <> \" and \"\n      <> \"db-extra-search-path=\" <> T.intercalate \",\" extraPaths\n      <> \". \" <> jsonMessage usageErr\n  SchemaCacheQueriedObs resultTime ->\n    pure $ \"Schema cache queried in \" <> showMillis resultTime  <> \" milliseconds\"\n  SchemaCacheLoadedObs resultTime summary ->\n    [\n      \"Schema cache loaded \" <> summary\n    , \"Schema cache loaded in \" <> showMillis resultTime <> \" milliseconds\"\n    ]\n  ConnectionRetryObs delay ->\n    pure $ \"Attempting to reconnect to the database in \" <> (show delay::Text) <> \" seconds...\"\n  QueryPgVersionError usageErr ->\n    pure $ \"Failed to query the PostgreSQL version. \" <> jsonMessage usageErr\n  DBListenStart host port fullName channel -> do\n    pure $ \"Listener connected to \" <> fullName <> \" on \" <> show (fold $ host <> fmap (\":\" <>) port) <> \" and listening for database notifications on the \" <> show channel <> \" channel\"\n  DBListenFail channel listenErr ->\n    pure $ \"Failed listening for database notifications on the \" <> show channel <> \" channel. \" <>\n      either showListenerConnError showListenerException listenErr\n  DBListenRetry delay ->\n    pure $ \"Retrying listening for database notifications in \" <> (show delay::Text) <> \" seconds...\"\n  DBListenBugHint ->\n    pure \"HINT:  This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();\"\n  DBListenerGotSCacheMsg channel ->\n    pure $ \"Received a schema cache reload message on the \" <> show channel <> \" channel\"\n  DBListenerGotConfigMsg channel ->\n    pure $ \"Received a config reload message on the \" <> show channel <> \" channel\"\n  DBListenerConnectionCleanupFail ex ->\n    pure $ \"Failed during listener connection cleanup: \" <> showOnSingleLine '\\t' (show ex)\n  (QueryObs MainQuery{mqOpenAPI=(x, y, z),..} _) ->\n      let snipts  = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]\n      in\n        showOnSingleLine '\\n' . T.decodeUtf8 <$> filter (/= mempty) snipts\n  ConfigReadErrorObs usageErr ->\n    pure $ \"Failed to query database settings for the config parameters.\" <> jsonMessage usageErr\n  QueryRoleSettingsErrorObs usageErr ->\n    pure $ \"Failed to query the role settings. \" <> jsonMessage usageErr\n  QueryErrorCodeHighObs usageErr ->\n    pure $ jsonMessage usageErr\n  ConfigInvalidObs err ->\n    pure $ \"Failed reloading config: \" <> err\n  ConfigSucceededObs ->\n     pure \"Config reloaded\"\n  PoolInit poolSize ->\n     pure $ \"Connection Pool initialized with a maximum size of \" <> show poolSize <> \" connections\"\n  PoolAcqTimeoutObs -> pure $ jsonMessage SQL.AcquisitionTimeoutUsageError\n  HasqlPoolObs (SQL.ConnectionObservation uuid status) ->\n    pure $ \"Connection \" <> show uuid <> (\n      case status of\n        SQL.ConnectingConnectionStatus   -> \" is being established\"\n        SQL.ReadyForUseConnectionStatus  -> \" is available\"\n        SQL.InUseConnectionStatus        -> \" is used\"\n        SQL.TerminatedConnectionStatus reason -> \" is terminated due to \" <> case reason of\n          SQL.AgingConnectionTerminationReason          -> \"max lifetime\"\n          SQL.IdlenessConnectionTerminationReason       -> \"max idletime\"\n          SQL.ReleaseConnectionTerminationReason        -> \"release\"\n          SQL.NetworkErrorConnectionTerminationReason _ -> \"network error\" -- usage error is already logged, no need to repeat the same message.\n    )\n  PoolRequest ->\n    pure \"Trying to borrow a connection from pool\"\n  PoolRequestFullfilled ->\n    pure \"Borrowed a connection from the pool\"\n  JwtCacheLookup _ ->\n    pure \"Looked up a JWT in JWT cache\"\n  JwtCacheEviction ->\n    pure \"Evicted entry from JWT cache\"\n  WarpErrorObs txt ->\n    pure $ \"Warp server error: \" <> txt\n  where\n    showMillis :: Double -> Text\n    showMillis x = toS $ showFFloat (Just 1) x \"\"\n\n    jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err\n\n\n    showListenerConnError :: SQL.ConnectionError -> Text\n    showListenerConnError = maybe \"Connection error\" (showOnSingleLine '\\t' . T.decodeUtf8)\n\n    showListenerException :: SomeException -> Text\n    showListenerException = showOnSingleLine '\\t' . show\n\n\nshowOnSingleLine :: Char -> Text -> Text\nshowOnSingleLine split txt = T.intercalate \" \" $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with \"\\t\\n\"\n"
  },
  {
    "path": "src/PostgREST/MainTx.hs",
    "content": "{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\n{-|\nModule      : PostgREST.MainTx\nDescription : PostgREST transaction executor\n\nThis module parametrizes, prepares, executes SQL queries and decodes their results.\n-}\nmodule PostgREST.MainTx\n  ( MainTx (..)\n  , DbResult (..)\n  , ResultSet (..)\n  , mainTx\n  ) where\n\nimport           Control.Lens                      ((^?))\nimport           Control.Monad.Extra               (whenJust)\nimport qualified Data.Aeson.Lens                   as L\nimport qualified Data.ByteString                   as BS hiding\n                                                         (break)\nimport qualified Data.ByteString.Char8             as BS\nimport qualified Data.HashMap.Strict               as HM\nimport qualified Data.Set                          as S\nimport qualified Hasql.Decoders                    as HD\nimport qualified Hasql.DynamicStatements.Statement as SQL\nimport qualified Hasql.Session                     as SQL (Session)\nimport qualified Hasql.Transaction                 as SQL\nimport qualified Hasql.Transaction.Sessions        as SQL\n\nimport qualified PostgREST.Error       as Error\nimport qualified PostgREST.SchemaCache as SchemaCache\n\n\nimport PostgREST.ApiRequest              (ApiRequest (..))\nimport PostgREST.ApiRequest.Preferences  (PreferCount (..),\n                                          PreferHandling (..),\n                                          PreferMaxAffected (..),\n                                          PreferTransaction (..),\n                                          Preferences (..))\nimport PostgREST.ApiRequest.Types        (Mutation (..))\nimport PostgREST.Auth.Types              (AuthResult (..))\nimport PostgREST.Config                  (AppConfig (..),\n                                          OpenAPIMode (..))\nimport PostgREST.Error                   (Error)\nimport PostgREST.MediaType               (MediaType (..))\nimport PostgREST.Plan                    (ActionPlan (..),\n                                          CrudPlan (..),\n                                          DbActionPlan (..),\n                                          InfoPlan (..),\n                                          InspectPlan (..))\nimport PostgREST.Query                   (MainQuery (..))\nimport PostgREST.SchemaCache             (SchemaCache (..))\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))\nimport PostgREST.SchemaCache.Routine     (Routine (..), RoutineMap)\nimport PostgREST.SchemaCache.Table       (TablesMap)\n\nimport Protolude hiding (Handler)\n\ntype DbHandler = ExceptT Error SQL.Transaction\n\ndata MainTx\n  = DbTx {\n      dqIsoLevel    :: SQL.IsolationLevel\n    , dqTxMode      :: SQL.Mode\n    , dqDbHandler   :: DbHandler DbResult\n    , dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error DbResult) -> SQL.Session (Either Error DbResult)\n    }\n  | NoDbTx DbResult\n\ndata DbResult\n  = DbCrudResult  CrudPlan ResultSet\n  | DbPlanResult  MediaType BS.ByteString\n  | MaybeDbResult InspectPlan  (Maybe (TablesMap, RoutineMap, Maybe Text))\n  | NoDbResult    InfoPlan\n\n-- | Standard result set format used for the mqMain query\ndata ResultSet\n  = RSStandard\n  { rsTableTotal :: Maybe Int64\n  -- ^ count of all the table rows\n  , rsQueryTotal :: Int64\n  -- ^ count of the query rows\n  , rsLocation   :: [(BS.ByteString, BS.ByteString)]\n  -- ^ The Location header(only used for inserts) is represented as a list of strings containing\n  -- variable bindings like @\"k1=eq.42\"@, or the empty list if there is no location header.\n  , rsBody       :: BS.ByteString\n  -- ^ the aggregated body of the query\n  , rsGucHeaders :: Maybe BS.ByteString\n  -- ^ the HTTP headers to be added to the response\n  , rsGucStatus  :: Maybe Text\n  -- ^ the HTTP status to be added to the response\n  , rsInserted   :: Maybe Int64\n  -- ^ the number of rows inserted (Only used for upserts)\n  }\n\nmainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx\nmainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x\nmainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =\n  DbTx isoLvl txMode dbHandler transaction\n  where\n    transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction\n    isoLvl = planIsoLvl conf authRole plan\n    txMode = planTxMode plan\n    dbHandler = do\n      lift $ SQL.statement mempty $ SQL.dynamicallyParameterized mqTxVars\n          HD.noResult configDbPreparedStatements\n      lift $ whenJust mqPreReq $ \\q ->\n        SQL.statement mempty $ SQL.dynamicallyParameterized q\n          HD.noResult configDbPreparedStatements\n      actionResult genQ plan conf apiReq sCache\n\nplanTxMode :: DbActionPlan -> SQL.Mode\nplanTxMode (DbCrud _ x) = pTxMode x\nplanTxMode (MayUseDb x) = ipTxmode x\n\nplanIsoLvl :: AppConfig -> ByteString -> DbActionPlan -> SQL.IsolationLevel\nplanIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of\n  DbCrud _ CallReadPlan{crProc} -> fromMaybe roleIsoLvl $ pdIsoLvl crProc\n  _                           -> roleIsoLvl\n  where\n    roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl\n\nactionResult :: MainQuery -> DbActionPlan -> AppConfig -> ApiRequest -> SchemaCache -> ExceptT Error SQL.Transaction DbResult\nactionResult MainQuery{..} (DbCrud True plan) conf@AppConfig{..} apiReq _ = do\n  explRes <- lift $ SQL.statement mempty $ SQL.dynamicallyParameterized mqMain planRow configDbPreparedStatements\n  optionalRollback conf apiReq\n  pure $ DbPlanResult (pMedia plan) explRes\n\nactionResult MainQuery{..} (DbCrud _ plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do\n  resultSet@RSStandard{rsTableTotal=tableTotal} <- lift $ SQL.statement mempty $ dynStmt (HD.singleRow $ standardRow True)\n  failNotSingular pMedia resultSet\n  optionalRollback conf apiReq\n  explainTotal <- lift . fmap join $ traverse (\\snip ->\n                    SQL.statement mempty $ SQL.dynamicallyParameterized snip decodeExplain configDbPreparedStatements)\n                    mqExplain\n\n  pure $ DbCrudResult plan\n    resultSet{rsTableTotal=case preferCount of\n      Just PlannedCount   -> explainTotal\n      Just EstimatedCount -> if tableTotal > (fromIntegral <$> configDbMaxRows)\n        then max <$> tableTotal <*> explainTotal\n        else tableTotal\n      _                   -> tableTotal}\n  where\n    dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements\n\n    decodeExplain :: HD.Result (Maybe Int64)\n    decodeExplain =\n      let row = HD.singleRow $ column HD.bytea in\n      (^? L.nth 0 . L.key \"Plan\" .  L.key \"Plan Rows\" . L._Integral) <$> row\n\nactionResult MainQuery{..} (DbCrud _ plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do\n  resultSet <- lift $ SQL.statement mempty $ dynStmt decodeRow\n  failMutation resultSet\n  optionalRollback conf apiReq\n  pure $ DbCrudResult plan resultSet\n  where\n    dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements\n    failMutation resultSet = case mrMutation of\n      MutationCreate -> do\n        failNotSingular pMedia resultSet\n      MutationUpdate -> do\n        failNotSingular pMedia resultSet\n        failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet\n      MutationSingleUpsert -> do\n        failPut resultSet\n      MutationDelete -> do\n        failNotSingular pMedia resultSet\n        failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet\n    decodeRow = fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)\n\nactionResult MainQuery{..} (DbCrud _ plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do\n  resultSet <- lift $ SQL.statement mempty $ dynStmt decodeRow\n  optionalRollback conf apiReq\n  failNotSingular pMedia resultSet\n  failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet\n  pure $ DbCrudResult plan resultSet\n  where\n    dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements\n    decodeRow = fromMaybe (RSStandard (Just 0) 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow True)\n\nactionResult MainQuery{mqOpenAPI=(tblsQ, funcsQ, schQ)} (MayUseDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ sCache =\n  mainActionQuery\n  where\n    mainActionQuery = lift $\n      case configOpenApiMode of\n        OAFollowPriv -> do\n          tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ  decodeAccessibleIdentifiers configDbPreparedStatements\n          accFuncs <-  SQL.statement mempty $ SQL.dynamicallyParameterized   funcsQ   SchemaCache.decodeFuncs configDbPreparedStatements\n          schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized     schQ decodeSchemaDesc configDbPreparedStatements\n          let tbls = HM.filterWithKey (\\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache\n\n          pure $ MaybeDbResult plan (Just (tbls, accFuncs, schDesc))\n        OAIgnorePriv -> do\n          schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements)\n\n          let tbls = HM.filterWithKey (\\(QualifiedIdentifier sch _) _ ->  sch == tSchema) (SchemaCache.dbTables sCache)\n              routs = HM.filterWithKey (\\(QualifiedIdentifier sch _) _ ->  sch == tSchema) (SchemaCache.dbRoutines sCache)\n\n          pure $ MaybeDbResult plan (Just (tbls, routs, schDesc))\n        OADisabled ->\n          pure $ MaybeDbResult plan Nothing\n\n    decodeSchemaDesc :: HD.Result (Maybe Text)\n    decodeSchemaDesc = join <$> HD.rowMaybe (nullableColumn HD.text)\n\n    decodeAccessibleIdentifiers :: HD.Result (S.Set QualifiedIdentifier)\n    decodeAccessibleIdentifiers =\n      let\n        row = QualifiedIdentifier\n          <$> column HD.text\n          <*> column HD.text\n      in\n      S.fromList <$> HD.rowList row\n\n-- Makes sure the querystring pk matches the payload pk\n-- e.g. PUT /items?id=eq.1 { \"id\" : 1, .. } is accepted,\n-- PUT /items?id=eq.14 { \"id\" : 2, .. } is rejected.\n-- If this condition is not satisfied then nothing is inserted,\n-- check the WHERE for INSERT in QueryBuilder.hs to see how it's done\nfailPut :: ResultSet -> DbHandler ()\nfailPut RSStandard{rsQueryTotal=queryTotal} =\n  when (queryTotal /= 1) $ do\n    lift SQL.condemn\n    throwError $ Error.ApiRequestErr Error.PutMatchingPkError\n\n-- |\n-- Fail a response if a single JSON object was requested and not exactly one\n-- was found.\nfailNotSingular :: MediaType -> ResultSet -> DbHandler ()\nfailNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =\n  when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do\n    lift SQL.condemn\n    throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal\n\nfailExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()\nfailExceedsMaxAffectedPref (Nothing,_) _ = pure ()\nfailExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do\n  lift SQL.condemn\n  throwError $ Error.ApiRequestErr . Error.MaxAffectedViolationError $ toInteger queryTotal\n\n-- | Set a transaction to roll back if requested\noptionalRollback :: AppConfig -> ApiRequest -> DbHandler ()\noptionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do\n  lift $ when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ do\n    SQL.sql \"SET CONSTRAINTS ALL IMMEDIATE\"\n    SQL.condemn\n  where\n    shouldCommit =\n      preferTransaction == Just Commit\n    shouldRollback =\n      preferTransaction == Just Rollback\n\n-- | We use rowList because when doing EXPLAIN (FORMAT TEXT), the result comes as many rows. FORMAT JSON comes as one.\nplanRow :: HD.Result BS.ByteString\nplanRow = BS.unlines <$> HD.rowList (column HD.bytea)\n\ncolumn :: HD.Value a -> HD.Row a\ncolumn = HD.column . HD.nonNullable\n\nnullableColumn :: HD.Value a -> HD.Row (Maybe a)\nnullableColumn = HD.column . HD.nullable\n\narrayColumn :: HD.Value a -> HD.Row [a]\narrayColumn = column . HD.listArray . HD.nonNullable\n\nstandardRow :: Bool -> HD.Row ResultSet\nstandardRow noLocation =\n  RSStandard <$> nullableColumn HD.int8 <*> column HD.int8\n             <*> (if noLocation then pure mempty else fmap splitKeyValue <$> arrayColumn HD.bytea)\n             <*> (fromMaybe mempty <$> nullableColumn HD.bytea)\n             <*> nullableColumn HD.bytea\n             <*> nullableColumn HD.text\n             <*> nullableColumn HD.int8\n  where\n    splitKeyValue :: ByteString -> (ByteString, ByteString)\n    splitKeyValue kv =\n      let (k, v) = BS.break (== '=') kv in\n      (k, BS.tail v)\n"
  },
  {
    "path": "src/PostgREST/MediaType.hs",
    "content": "{-# LANGUAGE DeriveAnyClass        #-}\n{-# LANGUAGE DeriveGeneric         #-}\n{-# LANGUAGE DuplicateRecordFields #-}\n{-# OPTIONS_GHC -Wno-unused-do-bind #-}\nmodule PostgREST.MediaType\n  ( MediaType(..)\n  , MTVndPlanOption (..)\n  , MTVndPlanFormat (..)\n  , toContentType\n  , toMime\n  , decodeMediaType\n  ) where\n\nimport qualified Data.Aeson                    as JSON\nimport qualified Data.ByteString               as BS\nimport qualified Data.Text                     as T\nimport qualified Text.ParserCombinators.Parsec as P\n\nimport Data.Map                  (fromList, (!?))\nimport Data.Text.Encoding        (decodeLatin1)\nimport Network.HTTP.Types.Header (Header, hContentType)\n\nimport Protolude\n\n-- | Enumeration of currently supported media types\ndata MediaType\n  = MTApplicationJSON\n  | MTGeoJSON\n  | MTTextCSV\n  | MTTextPlain\n  | MTTextXML\n  | MTOpenAPI\n  | MTUrlEncoded\n  | MTOctetStream\n  | MTAny\n  | MTOther Text\n  -- vendored media types\n  | MTVndArrayJSONStrip\n  | MTVndSingularJSON Bool\n  -- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.\n  | MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]\n  deriving (Eq, Show, Generic, JSON.ToJSON)\ninstance Hashable MediaType\n\ndata MTVndPlanOption\n  = PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL\n  deriving (Eq, Show, Generic, JSON.ToJSON)\ninstance Hashable MTVndPlanOption\n\ndata MTVndPlanFormat\n  = PlanJSON | PlanText\n  deriving (Eq, Show, Generic, JSON.ToJSON)\ninstance Hashable MTVndPlanFormat\n\n-- | Convert MediaType to a Content-Type HTTP Header\ntoContentType :: MediaType -> Header\ntoContentType ct = (hContentType, toMime ct <> charset)\n  where\n    charset = case ct of\n      MTOctetStream -> mempty\n      MTOther _     -> mempty\n      _             -> \"; charset=utf-8\"\n\n-- | Convert from MediaType to a ByteString representing the mime type\ntoMime :: MediaType -> ByteString\ntoMime MTApplicationJSON      = \"application/json\"\ntoMime MTVndArrayJSONStrip    = \"application/vnd.pgrst.array+json;nulls=stripped\"\ntoMime MTGeoJSON              = \"application/geo+json\"\ntoMime MTTextCSV              = \"text/csv\"\ntoMime MTTextPlain            = \"text/plain\"\ntoMime MTTextXML              = \"text/xml\"\ntoMime MTOpenAPI              = \"application/openapi+json\"\ntoMime (MTVndSingularJSON True)  = \"application/vnd.pgrst.object+json;nulls=stripped\"\ntoMime (MTVndSingularJSON False) = \"application/vnd.pgrst.object+json\"\ntoMime MTUrlEncoded           = \"application/x-www-form-urlencoded\"\ntoMime MTOctetStream          = \"application/octet-stream\"\ntoMime MTAny                  = \"*/*\"\ntoMime (MTOther ct)           = encodeUtf8 ct\ntoMime (MTVndPlan mt fmt opts)   =\n  \"application/vnd.pgrst.plan+\" <> toMimePlanFormat fmt <>\n  (\"; for=\\\"\" <> toMime mt <> \"\\\"\") <>\n  (if null opts then mempty else \"; options=\" <> BS.intercalate \"|\" (toMimePlanOption <$> opts))\n\ntoMimePlanOption :: MTVndPlanOption -> ByteString\ntoMimePlanOption PlanAnalyze  = \"analyze\"\ntoMimePlanOption PlanVerbose  = \"verbose\"\ntoMimePlanOption PlanSettings = \"settings\"\ntoMimePlanOption PlanBuffers  = \"buffers\"\ntoMimePlanOption PlanWAL      = \"wal\"\n\ntoMimePlanFormat :: MTVndPlanFormat -> ByteString\ntoMimePlanFormat PlanJSON = \"json\"\ntoMimePlanFormat PlanText = \"text\"\n\n-- | Convert from ByteString to MediaType.\n--\n-- >>> decodeMediaType \"application/json\"\n-- MTApplicationJSON\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.plan;\"\n-- MTVndPlan MTApplicationJSON PlanText []\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.plan;for=\\\"application/json\\\"\"\n-- MTVndPlan MTApplicationJSON PlanText []\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.plan ; for=\\\"text/xml\\\" ; options=analyze\"\n-- MTVndPlan MTTextXML PlanText [PlanAnalyze]\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.plan+json;for=\\\"text/csv\\\"\"\n-- MTVndPlan MTTextCSV PlanJSON []\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.array+json;nulls=stripped\"\n-- MTVndArrayJSONStrip\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.array+json\"\n-- MTApplicationJSON\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.object+json;nulls=stripped\"\n-- MTVndSingularJSON True\n--\n-- >>> decodeMediaType \"application/vnd.pgrst.object+json\"\n-- MTVndSingularJSON False\n--\n-- Test uppercase is parsed correctly (per issue #3478)\n-- >>> decodeMediaType \"ApplicatIon/vnd.PgRsT.object+json\"\n-- MTVndSingularJSON False\n--\n-- >>> decodeMediaType \"application/vnd.twkb\"\n-- MTOther \"application/vnd.twkb\"\n\ndecodeMediaType :: ByteString -> MediaType\ndecodeMediaType mt = decodeMediaType' $ decodeLatin1 mt\n  where\n    decodeMediaType' :: Text -> MediaType\n    decodeMediaType' mt' =\n      case (T.toLower mainType, T.toLower subType, params) of\n        (\"application\", \"json\", _)                  -> MTApplicationJSON\n        (\"application\", \"geo+json\", _)              -> MTGeoJSON\n        (\"text\", \"csv\", _)                          -> MTTextCSV\n        (\"text\", \"plain\", _)                        -> MTTextPlain\n        (\"text\", \"xml\", _)                          -> MTTextXML\n        (\"application\", \"openapi+json\", _)          -> MTOpenAPI\n        (\"application\", \"x-www-form-urlencoded\", _) -> MTUrlEncoded\n        (\"application\", \"octet-stream\", _)          -> MTOctetStream\n        (\"application\", \"vnd.pgrst.plan\", _)        -> getPlan PlanText\n        (\"application\", \"vnd.pgrst.plan+text\", _)   -> getPlan PlanText\n        (\"application\", \"vnd.pgrst.plan+json\", _)   -> getPlan PlanJSON\n        (\"application\", \"vnd.pgrst.object+json\", _) -> MTVndSingularJSON strippedNulls\n        (\"application\", \"vnd.pgrst.object\", _)      -> MTVndSingularJSON strippedNulls\n        (\"application\", \"vnd.pgrst.array+json\", _)  -> checkArrayNullStrip\n        (\"application\", \"vnd.pgrst.array\", _)       -> checkArrayNullStrip\n        (\"*\",\"*\",_)                                 -> MTAny\n        _                                           -> MTOther mt'\n      where\n        mediaTypeOrError = P.parse tokenizeMediaType \"parsec: tokenizeMediaType failed\" $ T.unpack mt'\n        (mainType, subType, params') = case mediaTypeOrError of\n          Right mt'' -> mt''\n          Left _     -> (mt',\"\",[])\n        params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321\n        getPlan fmt = MTVndPlan mtFor fmt $\n          [PlanAnalyze  | inOpts \"analyze\" ] ++\n          [PlanVerbose  | inOpts \"verbose\" ] ++\n          [PlanSettings | inOpts \"settings\"] ++\n          [PlanBuffers  | inOpts \"buffers\" ] ++\n          [PlanWAL      | inOpts \"wal\"     ]\n          where\n            mtFor = decodeMediaType' $ fromMaybe \"application/json\" (params !? \"for\")\n            inOpts str = str `elem` opts\n            opts = T.splitOn \"|\" $ fromMaybe mempty (params !? \"options\")\n        strippedNulls = fromMaybe \"false\" (params !? \"nulls\") == \"stripped\"\n        checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON\n\n-- | Split a Media Type string into components\n-- >>> P.parse tokenizeMediaType \"\" \"application/vnd.pgrst.plan+json;for=\\\"text/csv\\\"\"\n-- Right (\"application\",\"vnd.pgrst.plan+json\",[(\"for\",\"text/csv\")])\n--\n-- >>> P.parse tokenizeMediaType \"\" \"*/*\"\n-- Right (\"*\",\"*\",[])\n--\n-- >>> P.parse tokenizeMediaType \"\" \"application/vnd.pgrst.plan;wat=\\\"application/json;text/csv\\\"\"\n-- Right (\"application\",\"vnd.pgrst.plan\",[(\"wat\",\"application/json;text/csv\")])\n--\n-- >>> P.parse tokenizeMediaType \"\" \"application/vnd.pgrst.plan+text; for=\\\"text/xml\\\"; options=analyze|verbose|settings|buffers|wal\"\n-- Right (\"application\",\"vnd.pgrst.plan+text\",[(\"for\",\"text/xml\"),(\"options\",\"analyze|verbose|settings|buffers|wal\")])\n\n-- TODO: Improve mediatype parser as per RFC 2045 https://datatracker.ietf.org/doc/html/rfc2045#section-5.1\ntokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])\ntokenizeMediaType = do\n  mainType <- P.many1 (P.alphaNum <|> P.oneOf \".*\")\n  P.char '/'\n  subType <- P.many1 (P.alphaNum <|> P.oneOf \".*+-\")\n  params <- P.many pSemicolonSeparatedKeyVals\n  P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon, discard input after that because it has already failed or we have hit EOF\n  return (T.pack mainType, T.pack subType, params)\n    where\n      pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)\n      pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal\n        where\n          pKeyVal :: P.Parser (Text, Text)\n          pKeyVal = do\n            key <- P.many1 (P.alphaNum <|> P.oneOf \"-\")\n            P.spaces\n            P.char '='\n            P.spaces\n            val <- P.try pQuoted <|> P.try pUnQuoted\n            return (T.pack key, T.pack val)\n              where\n                pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf \"|-\")\n                pQuoted = P.char '\\\"' *> P.manyTill P.anyChar (P.char '\\\"')\n"
  },
  {
    "path": "src/PostgREST/Metrics.hs",
    "content": "{-# LANGUAGE RecordWildCards #-}\n{-|\nModule      : PostgREST.Logger\nDescription : Metrics based on the Observation module. See Observation.hs.\n-}\nmodule PostgREST.Metrics\n  ( init\n  , MetricsState (..)\n  , observationMetrics\n  , metricsToText\n  ) where\n\nimport qualified Data.ByteString.Lazy   as LBS\nimport qualified Hasql.Pool.Observation as SQL\n\nimport Prometheus\n\nimport PostgREST.Observation\n\nimport Protolude\n\ndata MetricsState =\n  MetricsState {\n    poolTimeouts         :: Counter,\n    poolAvailable        :: Gauge,\n    poolWaiting          :: Gauge,\n    poolMaxSize          :: Gauge,\n    schemaCacheLoads     :: Vector Label1 Counter,\n    schemaCacheQueryTime :: Gauge,\n    jwtCacheRequests     :: Counter,\n    jwtCacheHits         :: Counter,\n    jwtCacheEvictions    :: Counter\n  }\n\ninit :: Int -> IO MetricsState\ninit configDbPoolSize = do\n  metricState <- MetricsState <$>\n    register (counter (Info \"pgrst_db_pool_timeouts_total\" \"The total number of pool connection timeouts\")) <*>\n    register (gauge (Info \"pgrst_db_pool_available\" \"Available connections in the pool\")) <*>\n    register (gauge (Info \"pgrst_db_pool_waiting\" \"Requests waiting to acquire a pool connection\")) <*>\n    register (gauge (Info \"pgrst_db_pool_max\" \"Max pool connections\")) <*>\n    register (vector \"status\" $ counter (Info \"pgrst_schema_cache_loads_total\" \"The total number of times the schema cache was loaded\")) <*>\n    register (gauge (Info \"pgrst_schema_cache_query_time_seconds\" \"The query time in seconds of the last schema cache load\")) <*>\n    register (counter (Info \"pgrst_jwt_cache_requests_total\" \"The total number of JWT cache lookups\")) <*>\n    register (counter (Info \"pgrst_jwt_cache_hits_total\" \"The total number of JWT cache hits\")) <*>\n    register (counter (Info \"pgrst_jwt_cache_evictions_total\" \"The total number of JWT cache evictions\"))\n  setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)\n  pure metricState\n\n-- Only some observations are used as metrics\nobservationMetrics :: MetricsState -> ObservationHandler\nobservationMetrics MetricsState{..} obs = case obs of\n  PoolAcqTimeoutObs -> do\n    incCounter poolTimeouts\n  (HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of\n     SQL.ReadyForUseConnectionStatus  -> do\n      incGauge poolAvailable\n     SQL.InUseConnectionStatus        -> do\n      decGauge poolAvailable\n     SQL.TerminatedConnectionStatus  _ -> do\n      decGauge poolAvailable\n     SQL.ConnectingConnectionStatus -> pure ()\n  PoolRequest ->\n    incGauge poolWaiting\n  PoolRequestFullfilled ->\n    decGauge poolWaiting\n  SchemaCacheLoadedObs resTime _ -> do\n    withLabel schemaCacheLoads \"SUCCESS\" incCounter\n    setGauge schemaCacheQueryTime resTime\n  SchemaCacheErrorObs{} -> do\n    withLabel schemaCacheLoads \"FAIL\" incCounter\n  JwtCacheLookup True -> incCounter jwtCacheRequests *> incCounter jwtCacheHits\n  JwtCacheLookup False -> incCounter jwtCacheRequests\n  JwtCacheEviction -> incCounter jwtCacheEvictions\n  _ ->\n    pure ()\n\nmetricsToText :: IO LBS.ByteString\nmetricsToText = exportMetricsAsText\n"
  },
  {
    "path": "src/PostgREST/Network.hs",
    "content": "module PostgREST.Network\n  ( resolveSocketToAddress\n  , escapeHostName\n  , isSpecialHostName\n  ) where\n\nimport           Data.String    (IsString (..))\nimport qualified Network.Socket as NS\n\nimport Protolude\n\n-- | Resolves the socket to an address depending on the socket type. The Show\n--   instance of the socket types automatically resolves it to the correct\n--   address. Example resolution:\n-- -----------------------------------------------------\n-- | IPv4         | IPv6             | Unix            |\n-- -----------------------------------------------------\n-- | 127.0.0.1:80 | [2001:db8::1]:80 | /tmp/pgrst.sock |\n-- -----------------------------------------------------\nresolveSocketToAddress :: NS.Socket -> IO Text\nresolveSocketToAddress sock = do\n  sn <- NS.getSocketName sock\n  return $ showSocketAddr sn\n\n-- |\n-- >>> let addr_ipv4 = NS.SockAddrInet 80 (NS.tupleToHostAddress (127,0,0,1))\n-- >>> let addr_ipv6 = NS.SockAddrInet6 80 0 (0,0,0,1) 0\n-- >>> let addr_unix = NS.SockAddrUnix \"/tmp/pgrst.sock\"\n--\n-- >>> showSocketAddr addr_ipv4\n-- \"127.0.0.1:80\"\n\n-- >>> showSocketAddr addr_ipv6\n-- \"[::1]:80\"\n--\n-- >>> showSocketAddr addr_unix\n-- \"/tmp/pgrst.sock\"\nshowSocketAddr :: NS.SockAddr -> Text\nshowSocketAddr = fromString . show\n\n-- | When printing special addresses like !4 or *6, we use the following mapping.\n--   These special addresses come from:\n--     https://hackage.haskell.org/package/streaming-commons-0.2.3.0/docs/\\\n--     Data-Streaming-Network.html#t:HostPreference\n-- TODO: \"!6\" should not be printed as \"0.0.0.0\" address.\nescapeHostName :: Text -> Text\nescapeHostName \"*\"  = \"0.0.0.0\"\nescapeHostName \"*4\" = \"0.0.0.0\"\nescapeHostName \"!4\" = \"0.0.0.0\"\nescapeHostName \"*6\" = \"0.0.0.0\"\nescapeHostName \"!6\" = \"0.0.0.0\"\nescapeHostName h    = h\n\n-- | Check if a hostname is special\nisSpecialHostName :: Text -> Bool\nisSpecialHostName \"*\"  = True\nisSpecialHostName \"*4\" = True\nisSpecialHostName \"!4\" = True\nisSpecialHostName \"*6\" = True\nisSpecialHostName \"!6\" = True\nisSpecialHostName _    = False\n"
  },
  {
    "path": "src/PostgREST/Observation.hs",
    "content": "{-|\nModule      : PostgREST.Observation\nDescription : This module holds an Observation type which is the core of Observability for PostgREST.\n              The Observation and ObservationHandler (the observer) are abstractions that allow centralizing logging and metrics concerns,\n              only observer calls with an Observation constructor are applied at different parts in the codebase.\n              The Logger and Metrics modules then decide which observations to expose. Not all observations need to be logged nor all correspond to a metric.\n-}\nmodule PostgREST.Observation\n  ( Observation(..)\n  , ObsFatalError(..)\n  , ObservationHandler\n  ) where\n\nimport qualified Hasql.Connection           as SQL\nimport qualified Hasql.Pool                 as SQL\nimport qualified Hasql.Pool.Observation     as SQL\nimport           Network.HTTP.Types.Status  (Status)\nimport           PostgREST.Config.PgVersion\nimport           PostgREST.Query            (MainQuery)\n\nimport Protolude hiding (toList)\n\ndata Observation\n  = AdminStartObs Text\n  | AppStartObs ByteString\n  | AppServerAddressObs Text\n  | ExitUnsupportedPgVersion PgVersion PgVersion\n  | ExitDBNoRecoveryObs\n  | ExitDBFatalError ObsFatalError SQL.UsageError\n  | DBConnectedObs Text\n  | SchemaCacheEmptyObs\n  | SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError\n  | SchemaCacheQueriedObs Double\n  | SchemaCacheLoadedObs Double Text\n  | ConnectionRetryObs Int\n  | DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel\n  | DBListenFail Text (Either SQL.ConnectionError SomeException)\n  | DBListenRetry Int\n  | DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147\n  | DBListenerGotSCacheMsg ByteString\n  | DBListenerGotConfigMsg ByteString\n  | DBListenerConnectionCleanupFail SomeException\n  | QueryObs MainQuery Status\n  | ConfigReadErrorObs SQL.UsageError\n  | ConfigInvalidObs Text\n  | ConfigSucceededObs\n  | QueryRoleSettingsErrorObs SQL.UsageError\n  | QueryErrorCodeHighObs SQL.UsageError\n  | QueryPgVersionError SQL.UsageError\n  | PoolInit Int\n  | PoolAcqTimeoutObs\n  | HasqlPoolObs SQL.Observation\n  | PoolRequest\n  | PoolRequestFullfilled\n  | JwtCacheLookup Bool\n  | JwtCacheEviction\n  | WarpErrorObs Text\n\ndata ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01\n\ntype ObservationHandler = Observation -> IO ()\n"
  },
  {
    "path": "src/PostgREST/Plan/CallPlan.hs",
    "content": "{-# LANGUAGE NamedFieldPuns #-}\nmodule PostgREST.Plan.CallPlan\n  ( CallPlan(..)\n  , CallParams(..)\n  , CallArgs(..)\n  , RpcParamValue(..)\n  , toRpcParams\n  )\nwhere\n\nimport qualified Data.Aeson                        as JSON\nimport qualified Data.ByteString.Lazy              as LBS\nimport qualified Data.HashMap.Strict               as HM\nimport           PostgREST.SchemaCache.Identifiers (FieldName,\n                                                    QualifiedIdentifier)\nimport           PostgREST.SchemaCache.Routine     (Routine (..),\n                                                    RoutineParam (..))\n\nimport Protolude\n\ndata CallPlan = FunctionCall\n  { funCQi           :: QualifiedIdentifier\n  , funCParams       :: CallParams\n  , funCArgs         :: CallArgs\n  , funCScalar       :: Bool\n  , funCSetOfScalar  :: Bool\n  , funCFilterFields :: Set FieldName\n  , funCReturning    :: Set FieldName\n  }\n\ndata CallParams\n  = KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2)\n  | OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val)\n\ndata CallArgs\n  = DirectArgs (HM.HashMap Text RpcParamValue)\n  | JsonArgs (Maybe LBS.ByteString)\n\n-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs\n-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.\ndata RpcParamValue = Fixed Text | Variadic [Text]\ninstance JSON.ToJSON RpcParamValue where\n  toJSON (Fixed    v) = JSON.toJSON v\n  -- Not possible to get here anymore. Variadic arguments are only supported for\n  -- true variadic arguments, but the toJSON instance is only used for the \"single unnamed json argument\" case.\n  toJSON (Variadic v) = JSON.toJSON v\n\n-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{\"a\": \"val1\", \"b\": \"val2\"}\ntoRpcParams :: Routine -> [(Text, Text)] -> HM.HashMap Text RpcParamValue\ntoRpcParams proc prms =\n  if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to map\n    HM.fromList $ second Fixed <$> prms\n  else\n    HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms\n  where\n    mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue\n    mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a\n    mergeParams v _                       = v -- repeated params for non-variadic parameters are not merged\n\ntoRpcParamValue :: Routine -> (Text, Text) -> (Text, RpcParamValue)\ntoRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])\n                            | otherwise       = (k, Fixed v)\n  where\n    prmIsVariadic prm = isJust $ find (\\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc\n"
  },
  {
    "path": "src/PostgREST/Plan/MutatePlan.hs",
    "content": "module PostgREST.Plan.MutatePlan\n  ( MutatePlan(..)\n  )\nwhere\n\nimport qualified Data.ByteString.Lazy as LBS\n\nimport PostgREST.ApiRequest.Preferences  (PreferResolution)\nimport PostgREST.Plan.Types              (CoercibleField,\n                                          CoercibleLogicTree)\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier)\n\n\nimport Protolude\n\ndata MutatePlan\n  = Insert\n      { in_        :: QualifiedIdentifier\n      , insCols    :: [CoercibleField]\n      , insBody    :: Maybe LBS.ByteString\n      , onConflict :: Maybe (PreferResolution, [FieldName])\n      , where_     :: [CoercibleLogicTree]\n      , returning  :: [FieldName]\n      , insPkCols  :: [FieldName]\n      , applyDefs  :: Bool\n      }\n  | Update\n      { in_       :: QualifiedIdentifier\n      , updCols   :: [CoercibleField]\n      , updBody   :: Maybe LBS.ByteString\n      , where_    :: [CoercibleLogicTree]\n      , returning :: [FieldName]\n      , applyDefs :: Bool\n      }\n  | Delete\n      { in_       :: QualifiedIdentifier\n      , where_    :: [CoercibleLogicTree]\n      , returning :: [FieldName]\n      }\n"
  },
  {
    "path": "src/PostgREST/Plan/Negotiate.hs",
    "content": "{-|\nModule      : PostgREST.Plan.Negotiate\nDescription : PostgREST Content Negotiation\n\nThis module contains logic for content negotiation.\nRFC: https://datatracker.ietf.org/doc/html/rfc7231#section-3.4\n-}\n\nmodule PostgREST.Plan.Negotiate\n  ( negotiateContent\n  ) where\n\nimport qualified Data.HashMap.Strict as HM\n\nimport PostgREST.ApiRequest              (ApiRequest (..))\nimport PostgREST.Config                  (AppConfig (..))\nimport PostgREST.Error                   (ApiRequestError (..))\nimport PostgREST.MediaType               (MediaType (..))\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),\n                                          RelIdentifier (..))\nimport PostgREST.SchemaCache.Routine     (MediaHandler (..),\n                                          MediaHandlerMap,\n                                          ResolvedHandler)\n\nimport           PostgREST.ApiRequest.Preferences\nimport           PostgREST.ApiRequest.Types\nimport qualified PostgREST.MediaType              as MediaType\n\nimport Protolude hiding (from)\n\n-- We have two general cases of return values from database objects\n-- (tables/views/functions):\n--\n-- 1. \"un-mime-typed\" values, in most of the cases this is a composite/row\n--    value, for example for tables or views, but also often for functions.\n--    It can be simple integer values or text or bytea as well.\n--\n--    For this, we need handlers to transform the \"non-mime-typed\" values\n--    into \"mimetypes\". We have a default builtin handler that does\n--    \"application/json\". We can add more handlers via aggregates.\n--\n-- 2. \"mime-typed\" values, which specifically return a domain type that is\n--    associated to a certain mimetype. e.g, a function returning only\n--    \"image/png\".\n--\n-- FIXME:\n--   If the function returns a domain type - let's say image/png, we should\n--   accept */*, image/*, and image/png.\n--   Related issue: https://github.com/PostgREST/postgrest/issues/3391\n\n-- | Do content negotiation. i.e. choose a media type based on the\n--   intersection of accepted/produced media types.\nnegotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler\nnegotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =\n  case (act, firstAcceptedPick) of\n    (_, Nothing)                                         -> Left . MediaTypeError $ map MediaType.toMime accepts\n    (ActDb (ActRelationMut _ _), Just (x, mt))           -> Right (if rep == Just Full then x else NoAgg, mt)\n    -- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849\n    -- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.\n    (ActDb (ActRelationRead _ True), Just (_, mt))       -> Right (NoAgg, mt)\n    (ActDb (ActRoutine  _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)\n    (_, Just (x, mt))                                    -> Right (x, mt)\n  where\n    firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.\n    matchMT mt = case mt of\n      -- all the vendored media types have special handling as they have media type parameters, they cannot be overridden\n      m@(MTVndSingularJSON strip)                 -> Just (BuiltinAggSingleJson strip, m)\n      m@MTVndArrayJSONStrip                       -> Just (BuiltinAggArrayJsonStrip, m)\n      m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)\n      m@(MTVndPlan MTVndArrayJSONStrip _ _)       -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)\n      -- TODO the plan should have its own MediaHandler instead of relying on MediaType\n      m@(MTVndPlan mType _ _)                     -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m\n      -- all the other media types can be overridden\n      x                                           -> lookupHandler x\n    mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed\n    lookupHandler mt =\n      when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`\n      when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|>    -- lookup for identifier and a particular media type\n      HM.lookup (RelAnyElement, mt) produces                                    -- lookup for anyelement and a particular media type\n    when' :: Bool -> Maybe a -> Maybe a\n    when' True (Just a) = Just a\n    when' _ _           = Nothing\n"
  },
  {
    "path": "src/PostgREST/Plan/ReadPlan.hs",
    "content": "module PostgREST.Plan.ReadPlan\n  ( ReadPlanTree\n  , ReadPlan(..)\n  , JoinCondition(..)\n  , SpreadType(..)\n  ) where\n\nimport Data.Tree (Tree (..))\n\nimport PostgREST.ApiRequest.Types         (Alias, Depth, Hint,\n                                           JoinType, NodeName)\nimport PostgREST.Plan.Types               (CoercibleLogicTree,\n                                           CoercibleOrderTerm,\n                                           CoercibleSelectField (..),\n                                           RelSelectField (..),\n                                           SpreadType (..))\nimport PostgREST.RangeQuery               (NonnegRange)\nimport PostgREST.SchemaCache.Identifiers  (FieldName,\n                                           QualifiedIdentifier)\nimport PostgREST.SchemaCache.Relationship (Relationship)\n\n\nimport Protolude\n\ntype ReadPlanTree = Tree ReadPlan\n\ndata JoinCondition =\n  JoinCondition\n    (QualifiedIdentifier, FieldName)\n    (QualifiedIdentifier, FieldName)\n  deriving (Eq, Show)\n\n-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable\ndata ReadPlan = ReadPlan\n  { select       :: [CoercibleSelectField]\n  , from         :: QualifiedIdentifier\n  , fromAlias    :: Maybe Alias\n  , where_       :: [CoercibleLogicTree]\n  , order        :: [CoercibleOrderTerm]\n  , range_       :: NonnegRange\n  , relName      :: NodeName\n  , relToParent  :: Maybe Relationship\n  , relJoinConds :: [JoinCondition]\n  , relAlias     :: Maybe Alias\n  , relAggAlias  :: Alias\n  , relHint      :: Maybe Hint\n  , relJoinType  :: Maybe JoinType\n  , relSpread    :: Maybe SpreadType\n  , relSelect    :: [RelSelectField]\n  , depth        :: Depth\n  -- ^ used for aliasing\n  }\n  deriving (Eq, Show)\n"
  },
  {
    "path": "src/PostgREST/Plan/Types.hs",
    "content": "module PostgREST.Plan.Types\n  ( CoercibleField(..)\n  , CoercibleSelectField(..)\n  , unknownField\n  , CoercibleLogicTree(..)\n  , CoercibleFilter(..)\n  , TransformerProc\n  , ToTsVector(..)\n  , CoercibleOrderTerm(..)\n  , RelSelectField(..)\n  , RelJsonEmbedMode(..)\n  , SpreadSelectField(..)\n  , SpreadType(..)\n  ) where\n\nimport PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,\n                                   Field, JsonPath, Language,\n                                   LogicOperator, OpExpr,\n                                   OrderDirection, OrderNulls)\n\nimport PostgREST.SchemaCache.Identifiers (FieldName)\n\nimport Protolude\n\ntype TransformerProc = Text\n\nnewtype ToTsVector = ToTsVector (Maybe Language)\n  deriving (Eq, Show)\n\n-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.\n-- |\n-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.\n-- |\n-- | In the simplest case, it allows us to parse JSON payloads with `json_to_recordset`, for which we need to know both the name and the type of each thing we'd like to extract. At a higher level, CoercibleField generalises to reflect that any value we work with in a query may need type specific handling.\n-- |\n-- | CoercibleField is the foundation for the Data Representations feature. This feature allow user-definable mappings between database types so that the same data can be presented or interpreted in various ways as needed. Sometimes the way Postgres coerces data implicitly isn't right for the job. Different mappings might be appropriate for different situations: parsing a filter from a query string requires one function (text -> field type) while parsing a payload from JSON takes another (json -> field type). And the reverse, outputting a field as JSON, requires yet a third (field type -> json). CoercibleField is that \"job specific\" reference to an element paired with the type we desire for that particular purpose and the function we'll use to get there, if any.\n-- |\n-- | In the planning phase, we \"resolve\" generic named elements into these specialised CoercibleFields. Again this is context specific: two different CoercibleFields both representing the exact same table column in the database, even in the same query, might have two different target types and mapping functions. For example, one might represent a column in a filter, and another the very same column in an output role to be sent in the response body.\n-- |\n-- | The type value is allowed to be the empty string. The analog here is soft type checking in programming languages: sometimes we don't need a variable to have a specified type and things will work anyhow. So the empty type variant is valid when we don't know and *don't need to know* about the specific type in some context. Note that this variation should not be used if it guarantees failure: in that case you should instead raise an error at the planning stage and bail out. For example, we can't parse JSON with `json_to_recordset` without knowing the types of each recipient field, and so error out. Using the empty string for the type would be incorrect and futile. On the other hand we use the empty type for RPC calls since type resolution isn't implemented for RPC, but it's fine because the query still works with Postgres' implicit coercion. In the future, hopefully we will support data representations across the board and then the empty type may be permanently retired.\ndata CoercibleField = CoercibleField\n  { cfName       :: FieldName\n  , cfJsonPath   :: JsonPath\n  , cfToJson     :: Bool\n  , cfToTsVector :: Maybe ToTsVector      -- ^ If the field should be converted using to_tsvector(<language>, <field>)\n  , cfIRType     :: Text                  -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.\n  , cfBaseType   :: Text                  -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)\n  , cfTransform  :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.\n  , cfDefault    :: Maybe Text\n  , cfFullRow    :: Bool                  -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.\n  } deriving (Eq, Show)\n\nunknownField :: FieldName -> JsonPath -> CoercibleField\nunknownField name path = CoercibleField name path False Nothing \"\" \"\" Nothing Nothing False\n\n-- | Like an API request LogicTree, but with coercible field information.\ndata CoercibleLogicTree\n  = CoercibleExpr Bool LogicOperator [CoercibleLogicTree]\n  | CoercibleStmnt CoercibleFilter\n  deriving (Eq, Show)\n\ndata CoercibleFilter = CoercibleFilter\n  { field  :: CoercibleField\n  , opExpr :: OpExpr\n  }\n  | CoercibleFilterNullEmbed Bool FieldName\n  deriving (Eq, Show)\n\ndata CoercibleOrderTerm\n  = CoercibleOrderTerm\n    { coField     :: CoercibleField\n    , coDirection :: Maybe OrderDirection\n    , coNullOrder :: Maybe OrderNulls\n    }\n  | CoercibleOrderRelationTerm\n    { coRelation  :: FieldName\n    , coRelTerm   :: Field\n    , coDirection :: Maybe OrderDirection\n    , coNullOrder :: Maybe OrderNulls\n    }\n  deriving (Eq, Show)\n\ndata CoercibleSelectField = CoercibleSelectField\n  { csField       :: CoercibleField\n  , csAggFunction :: Maybe AggregateFunction\n  , csAggCast     :: Maybe Cast\n  , csCast        :: Maybe Cast\n  , csAlias       :: Maybe Alias\n  }\n  deriving (Eq, Show)\n\ndata RelJsonEmbedMode = JsonObject | JsonArray\n  deriving (Show, Eq)\n\ndata RelSelectField\n  = JsonEmbed\n      { rsSelName    :: FieldName\n      , rsAggAlias   :: Alias\n      , rsEmbedMode  :: RelJsonEmbedMode\n      , rsEmptyEmbed :: Bool\n      }\n  | Spread\n      { rsSpreadSel :: [SpreadSelectField]\n      , rsAggAlias  :: Alias\n      }\n  deriving (Eq, Show)\n\ndata SpreadSelectField =\n  SpreadSelectField\n  { ssSelName        :: FieldName\n  , ssSelAggFunction :: Maybe AggregateFunction\n  , ssSelAggCast     :: Maybe Cast\n  , ssSelAlias       :: Maybe Alias\n  }\n  deriving (Eq, Show)\n\ndata SpreadType\n  = ToOneSpread\n  | ToManySpread\n    { stExtraSelect :: [(Maybe FieldName, CoercibleSelectField)]\n    , stOrder       :: [CoercibleOrderTerm]\n    }\n  deriving (Eq, Show)\n"
  },
  {
    "path": "src/PostgREST/Plan.hs",
    "content": "{-|\nModule      : PostgREST.Plan\nDescription : PostgREST Request Planner\n\nThis module is in charge of building an intermediate\nrepresentation between the HTTP request and the\nfinal response, which may or not result in SQL execution\n(computing OpenAPI or OPTIONS requests don't require database interaction)\n\nA query tree is built in case of resource embedding. By inferring the\nrelationship between tables, join conditions are added for every embedded\nresource.\n-}\n{-# LANGUAGE DuplicateRecordFields #-}\n{-# LANGUAGE LambdaCase            #-}\n{-# LANGUAGE NamedFieldPuns        #-}\n{-# LANGUAGE RecordWildCards       #-}\n\nmodule PostgREST.Plan\n  ( actionPlan\n  , ActionPlan(..)\n  , DbActionPlan(..)\n  , InspectPlan(..)\n  , InfoPlan(..)\n  , CrudPlan(..)\n  ) where\n\nimport qualified Data.HashMap.Strict           as HM\nimport qualified Data.HashMap.Strict.InsOrd    as HMI\nimport qualified Data.List                     as L\nimport qualified Data.Set                      as S\nimport qualified Data.Text                     as T\nimport qualified PostgREST.SchemaCache.Routine as Routine\n\nimport Data.Either.Combinators (mapLeft, mapRight)\nimport Data.List               (delete, lookup)\nimport Data.Maybe              (fromJust)\nimport Data.Tree               (Tree (..))\n\nimport PostgREST.ApiRequest                  (ApiRequest (..))\nimport PostgREST.Config                      (AppConfig (..))\nimport PostgREST.Error                       (ApiRequestError (..),\n                                              Error (..),\n                                              SchemaCacheError (..))\nimport PostgREST.MediaType                   (MediaType (..))\nimport PostgREST.Plan.Negotiate              (negotiateContent)\nimport PostgREST.Query.SqlFragment           (sourceCTEName)\nimport PostgREST.RangeQuery                  (NonnegRange, allRange,\n                                              convertToLimitZeroRange,\n                                              restrictRange)\nimport PostgREST.SchemaCache                 (SchemaCache (..))\nimport PostgREST.SchemaCache.Identifiers     (FieldName,\n                                              QualifiedIdentifier (..),\n                                              Schema)\nimport PostgREST.SchemaCache.Relationship    (Cardinality (..),\n                                              Junction (..),\n                                              Relationship (..),\n                                              RelationshipsMap,\n                                              relIsToOne)\nimport PostgREST.SchemaCache.Representations (DataRepresentation (..),\n                                              RepresentationsMap)\nimport PostgREST.SchemaCache.Routine         (MediaHandler (..),\n                                              Routine (..),\n                                              RoutineMap,\n                                              RoutineParam (..),\n                                              funcReturnsScalar,\n                                              funcReturnsSetOfScalar,\n                                              funcReturnsSingle)\nimport PostgREST.SchemaCache.Table           (Column (..), Table (..),\n                                              TablesMap,\n                                              tableColumnsList,\n                                              tablePKCols)\n\nimport PostgREST.ApiRequest.Preferences\nimport PostgREST.ApiRequest.Types\nimport PostgREST.Plan.CallPlan\nimport PostgREST.Plan.MutatePlan\nimport PostgREST.Plan.ReadPlan          as ReadPlan\nimport PostgREST.Plan.Types\n\nimport qualified Hasql.Transaction.Sessions       as SQL\nimport qualified PostgREST.ApiRequest.QueryParams as QueryParams\nimport qualified PostgREST.MediaType              as MediaType\n\nimport Protolude hiding (from)\n\n-- $setup\n-- Setup for doctests\n-- >>> import Data.Ranged.Ranges (fullRange)\n\n-- Plan for reading or writing to the db\ndata CrudPlan\n  = WrappedReadPlan\n  { wrReadPlan :: ReadPlanTree\n  , pTxMode    :: SQL.Mode\n  , wrHandler  :: MediaHandler\n  , pMedia     :: MediaType\n  , wrHdrsOnly :: Bool\n  , crudQi     :: QualifiedIdentifier\n  }\n  | MutateReadPlan {\n    mrReadPlan   :: ReadPlanTree\n  , mrMutatePlan :: MutatePlan\n  , pTxMode      :: SQL.Mode\n  , mrHandler    :: MediaHandler\n  , pMedia       :: MediaType\n  , mrMutation   :: Mutation\n  , crudQi       :: QualifiedIdentifier\n  }\n  | CallReadPlan {\n    crReadPlan :: ReadPlanTree\n  , crCallPlan :: CallPlan\n  , pTxMode    :: SQL.Mode\n  , crProc     :: Routine\n  , crHandler  :: MediaHandler\n  , pMedia     :: MediaType\n  , crInvMthd  :: InvokeMethod\n  , crQi       :: QualifiedIdentifier\n  }\n\n-- Plan for reading db object metadadta\ndata InspectPlan = InspectPlan {\n    ipMedia    :: MediaType\n  , ipTxmode   :: SQL.Mode\n  , ipHdrsOnly :: Bool\n  , ipSchema   :: Schema\n  }\n\n-- A Plan may use the the database or not\ndata ActionPlan\n  = Db DbActionPlan\n  | NoDb InfoPlan\n\ntype IsDbExplain = Bool\n\n-- A db plan can consist on read/write, rpc call or reading metadata (which may use the db or just use cached objects)\ndata DbActionPlan\n  = DbCrud   IsDbExplain CrudPlan\n  | MayUseDb InspectPlan\n\n-- Plans that don't use the database\ndata InfoPlan\n  = RelInfoPlan QualifiedIdentifier -- info about relation\n  | RoutineInfoPlan Routine -- info about function\n  | SchemaInfoPlan -- info about schema cache\n\nactionPlan :: Action -> AppConfig -> ApiRequest -> SchemaCache -> Either Error ActionPlan\nactionPlan act conf apiReq sCache = case act of\n  ActDb dbAct              -> Db <$> dbActionPlan dbAct conf apiReq sCache\n  ActRelationInfo ident    -> pure . NoDb $ RelInfoPlan ident\n  ActRoutineInfo ident inv ->\n    let crPln = callReadPlan ident conf sCache apiReq inv in\n    NoDb . RoutineInfoPlan . crProc <$> crPln\n  ActSchemaInfo            -> pure $ NoDb SchemaInfoPlan\n\ndbActionPlan :: DbAction -> AppConfig -> ApiRequest -> SchemaCache -> Either Error DbActionPlan\ndbActionPlan dbAct conf apiReq sCache = case dbAct of\n  ActRelationRead identifier headersOnly ->\n    toDbActPlan <$> wrappedReadPlan identifier conf sCache apiReq headersOnly\n  ActRelationMut identifier mut ->\n    toDbActPlan <$> mutateReadPlan mut apiReq identifier conf sCache\n  ActRoutine identifier invMethod ->\n    toDbActPlan <$> callReadPlan identifier conf sCache apiReq invMethod\n  ActSchemaRead tSchema headersOnly ->\n    MayUseDb <$> inspectPlan apiReq headersOnly tSchema\n  where\n    toDbActPlan pl = case pMedia pl of\n      MTVndPlan{} -> DbCrud True pl\n      _           -> DbCrud False pl\n\nwrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan\nwrappedReadPlan  identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do\n  qi <- findTable identifier sCache\n  rPlan <- readPlan qi conf sCache apiRequest\n  (handler, mediaType)  <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)\n  if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()\n  return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi\n\nmutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan\nmutateReadPlan  mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do\n  qi <- findTable identifier sCache\n  rPlan <- readPlan qi conf sCache apiRequest\n  mPlan <- mutatePlan mutation qi apiRequest sCache rPlan\n  if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()\n  (handler, mediaType)  <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)\n  return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi\n\ncallReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan\ncallReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do\n  let paramKeys = case invMethod of\n        InvRead _ -> S.fromList $ fst <$> qsParams'\n        Inv       -> iColumns\n  proc@Function{..} <- mapLeft SchemaCacheErr $\n    findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)\n  let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations\n  rPlan <- readPlan relIdentifier conf sCache apiRequest\n  let args = case (invMethod, iContentMediaType) of\n        (InvRead _, _)      -> DirectArgs $ toRpcParams proc qsParams'\n        (Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload\n        (Inv, _)            -> JsonArgs $ payRaw <$> iPayload\n      txMode = case (invMethod, pdVolatility) of\n          (InvRead _,  _)          -> SQL.Read\n          (Inv, Routine.Stable)    -> SQL.Read\n          (Inv, Routine.Immutable) -> SQL.Read\n          (Inv, Routine.Volatile)  -> SQL.Write\n      cPlan = callPlan proc apiRequest paramKeys args rPlan\n  (handler, mediaType)  <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)\n  if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()\n  failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc\n  return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier\n  where\n    qsParams' = QueryParams.qsParams iQueryParams\n\n    failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()\n    failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestErr MaxAffectedRpcViolation else Right ()\n    failMaxAffectedRpcReturnsSingle _ _ = Right ()\n\nhasDefaultSelect :: ReadPlanTree -> Bool\nhasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == \"*\"\nhasDefaultSelect _ = False\n\ninspectPlan :: ApiRequest -> Bool -> Schema -> Either Error InspectPlan\ninspectPlan apiRequest headersOnly schema = do\n  let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny]\n      accepts     = iAcceptMediaType apiRequest\n  mediaType <- if not . null $ L.intersect accepts producedMTs\n    then Right MTOpenAPI\n    else Left . ApiRequestErr . MediaTypeError $ MediaType.toMime <$> accepts\n  return $ InspectPlan mediaType SQL.Read headersOnly schema\n\n{-|\n  Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,\n  the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.\n-}\nfindProc :: QualifiedIdentifier -> S.Set Text -> RoutineMap -> MediaType -> Bool -> Either SchemaCacheError Routine\nfindProc qi argumentsKeys allProcs contentMediaType isInvPost =\n  case matchProc of\n    ([], [])     -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) contentMediaType isInvPost (HM.keys allProcs) lookupProcName\n    -- If there are no functions with named arguments, fallback to the single unnamed argument function\n    ([], [proc]) -> Right proc\n    ([], procs)  -> Left $ AmbiguousRpc (toList procs)\n    -- Matches the functions with named arguments\n    ([proc], _)  -> Right proc\n    (procs, _)   -> Left $ AmbiguousRpc (toList procs)\n  where\n    matchProc = overloadedProcPartition lookupProcName\n    -- First find the proc by name\n    lookupProcName = HM.lookupDefault mempty qi allProcs\n    -- The partition obtained has the form (overloadedProcs,fallbackProcs)\n    -- where fallbackProcs are functions with a single unnamed parameter\n    overloadedProcPartition = foldr select ([],[])\n    select proc ~(ts,fs)\n      | matchesParams proc         = (proc:ts,fs)\n      | hasSingleUnnamedParam proc = (ts,proc:fs)\n      | otherwise                  = (ts,fs)\n    -- If the function is called with post and has a single unnamed parameter\n    -- it can be called depending on content type and the parameter type.\n    -- The parameter must have no declared name (ppName == mempty).\n    hasSingleUnnamedParam Function{pdParams=[RoutineParam{ppName, ppType}]} =\n      isInvPost && ppName == mempty && case (contentMediaType, ppType) of\n        (MTApplicationJSON, \"json\")  -> True\n        (MTApplicationJSON, \"jsonb\") -> True\n        (MTTextPlain, \"text\")        -> True\n        (MTTextXML, \"xml\")           -> True\n        (MTOctetStream, \"bytea\")     -> True\n        _                            -> False\n    hasSingleUnnamedParam _ = False\n    matchesParams proc =\n      let\n        params = pdParams proc\n      in\n      -- If the function has no parameters, the arguments keys must be empty as well\n      if null params\n        then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])\n      -- A function has optional and required parameters. Optional parameters have a default value and\n      -- don't require arguments for the function to be executed, required parameters must have an argument present.\n      else case L.partition ppReq params of\n      -- If the function only has required parameters, the arguments keys must match those parameters\n        (reqParams, [])        -> argumentsKeys == S.fromList (ppName <$> reqParams)\n      -- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)\n        ([], optParams)        -> argumentsKeys `S.isSubsetOf` S.fromList (ppName <$> optParams)\n      -- If the function has required and optional parameters, the arguments keys have to match the required parameters\n      -- and can match any or none of the default parameters.\n        (reqParams, optParams) -> argumentsKeys `S.difference` S.fromList (ppName <$> optParams) == S.fromList (ppName <$> reqParams)\n\n-- | During planning we need to resolve Field -> CoercibleField (finding the context specific target type and map function).\n-- | ResolverContext facilitates this without the need to pass around a laundry list of parameters.\ndata ResolverContext = ResolverContext\n  { tables          :: TablesMap\n  , representations :: RepresentationsMap\n  , qi              :: QualifiedIdentifier  -- ^ The table we're currently attending; changes as we recurse into joins etc.\n  , outputType      :: Text                 -- ^ The output type for the response payload; e.g. \"csv\", \"json\", \"binary\".\n  }\n\nresolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField\nresolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False\n\nresolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField\nresolveTableFieldName table fieldName toTsV=\n  fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=\n    Just . flip resolveColumnField toTsV\n\n-- | 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.\nresolveTypeOrUnknown :: ResolverContext -> Field -> Maybe ToTsVector -> CoercibleField\nresolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =\n  case res of\n    -- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`\n    -- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594\n    cf@CoercibleField{cfIRType=\"json\"}       -> cf{cfJsonPath=jp, cfToJson=False}\n    cf@CoercibleField{cfIRType=\"jsonb\"}      -> cf{cfJsonPath=jp, cfToJson=False}\n    -- Do not apply to_tsvector to tsvector types\n    cf@CoercibleField{cfBaseType=\"tsvector\"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}\n    -- other types will get converted `to_jsonb(col)->attr`, even unknown types\n    cf                                       -> cf{cfJsonPath=jp, cfToJson=True}\n  where\n    res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=\n          Just . (\\t -> resolveTableFieldName t fn toTsV)\n\n-- | Install any pre-defined data representation from source to target to coerce this reference.\n--\n-- Note that we change the IR type here. This might seem unintuitive. The short of it is that for a CoercibleField without a transformer, input type == output type. A transformer maps from a -> b, so by definition the input type will be a and the output type b after. And cfIRType is the *input* type.\n--\n-- It might feel odd that once a transformer is added we 'forget' the target type (because now a /= b). You might also note there's no obvious way to stack transforms (even if there was a stack, you erased what type you're working with so it's awkward). Alas as satisfying as it would be to engineer a layered mapping system with full type information, we just don't need it.\nwithTransformer :: ResolverContext -> Text -> Text -> CoercibleField -> CoercibleField\nwithTransformer ResolverContext{representations} sourceType targetType field =\n  fromMaybe field $ HM.lookup (sourceType, targetType) representations >>=\n    (\\fieldRepresentation -> Just field{cfIRType=sourceType, cfTransform=Just (drFunction fieldRepresentation)})\n\n-- | Map the intermediate representation type to the output type, if available.\nwithOutputFormat :: ResolverContext -> CoercibleField -> CoercibleField\nwithOutputFormat ctx@ResolverContext{outputType} field@CoercibleField{cfIRType} = withTransformer ctx cfIRType outputType field\n\n-- | Map text into the intermediate representation type, if available.\nwithTextParse :: ResolverContext -> CoercibleField -> CoercibleField\nwithTextParse ctx field@CoercibleField{cfIRType} = withTransformer ctx \"text\" cfIRType field\n\n-- | Map json into the intermediate representation type, if available.\nwithJsonParse :: ResolverContext -> CoercibleField -> CoercibleField\nwithJsonParse ctx field@CoercibleField{cfIRType} = withTransformer ctx \"json\" cfIRType field\n\n-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.\nresolveOutputField :: ResolverContext -> Field -> CoercibleField\nresolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field Nothing\n\n-- | Map the query string format of a value (text) into the intermediate representation type, if available.\nresolveQueryInputField :: ResolverContext -> Field -> OpExpr -> CoercibleField\nresolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnknown ctx field toTsVector\n  where\n    toTsVector = case opExpr of\n      OpExpr _ (Fts _ lang _) -> Just $ ToTsVector lang\n      _                       -> Nothing\n\n-- | Builds the ReadPlan tree on a number of stages.\n-- | Adds filters, order, limits on its respective nodes.\n-- | Adds joins conditions obtained from resource embedding.\nreadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree\nreadPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest  =\n  let\n    -- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.\n    ctx = ResolverContext dbTables dbRepresentations qi \"json\"\n  in\n    treeRestrictRange configDbMaxRows (iAction apiRequest) =<<\n    addToManyOrderSelects =<<\n    hoistSpreadAggFunctions =<<\n    validateAggFunctions configDbAggregates =<<\n    addRelSelects =<<\n    addNullEmbedFilters =<<\n    addRelatedOrders =<<\n    addAliases =<<\n    expandStars ctx =<<\n    addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<\n    addLogicTrees ctx apiRequest =<<\n    addRanges apiRequest =<<\n    addOrders ctx apiRequest =<<\n    addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)\n\n-- Build the initial read plan tree\ninitReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree\ninitReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =\n  foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []\n  where\n    rootDepth = 0\n    defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth\n    treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree\n    treeEntry depth (Node si fldForest) (Node q rForest) =\n      let nxtDepth = succ depth in\n      case si of\n        SelectRelation{..} ->\n          Node q $\n            foldr (treeEntry nxtDepth)\n            (Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relAlias=selAlias, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth} [])\n            fldForest:rForest\n        SpreadRelation{..} ->\n          Node q $\n            foldr (treeEntry nxtDepth)\n            (Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relSpread=Just ToOneSpread} [])\n            fldForest:rForest\n        SelectField{..} ->\n          Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest\n\n-- If an alias is explicitly specified, it is always respected. However, an alias may be\n-- determined automatically in these cases:\n-- * A select term with a JSON path\n-- * Domain representations\n-- * Aggregates in spread relationships\naddAliases :: ReadPlanTree -> Either Error ReadPlanTree\naddAliases = Right . fmap addAliasToPlan\n  where\n    addAliasToPlan rp@ReadPlan{select=sel, relSpread=spr} = rp{select=map (aliasSelectField $ isJust spr) sel}\n\n    aliasSelectField :: Bool -> CoercibleSelectField -> CoercibleSelectField\n    aliasSelectField isSpread field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}\n      | isJust alias = field\n      | isJust aggFun = fieldAliasForSpreadAgg isSpread field\n      | isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }\n      | isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }\n      | otherwise = field\n\n    -- Spread relationships with non-aliased aggregates can cause problems when selecting the fields in the top level resource.\n    -- The top level won't know the name of the field in this case:\n    -- A nested to-one spread like `/top_table?select=...middle_table(...nested_table(count()))`\n    -- will do a `SELECT nested_table` instead of `SELECT *`, because doing a `COUNT(*)` in `top_table`\n    -- would not return the desired results.\n    --\n    -- That's why we need to use the aggregate name as an alias (e.g. COUNT(...) AS \"count\").\n    -- Since PostgreSQL labels the columns with the aggregate name, it shouldn't be a problem to\n    -- apply the aliases to all the aggregates regardless if the previous conditions are met.\n    fieldAliasForSpreadAgg True field@CoercibleSelectField{csAggFunction=Just agg} =\n      field { csAlias = Just (T.toLower $ show agg) }\n    fieldAliasForSpreadAgg _ field = field\n\n    isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True\n    isJsonKeyPath _                                 = False\n\n    isTransformPath CoercibleField{cfTransform=(Just _), cfName=_} = True\n    isTransformPath _ = False\n\n    lastJsonKey CoercibleField{cfName=fieldName, cfJsonPath=jsonPath} =\n      case jOp <$> lastMay jsonPath of\n            Just (JKey key) -> Just key\n            Just (JIdx _)   -> Just $ fromMaybe fieldName lastKey\n              -- We get the lastKey because on:\n              -- `select=data->1->mycol->>2`, we need to show the result as [ {\"mycol\": ..}, {\"mycol\": ..} ]\n              -- `select=data->3`, we need to show the result as [ {\"data\": ..}, {\"data\": ..} ]\n              where lastKey = jVal <$> find (\\case JKey{} -> True; _ -> False) (jOp <$> reverse jsonPath)\n            Nothing -> Nothing\n\nknownColumnsInContext :: ResolverContext -> [Column]\nknownColumnsInContext ResolverContext{..} =\n  fromMaybe [] $ HM.lookup qi tables >>=\n  Just . tableColumnsList\n\n-- | Expand \"select *\" into explicit field names of the table in the following situations:\n-- * When there are data representations present.\n-- * When there is an aggregate function in a given ReadPlan or its parent.\n-- * When the ReadPlan is a to-many spread relationship\nexpandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree\nexpandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree\n  where\n    expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree\n    expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias, relSpread=spread} children) =\n      let\n        newHasAgg = hasAgg || any (isJust . csAggFunction) select || case spread of Just ToManySpread{} -> True; _ -> False\n        newCtx = adjustContext ctx fromQI alias\n        newRPlan = expandStarsForTable newCtx newHasAgg rp\n      in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)\n\n    -- Choose the appropriate context based on whether we're dealing with \"pgrst_source\"\n    adjustContext :: ResolverContext -> QualifiedIdentifier -> Maybe Text -> ResolverContext\n    -- When the schema is \"\" and the table is the source CTE, we assume the true source table is given in the from\n    -- alias and belongs to the request schema. See the bit in `addRels` with `newFrom = ...`.\n    adjustContext context@ResolverContext{qi=ctxQI} (QualifiedIdentifier \"\" \"pgrst_source\") (Just a) = context{qi=ctxQI{qiName=a}}\n    adjustContext context fromQI _ = context{qi=fromQI}\n\nexpandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan\nexpandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread}\n  -- We expand if either of the below are true:\n  -- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.\n  -- * We have a '*' select AND the target table has at least one data representation.\n  -- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed,\n  -- we tag it as \"full row\" in that case.\n  | hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields}\n  | otherwise = rp\n  where\n    hasStarSelect = \"*\" `elem` map (cfName . csField) filteredSelectFields\n    filteredSelectFields = filter (shouldExpandOrTag . csAggFunction) selectFields\n    shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)\n    hasDataRepresentation = any hasOutputRep knownColumns\n    knownColumns = knownColumnsInContext ctx\n\n    hasOutputRep :: Column -> Bool\n    hasOutputRep col = HM.member (colNominalType col, outputType) representations\n\n    expandStarSelectField :: Bool -> [Column] -> CoercibleSelectField -> [CoercibleSelectField]\n    expandStarSelectField _ columns sel@CoercibleSelectField{csField=CoercibleField{cfName=\"*\", cfJsonPath=[]}, csAggFunction=Nothing} =\n      map (\\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col Nothing }) columns\n    expandStarSelectField True _ sel@CoercibleSelectField{csField=fld@CoercibleField{cfName=\"*\", cfJsonPath=[]}, csAggFunction=Just Count} =\n      [sel { csField = fld { cfFullRow = True } }]\n    expandStarSelectField _ _ selectField = [selectField]\n\n-- | Enforces the `max-rows` config on the result\ntreeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree\ntreeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request\ntreeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request\n  where\n    nodeRestrictRange :: Maybe Integer -> ReadPlan -> ReadPlan\n    nodeRestrictRange m q@ReadPlan{range_=r} = q{range_= convertToLimitZeroRange r (restrictRange m r) }\n\n-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)\n-- also adds aliasing\naddRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either Error ReadPlanTree\naddRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,relSpread,depth} forest) =\n  case parentNode of\n    Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->\n      let\n        newReadPlan = (\\r ->\n          let newAlias = Just (qiName (relForeignTable r) <> \"_\" <> show depth)\n              aggAlias = qiName (relTable r) <> \"_\" <> fromMaybe relName relAlias <> \"_\" <> show depth\n              updSpread = if isJust relSpread && not (relIsToOne r) then Just $ ToManySpread [] [] else relSpread in\n          case r of\n            Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing\n              rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r, relSpread=updSpread}\n            ComputedRelationship{} ->\n              rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias, relSpread=updSpread}\n            _ ->\n              rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r, relSpread=updSpread}\n          ) <$> rel\n        origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship\n          then fromMaybe (qiName parentNodeQi) parentAlias\n          else qiName parentNodeQi\n        rel = findRel schema allRels origin relName relHint\n      in\n      Node <$> newReadPlan <*> (updateForest . hush $ Node <$> newReadPlan <*> pure forest)\n    Nothing -> -- root case\n      let\n        newFrom  = QualifiedIdentifier mempty sourceCTEName\n        newAlias = Just (qiName $ from rPlan)\n        newReadPlan = case action of\n          -- the CTE for mutations/rpc is used as WITH sourceCTEName .. SELECT .. FROM sourceCTEName as alias,\n          -- we use the table name as an alias so findRel can find the right relationship.\n          ActDb (ActRelationMut _ _) -> rPlan{from=newFrom, fromAlias=newAlias}\n          ActDb (ActRoutine _ _)     -> rPlan{from=newFrom, fromAlias=newAlias}\n          _                  -> rPlan\n      in\n      Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest)\n  where\n    updateForest :: Maybe ReadPlanTree -> Either Error [ReadPlanTree]\n    updateForest rq = addRels schema action allRels rq `traverse` forest\n\ngetJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]\ngetJoinConditions _ _ ComputedRelationship{} = []\ngetJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=fQi,relCardinality=card} =\n  case card of\n    M2M (Junction QualifiedIdentifier{qiName=jtn} _ _ jcols1 jcols2) ->\n      (toJoinCondition Nothing Nothing ftN jtn <$> jcols2) ++ (toJoinCondition parentAlias tblAlias tN jtn <$> jcols1)\n    O2M _ cols ->\n      toJoinCondition parentAlias tblAlias tN ftN <$> cols\n    M2O _ cols ->\n      toJoinCondition parentAlias tblAlias tN ftN <$> cols\n    O2O _ cols _ ->\n      toJoinCondition parentAlias tblAlias tN ftN <$> cols\n  where\n    QualifiedIdentifier{qiSchema=tSchema, qiName=tN} = qi\n    QualifiedIdentifier{qiName=ftN} = fQi\n    toJoinCondition :: Maybe Alias -> Maybe Alias -> Text -> Text -> (FieldName, FieldName) -> JoinCondition\n    toJoinCondition prAl newAl tb ftb (c, fc) =\n      let qi1 = QualifiedIdentifier tSchema ftb\n          qi2 = QualifiedIdentifier tSchema tb in\n        JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAl, fc)\n                      (maybe qi2 (QualifiedIdentifier mempty) prAl, c)\n\n-- Finds a relationship between an origin and a target in the request:\n-- /origin?select=target(*) If more than one relationship is found then the\n-- request is ambiguous and we return an error.  In that case the request can\n-- be disambiguated by adding precision to the target or by using a hint:\n-- /origin?select=target!hint(*). The origin can be a table or view.\nfindRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either Error Relationship\nfindRel schema allRels origin target hint =\n  case rels of\n    []  -> Left $ SchemaCacheErr $ NoRelBetween origin target hint schema allRels\n    [r] -> Right r\n    rs  -> Left $ SchemaCacheErr $ AmbiguousRelBetween origin target rs\n  where\n    matchFKSingleCol hint_ card = case card of\n      O2M{relColumns=[(col, _)]} -> hint_ == col\n      M2O{relColumns=[(col, _)]} -> hint_ == col\n      O2O{relColumns=[(col, _)]} -> hint_ == col\n      _                          -> False\n    matchFKRefSingleCol hint_ card  = case card of\n      O2M{relColumns=[(_, fCol)]} -> hint_ == fCol\n      M2O{relColumns=[(_, fCol)]} -> hint_ == fCol\n      O2O{relColumns=[(_, fCol)]} -> hint_ == fCol\n      _                           -> False\n    matchConstraint tar card = case card of\n      O2M{relCons} -> tar == relCons\n      M2O{relCons} -> tar == relCons\n      O2O{relCons} -> tar == relCons\n      _            -> False\n    matchJunction hint_ card = case card of\n      M2M Junction{junTable} -> hint_ == qiName junTable\n      _                      -> False\n    isM2O card = case card of\n      M2O _ _ -> True\n      _       -> False\n    isO2M card = case card of\n      O2M _ _ -> True\n      _       -> False\n    rels = filter (\\case\n      ComputedRelationship{relFunction} -> target == qiName relFunction\n      Relationship{..} ->\n        -- In a self-relationship we have a single foreign key but two relationships with different cardinalities: M2O/O2M. For disambiguation, we use the convention of getting:\n        -- TODO: handle one-to-one and many-to-many self-relationships\n        if relIsSelf\n        then case hint of\n          Nothing ->\n            -- The O2M by using the table name in the target\n            target == qiName relForeignTable && isO2M relCardinality -- /family_tree?select=children:family_tree(*)\n            ||\n            -- The M2O by using the column name in the target\n            matchFKSingleCol target relCardinality && isM2O relCardinality -- /family_tree?select=parent(*)\n          Just hnt ->\n            -- /organizations?select=auditees:organizations!auditor(*)\n            target == qiName relForeignTable && isO2M relCardinality\n            && matchFKRefSingleCol hnt relCardinality -- auditor\n        else case hint of\n          -- DEPRECATED(remove after 2 major releases since v11.1.0): remove target\n          -- target = table / view / constraint / column-from-origin (constraint/column-from-origin can only come from tables https://github.com/PostgREST/postgrest/issues/2277)\n          -- DEPRECATED(remove after 2 major releases since v11.1.0): remove hint as table/view/columns and only leave it as constraint\n          -- hint   = table / view / constraint / column-from-origin / column-from-target (hint can take table / view values to aid in finding the junction in an m2m relationship)\n          Nothing ->\n              -- /projects?select=clients(*)\n              target == qiName relForeignTable -- clients\n              ||\n              -- /projects?select=projects_client_id_fkey(*)\n              matchConstraint target relCardinality -- projects_client_id_fkey\n              && not relFTableIsView\n              ||\n              -- /projects?select=client_id(*)\n              matchFKSingleCol target relCardinality -- client_id\n              && not relFTableIsView\n          Just hnt ->\n            -- /projects?select=clients(*)\n            target == qiName relForeignTable -- clients\n            && (\n              -- /projects?select=clients!projects_client_id_fkey(*)\n              matchConstraint hnt relCardinality || -- projects_client_id_fkey\n\n              -- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)\n              matchFKSingleCol hnt relCardinality      || -- client_id\n              matchFKRefSingleCol hnt relCardinality   || -- id\n\n              -- /users?select=tasks!users_tasks(*) many-to-many between users and tasks\n              matchJunction hnt relCardinality -- users_tasks\n            )\n      ) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels\n\n\naddRelSelects :: ReadPlanTree -> Either Error ReadPlanTree\naddRelSelects node@(Node rp forest)\n  | null forest = Right node\n  | otherwise   =\n    let newForest     = rights $ addRelSelects <$> forest\n        newRelSelects = mapMaybe generateRelSelectField newForest\n    in Right $ Node rp { relSelect = newRelSelects } newForest\n\ngenerateRelSelectField :: ReadPlanTree -> Maybe RelSelectField\ngenerateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relSpread = Just _} _) =\n  Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }\ngenerateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relSpread = Nothing} forest) =\n  Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }\n  where\n    rsSelName = fromMaybe relName relAlias\n    rsEmbedMode = if relIsToOne rel then JsonObject else JsonArray\n    rsEmptyEmbed = hasOnlyNullEmbed (null select) forest\n    hasOnlyNullEmbed = foldr checkIfNullEmbed\n    checkIfNullEmbed :: ReadPlanTree -> Bool -> Bool\n    checkIfNullEmbed (Node ReadPlan{select=s} f) isNullEmbed =\n      isNullEmbed && hasOnlyNullEmbed (null s) f\ngenerateRelSelectField _ = Nothing\n\ngenerateSpreadSelectFields :: ReadPlan -> [SpreadSelectField]\ngenerateSpreadSelectFields ReadPlan{select, relSelect} =\n  -- We combine the select and relSelect fields into a single list of SpreadSelectField.\n  selectSpread ++ relSelectSpread\n  where\n    selectSpread = map selectToSpread select\n    selectToSpread :: CoercibleSelectField -> SpreadSelectField\n    selectToSpread CoercibleSelectField{csField = CoercibleField{cfName}, csAlias} =\n      SpreadSelectField { ssSelName = fromMaybe cfName csAlias, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }\n\n    relSelectSpread = concatMap relSelectToSpread relSelect\n    relSelectToSpread :: RelSelectField -> [SpreadSelectField]\n    relSelectToSpread (JsonEmbed{rsSelName}) =\n      [SpreadSelectField { ssSelName = rsSelName, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }]\n    relSelectToSpread (Spread{rsSpreadSel}) =\n      rsSpreadSel\n\n-- When aggregates are present in a ReadPlan with a to-one spread, we \"hoist\"\n-- to the highest level possible so that their semantics make sense. For instance,\n-- imagine the user performs the following request:\n-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`\n--\n-- In this case, it is sensible that we would expect to receive the sum of the\n-- `invoice_total`, grouped by the `client_id`. Without hoisting, the sum would\n-- be performed in the sub-query for the joined table `project_invoices`, thus\n-- making it essentially a no-op. With hoisting, we hoist the aggregate function\n-- so that the aggregate function is performed in a more sensible context.\n--\n-- We will try to hoist the aggregate function to the highest possible level,\n-- which means that we hoist until we reach the root node, or until we reach a\n-- ReadPlan that will be embedded a JSON object or JSON array.\n\n-- This type alias represents an aggregate that is to be hoisted to the next\n-- level up. The first tuple of `Alias` and `FieldName` contain the alias for\n-- the joined table and the original field name for the hoisted field.\n--\n-- The second tuple contains the aggregate function to be applied, the cast, and\n-- the alias, if it was supplied by the user or otherwise determined.\n--\n-- No hoisting is done for to-many spreads\ntype HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))\n\nhoistSpreadAggFunctions :: ReadPlanTree -> Either Error ReadPlanTree\nhoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree\n\napplySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])\napplySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relSpread} children) =\n  let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children\n      allChildAggLists = concat childAggLists\n      isToOneSpread = relSpread == Just ToOneSpread\n      (newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not isToOneSpread)\n                                then (select rp, [])\n                                else hoistFromSelectFields relAggAlias (select rp)\n\n      -- If the current `ReadPlan` is a to-one spread rel and it has aggregates hoisted from\n      -- child relationships, then it must hoist those aggregates to its parent rel.\n      -- So we update them with the current `relAggAlias`.\n      hoistAgg ((_, fieldName), hoistFunc) = ((relAggAlias, fieldName), hoistFunc)\n      hoistedAggList = if isToOneSpread\n                       then aggList ++ map hoistAgg allChildAggLists\n                       else aggList\n\n      newRelSelects = if null children || isToOneSpread\n                      then relSelect rp\n                      else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp\n  in  (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, hoistedAggList)\n\n-- Hoist aggregate functions from the select list of a ReadPlan, and return the\n-- updated select list and the list of hoisted aggregates.\nhoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])\nhoistFromSelectFields relAggAlias fields =\n    let (newFields, maybeAggs) = foldr processField ([], []) fields\n    in (newFields, catMaybes maybeAggs)\n  where\n    processField field (newFields, aggList) =\n      let (modifiedField, maybeAgg) = modifyField field\n      in (modifiedField : newFields, maybeAgg : aggList)\n\n    modifyField field@CoercibleSelectField{csAggFunction=Just aggFunc, csField, csAggCast, csAlias} =\n      let determineFieldName = fromMaybe (cfName csField) csAlias\n          updatedField = field {csAggFunction = Nothing, csAggCast = Nothing}\n          hoistedField = Just ((relAggAlias, determineFieldName), (aggFunc, csAggCast, csAlias))\n      in (updatedField, hoistedField)\n    modifyField field = (field, Nothing)\n\n-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,\n-- and any applicable casts or aliases.\nhoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField\nhoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = relAggAlias}) =\n    r { rsSpreadSel = map updateSelect spreadSelects }\n  where\n    updateSelect s =\n        case lookup (relAggAlias, ssSelName s) aggList of\n            Just (aggFunc, aggCast, fldAlias) ->\n                s { ssSelAggFunction = Just aggFunc,\n                    ssSelAggCast     = aggCast,\n                    ssSelAlias       = fldAlias }\n            Nothing -> s\nhoistIntoRelSelectFields _ r = r\n\n-- | Handle ordering in a To-Many Spread Relationship\n-- * It removes the ordering done in the ReadPlan and moves it to the SpreadType.\n--   We also select the ordering columns and alias them to avoid collisions. This is because it would be impossible\n--   to order once it's aggregated if it's not selected in the inner query beforehand.\naddToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree\naddToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)\n  | anyAggSel || anyAggRelSel = Left $ ApiRequestErr $ NotImplemented \"Aggregates are not implemented for one-to-many or many-to-many spreads.\"\n  | otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest\n  where\n    newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}\n    anyAggSel = any (isJust . csAggFunction) select\n    anyAggRelSel = any (\\case Spread sels _ -> any (isJust . ssSelAggFunction) sels; _ -> False) relSelect\n    (addSprExtraSelects, addSprOrder) = unzip $ zipWith ordToExtraSelsAndSprOrds [1..] order\n    ordToExtraSelsAndSprOrds i = \\case\n      CoercibleOrderTerm fld dir ordr -> (\n          (Nothing, CoercibleSelectField fld Nothing Nothing Nothing (Just $ selOrdAlias (cfName fld) i)),\n          CoercibleOrderTerm (unknownField (selOrdAlias (cfName fld) i) []) dir ordr\n        )\n      CoercibleOrderRelationTerm rel (fld,jp) dir ordr -> (\n          (Just rel, CoercibleSelectField (unknownField fld jp) Nothing Nothing Nothing (Just $ selOrdAlias fld i)),\n          CoercibleOrderTerm (unknownField (selOrdAlias fld i) []) dir ordr\n        )\n    selOrdAlias :: Alias -> Integer -> Alias\n    selOrdAlias name i = relAggAlias <> \"_\" <> name <> \"_\" <> show i -- add index to avoid collisions in aliases\naddToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `traverse` forest\n\nvalidateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree\nvalidateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)\n  | not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestErr AggregatesNotAllowed\n  | otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest\n\n-- | Lookup table in the schema cache before creating read plan\nfindTable :: QualifiedIdentifier -> SchemaCache -> Either Error QualifiedIdentifier\nfindTable qi@QualifiedIdentifier{..} sc@SchemaCache{dbTables} =\n  case HM.lookup qi dbTables of\n    Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName sc\n    Just _ -> Right qi\n\naddFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree\naddFilters ctx ApiRequest{..} rReq =\n  foldr addFilterToNode (Right rReq) flts\n  where\n    QueryParams.QueryParams{..} = iQueryParams\n    flts =\n      case iAction of\n        ActDb (ActRelationRead _  _) -> qsFilters\n        ActDb (ActRoutine _ _)       -> qsFilters\n        _                            -> qsFiltersNotRoot\n\n    addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree ->  Either Error ReadPlanTree\n    addFilterToNode =\n      updateNode (\\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf}  f)\n\naddOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree\naddOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder\n  where\n    QueryParams.QueryParams{..} = iQueryParams\n\n    addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree\n    addOrderToNode = updateNode (\\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)\n\nresolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm\nresolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d\nresolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld Nothing) dir nulls\n\n-- Validates that the related resource on the order is an embedded resource,\n-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),\n-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.\naddRelatedOrders :: ReadPlanTree -> Either Error ReadPlanTree\naddRelatedOrders (Node rp@ReadPlan{order,from} forest) = do\n  newOrder <- newRelOrder `traverse` order\n  Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest\n  where\n    newRelOrder cot@CoercibleOrderTerm{}                   = Right cot\n    newRelOrder cot@CoercibleOrderRelationTerm{coRelation} =\n      let foundRP = rootLabel <$> find (\\(Node ReadPlan{relName, relAlias} _) -> coRelation == fromMaybe relName relAlias) forest in\n      case foundRP of\n        Just ReadPlan{relName,relAlias,relAggAlias,relToParent} ->\n          let isToOne = relIsToOne <$> relToParent\n              name    = fromMaybe relName relAlias in\n          if isToOne == Just True\n            then Right $ cot{coRelation=relAggAlias}\n            else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name\n        Nothing ->\n          Left $ ApiRequestErr $ NotEmbedded coRelation\n\n-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`\n--\n-- (It doesn't err but uses an Either ApiRequestError type so it can combine with the other functions that modify the read plan tree)\n--\n-- Setup:\n--\n-- >>> let nullOp    = OpExpr True (Is IsNull)\n-- >>> let nonNullOp = OpExpr False (Is IsNull)\n-- >>> let notEqOp   = OpExpr True (Op OpNotEqual \"val\")\n-- >>> :{\n-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`\n-- let\n-- subForestPlan =\n--   [\n--     Node {\n--       rootLabel = ReadPlan {\n--         select = [], -- there will be fields at this stage but we just omit them for brevity\n--         from = QualifiedIdentifier {qiSchema = \"test\", qiName = \"projects\"},\n--         fromAlias = Just \"projects_1\", where_ = [], order = [], range_ = fullRange,\n--         relName = \"projects\",\n--         relToParent = Nothing,\n--         relJoinConds = [],\n--         relAlias = Nothing, relAggAlias = \"clients_projects_1\", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1,\n--         relSelect = []\n--       },\n--       subForest = []\n--     }\n--   ]\n-- :}\n--\n-- >>> :{\n-- -- this represents the full URL `/clients?select=*,projects(*)&projects=not.is.null`, if subForst takes the above subForestPlan and nullOp\n-- let\n-- readPlanTree op subForst =\n--   Node {\n--     rootLabel = ReadPlan {\n--       select = [], -- there will be fields at this stage but we just omit them for brevity\n--       from = QualifiedIdentifier { qiSchema = \"test\", qiName = \"clients\"},\n--       fromAlias = Nothing,\n--       where_ = [\n--         CoercibleStmnt (\n--           CoercibleFilter {\n--            field = CoercibleField {cfName = \"projects\", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = \"\", cfBaseType = \"\", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},\n--            opExpr = op\n--           }\n--         )\n--       ],\n--       order = [], range_ = fullRange, relName = \"clients\", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = \"\", relHint = Nothing,\n--       relJoinType = Nothing, relSpread = Nothing, depth = 0,\n--       relSelect = []\n--     },\n--     subForest = subForst\n--   }\n-- :}\n--\n-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.\n--\n-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])\n-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = \"projects\", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = \"\", cfBaseType = \"\", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]\n--\n-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.\n--\n-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp subForestPlan)\n-- Right [CoercibleStmnt (CoercibleFilterNullEmbed True \"clients_projects_1\")]\n--\n-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan)\n-- Right [CoercibleStmnt (CoercibleFilterNullEmbed False \"clients_projects_1\")]\naddNullEmbedFilters :: ReadPlanTree -> Either Error ReadPlanTree\naddNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do\n  let forestReadPlans = rootLabel <$> forest\n  newLogic <- newNullFilters forestReadPlans `traverse` curLogic\n  Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)\n  where\n    newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either Error CoercibleLogicTree\n    newNullFilters rPlans = \\case\n      (CoercibleExpr b lOp trees) ->\n        CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)\n      flt@(CoercibleStmnt (CoercibleFilter CoercibleField{cfName=fld, cfJsonPath=[]} opExpr)) ->\n        let foundRP = find (\\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in\n        case (foundRP, opExpr) of\n          (Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias\n          _                                                   -> Right flt\n      flt@(CoercibleStmnt _) ->\n        Right flt\n\naddRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree\naddRanges ApiRequest{..} rReq =\n  case iAction of\n    ActDb (ActRelationMut _ _) -> Right rReq\n    _                          -> foldr addRangeToNode (Right rReq) =<< ranges\n  where\n    ranges :: Either Error [(EmbedPath, NonnegRange)]\n    ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange\n\n    addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree\n    addRangeToNode = updateNode (\\r (Node q f) -> Node q{range_=r} f)\n\naddLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree\naddLogicTrees ctx ApiRequest{..} rReq =\n  foldr addLogicTreeToNode (Right rReq) logic\n  where\n    QueryParams.QueryParams{..} = iQueryParams\n\n    logic =\n      case iAction of\n        ActDb (ActRelationRead _  _) -> qsLogic\n        ActDb (ActRoutine _ _)       -> qsLogic\n        -- For mutations, take the non-root logic filters. These will only affect the embeddings and not the top level of the returned representation.\n        _                            -> filter (not . null . fst) qsLogic\n\n    addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree\n    addLogicTreeToNode = updateNode (\\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)\n\nresolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree\nresolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt\nresolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)\n\nresolveFilter :: ResolverContext -> Filter -> CoercibleFilter\nresolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}\n\n-- Find a Node of the Tree and apply a function to it\nupdateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree\nupdateNode f ([], a) rr = f a <$> rr\nupdateNode _ _ (Left e) = Left e\nupdateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =\n  case findNode of\n    Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName\n    Just target ->\n      (\\node -> Node rootNode $ node : delete target forest) <$>\n      updateNode f (remainingPath, a) (Right target)\n  where\n    findNode :: Maybe ReadPlanTree\n    findNode = find (\\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest\n\nmutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan\nmutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =\n  case mutation of\n    MutationCreate ->\n      mapRight (\\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError\n    MutationUpdate ->\n      mapRight (\\typedColumns -> Update qi typedColumns body combinedLogic returnings applyDefaults) typedColumnsOrError\n    MutationSingleUpsert ->\n        if null qsLogic &&\n           qsFilterFields == S.fromList pkCols &&\n           not (null (S.fromList pkCols)) &&\n           all (\\case\n              Filter _ (OpExpr False (OpQuant OpEqual Nothing _)) -> True\n              _                                                   -> False) qsFiltersRoot\n          then mapRight (\\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError\n        else\n          Left $ ApiRequestErr InvalidFilters\n    MutationDelete -> Right $ Delete qi combinedLogic returnings\n  where\n    ctx = ResolverContext dbTables dbRepresentations qi \"json\"\n    confCols = fromMaybe pkCols qsOnConflict\n    QueryParams.QueryParams{..} = iQueryParams\n    returnings =\n      if preferRepresentation == Just None || isNothing preferRepresentation\n        then []\n        else S.toList $ inferColsEmbedNeeds readReq pkCols\n    -- TODO: remove fromJust by refactoring later\n    -- we can use fromJust, we have already looked up the table before building mutatePlan\n    tbl = fromJust $ HM.lookup qi dbTables\n    pkCols = maybe mempty tablePKCols (Just tbl)\n    logic = map (resolveLogicTree ctx . snd) qsLogic\n    combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot\n    body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)\n    applyDefaults = preferMissing == Just ApplyDefaults\n    typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns\n\nresolveOrError :: ResolverContext -> Table -> FieldName -> Either Error CoercibleField\nresolveOrError ctx table field = case resolveTableFieldName table field Nothing of\n    CoercibleField{cfIRType=\"\"} -> Left $ SchemaCacheErr $ ColumnNotFound (tableName table) field\n    cf                          -> Right $ withJsonParse ctx cf\n\ncallPlan :: Routine -> ApiRequest -> S.Set FieldName -> CallArgs -> ReadPlanTree -> CallPlan\ncallPlan proc ApiRequest{} paramKeys args readReq = FunctionCall {\n  funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)\n, funCParams = callParams\n, funCArgs = args\n, funCScalar = funcReturnsScalar proc\n, funCSetOfScalar = funcReturnsSetOfScalar proc\n, funCFilterFields = getFilterFieldNames readReq\n, funCReturning = inferColsEmbedNeeds readReq []\n}\n  where\n    specifiedParams = filter (\\x -> ppName x `S.member` paramKeys)\n    callParams = case pdParams proc of\n      [prm] | ppName prm == mempty -> OnePosParam prm\n            | otherwise            -> KeyParams $ specifiedParams [prm]\n      prms  -> KeyParams $ specifiedParams prms\n\n-- | Get filter fields/column names from read plan\ngetFilterFieldNames :: ReadPlanTree -> Set FieldName\ngetFilterFieldNames rpt = S.fromList $ foldr (\\rp names -> names <> rpToFieldNames rp) [] rpt\n  where\n    rpToFieldNames :: ReadPlan -> [FieldName]\n    rpToFieldNames = logicTreesToFieldName . ReadPlan.where_\n\n    logicTreesToFieldName :: [CoercibleLogicTree] -> [FieldName]\n    logicTreesToFieldName = concatMap coLogicTreeToFieldNames\n\n    coLogicTreeToFieldNames :: CoercibleLogicTree -> [FieldName]\n    coLogicTreeToFieldNames = \\case\n      CoercibleStmnt (CoercibleFilter{field=CoercibleField{cfName}}) -> [cfName]\n      CoercibleStmnt (CoercibleFilterNullEmbed _ cfName) -> [cfName] -- needs test coverage\n      CoercibleExpr _ _ clts -> concatMap coLogicTreeToFieldNames clts\n\n-- | Infers the columns needed for an embed to be successful after a mutation or a function call.\ninferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> S.Set FieldName\ninferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols\n  -- if * is part of the select, we must not add pk or fk columns manually -\n  -- otherwise those would be selected and output twice\n  | \"*\" `S.member` fldNames = S.singleton \"*\"\n  | otherwise               = returnings\n  where\n    fldNames = S.fromList $ cfName . csField <$> select\n    -- Without fkCols, when a mutatePlan to\n    -- /projects?select=name,clients(name) occurs, the RETURNING SQL part would\n    -- be `RETURNING name`(see QueryBuilder).  This would make the embedding\n    -- fail because the following JOIN would need the \"client_id\" column from\n    -- projects.  So this adds the foreign key columns to ensure the embedding\n    -- succeeds, result would be `RETURNING name, client_id`.\n    fkCols = S.fromList $ concat $ mapMaybe (\\case\n        Node ReadPlan{relToParent=Just Relationship{relCardinality=O2M _ cols}} _ ->\n          Just $ fst <$> cols\n        Node ReadPlan{relToParent=Just Relationship{relCardinality=M2O _ cols}} _ ->\n          Just $ fst <$> cols\n        Node ReadPlan{relToParent=Just Relationship{relCardinality=O2O _ cols _}} _ ->\n          Just $ fst <$> cols\n        Node ReadPlan{relToParent=Just Relationship{relCardinality=M2M Junction{junColsSource=cols}}} _ ->\n          Just $ fst <$> cols\n        Node ReadPlan{relToParent=Just ComputedRelationship{}} _ ->\n          Nothing\n        Node ReadPlan{relToParent=Nothing} _ ->\n          Nothing\n      ) forest\n    hasComputedRel = isJust $ find (\\case\n      Node ReadPlan{relToParent=Just ComputedRelationship{}} _ -> True\n      _                                                    -> False\n      ) forest\n    -- However if the \"client_id\" is present, e.g. mutatePlan to\n    -- /projects?select=client_id,name,clients(name) we would get `RETURNING\n    -- client_id, name, client_id` and then we would produce the \"column\n    -- reference \\\"client_id\\\" is ambiguous\" error from PostgreSQL. So we\n    -- deduplicate with Set: We are adding the primary key columns as well to\n    -- make sure, that a proper location header can always be built for\n    -- INSERT/POST\n    returnings =\n      if not hasComputedRel\n        then fldNames <> fkCols <> S.fromList pkCols\n        else S.singleton \"*\" -- on computed relationships we cannot know the required columns for an embedding to succeed, so we just return all\n\n-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree\n-- they are later concatenated with AND in the QueryBuilder\naddFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]\naddFilterToLogicForest flt lf = CoercibleStmnt flt : lf\n"
  },
  {
    "path": "src/PostgREST/Query/PreQuery.hs",
    "content": "{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\n{-|\nModule      : PostgREST.Query.PreQuery\nDescription : Builds queries that run prior to the main query\n-}\nmodule PostgREST.Query.PreQuery\n  ( txVarQuery\n  , preReqQuery\n  ) where\n\nimport qualified Data.Aeson                      as JSON\nimport qualified Data.Aeson.KeyMap               as KM\nimport qualified Data.ByteString.Lazy.Char8      as LBS\nimport qualified Data.HashMap.Strict             as HM\nimport qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)\n\n\n\nimport PostgREST.ApiRequest              (ApiRequest (..))\nimport PostgREST.ApiRequest.Preferences  (PreferTimezone (..),\n                                          Preferences (..))\nimport PostgREST.Auth.Types              (AuthResult (..))\nimport PostgREST.Config                  (AppConfig (..))\nimport PostgREST.Plan                    (CrudPlan (..),\n                                          DbActionPlan (..))\nimport PostgREST.Query.SqlFragment       (escapeIdentList, fromQi,\n                                          intercalateSnippet,\n                                          setConfigWithConstantName,\n                                          setConfigWithConstantNameJSON,\n                                          setConfigWithDynamicName)\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))\nimport PostgREST.SchemaCache.Routine     (Routine (..))\n\nimport Protolude hiding (Handler)\n\n-- sets transaction variables\ntxVarQuery :: DbActionPlan -> AppConfig -> AuthResult -> ApiRequest -> SQL.Snippet\ntxVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =\n    -- To ensure `GRANT SET ON PARAMETER <superuser_setting> TO authenticator` works, the role settings must be set before the impersonated role.\n    -- Otherwise the GRANT SET would have to be applied to the impersonated role. See https://github.com/PostgREST/postgrest/issues/3045\n    \"select \" <> intercalateSnippet \", \" (\n      searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ funcSettingsSql ++ appSettingsSql\n    )\n  where\n    methodSql = setConfigWithConstantName (\"request.method\", iMethod)\n    pathSql = setConfigWithConstantName (\"request.path\", iPath)\n    headersSql = setConfigWithConstantNameJSON \"request.headers\" iHeaders\n    cookiesSql = setConfigWithConstantNameJSON \"request.cookies\" iCookies\n    claimsSql = [setConfigWithConstantName (\"request.jwt.claims\", LBS.toStrict $ JSON.encode claims)]\n      where\n        claims = authClaims & KM.insert \"role\" (JSON.String $ decodeUtf8 authRole) -- insert \"role\" to claims as well\n\n    roleSql = [setConfigWithConstantName (\"role\", authRole)]\n    roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)\n    appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings\n    timezoneSql = maybe mempty (\\(PreferTimezone tz) -> [setConfigWithConstantName (\"timezone\", tz)]) $ preferTimezone iPreferences\n    funcSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> funcSettings\n    searchPathSql =\n      let schemas = escapeIdentList (iSchema : configDbExtraSearchPath) in\n      setConfigWithConstantName (\"search_path\", schemas)\n    funcSettings = case dbActPlan of\n      DbCrud _ CallReadPlan{crProc} -> pdFuncSettings crProc\n      _                             -> mempty\n\n-- runs the pre-request function\npreReqQuery :: QualifiedIdentifier -> SQL.Snippet\npreReqQuery preRequest = \"select \" <> fromQi preRequest <> \"()\"\n"
  },
  {
    "path": "src/PostgREST/Query/QueryBuilder.hs",
    "content": "{-# LANGUAGE DuplicateRecordFields #-}\n{-# LANGUAGE NamedFieldPuns        #-}\n{-# LANGUAGE RecordWildCards       #-}\n{-|\nModule      : PostgREST.Query.QueryBuilder\nDescription : PostgREST SQL queries generating functions.\n\nThis module provides functions to consume data types that\nrepresent database queries (e.g. ReadPlanTree, MutatePlan) and SqlFragment\nto produce SqlQuery type outputs.\n-}\nmodule PostgREST.Query.QueryBuilder\n  ( readPlanToQuery\n  , mutatePlanToQuery\n  , readPlanToCountQuery\n  , callPlanToQuery\n  , limitedQuery\n  ) where\n\nimport qualified Data.Aeson                      as JSON\nimport qualified Data.ByteString.Char8           as BS\nimport qualified Data.HashMap.Strict             as HM\nimport qualified Data.Set                        as S\nimport qualified Hasql.DynamicStatements.Snippet as SQL\nimport qualified Hasql.Encoders                  as HE\n\nimport Data.Maybe (fromJust)\nimport Data.Tree  (Tree (..))\n\nimport PostgREST.ApiRequest.Preferences   (PreferResolution (..))\nimport PostgREST.SchemaCache.Identifiers  (QualifiedIdentifier (..))\nimport PostgREST.SchemaCache.Relationship (Cardinality (..),\n                                           Junction (..),\n                                           Relationship (..))\nimport PostgREST.SchemaCache.Routine      (RoutineParam (..))\n\nimport PostgREST.ApiRequest.Types\nimport PostgREST.Plan.CallPlan\nimport PostgREST.Plan.MutatePlan\nimport PostgREST.Plan.ReadPlan\nimport PostgREST.Plan.Types\nimport PostgREST.Query.SqlFragment\n\nimport Protolude\n\nreadPlanToQuery :: ReadPlanTree -> SQL.Snippet\nreadPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect, relSpread} forest) =\n  \"SELECT \" <>\n  intercalateSnippet \", \" (selects ++ sprExtraSelects ++ joinsSelects) <>\n  fromFrag <>\n  intercalateSnippet \" \" joins <>\n  (if null logicForest && null relJoinConds\n    then mempty\n    else \" WHERE \" <> intercalateSnippet \" AND \" (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> \" \" <>\n  groupF qi select relSelect <> \" \" <>\n  orderF qi order <> \" \" <>\n  limitOffsetF readRange\n  where\n    fromFrag = fromF relToParent mainQi fromAlias\n    qi = getQualifiedIdentifier relToParent mainQi fromAlias\n    -- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage\n    defSelect = [CoercibleSelectField (unknownField \"*\" []) Nothing Nothing Nothing Nothing]\n    joins = getJoins node\n    selects = pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)\n    joinsSelects = getJoinSelects node\n    sprExtraSelects = case relSpread of\n      Just (ToManySpread sels _) -> (\\s -> pgFmtSelectItem (maybe qi (QualifiedIdentifier \"\") $ fst s) $ snd s) <$> sels\n      _ -> mempty\n\ngetJoinSelects :: ReadPlanTree -> [SQL.Snippet]\ngetJoinSelects (Node ReadPlan{relSelect} _) =\n  join $ map relSelectToSnippet relSelect\n  where\n    relSelectToSnippet :: RelSelectField -> [SQL.Snippet]\n    relSelectToSnippet fld =\n      let aggAlias = pgFmtIdent $ rsAggAlias fld\n      in\n        case fld of\n          JsonEmbed{rsEmptyEmbed = True} ->\n            []\n          JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->\n            [\"row_to_json(\" <> aggAlias <> \".*)::jsonb AS \" <> pgFmtIdent rsSelName]\n          JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->\n            [\"COALESCE( \" <> aggAlias <> \".\" <> aggAlias <> \", '[]') AS \" <> pgFmtIdent rsSelName]\n          Spread{rsSpreadSel, rsAggAlias} ->\n            pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel\n\ngetJoins :: ReadPlanTree -> [SQL.Snippet]\ngetJoins (Node _ []) = []\ngetJoins (Node ReadPlan{relSelect} forest) =\n  map (\\fld ->\n         let alias = rsAggAlias fld\n             matchingNode = fromJust $ find (\\(Node ReadPlan{relAggAlias} _) -> alias == relAggAlias) forest\n         in getJoin fld matchingNode\n      ) relSelect\n\ngetJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet\ngetJoin fld node@(Node ReadPlan{relJoinType, relSpread} _) =\n  let\n    correlatedSubquery sub al cond =\n      \" \" <> (if relJoinType == Just JTInner then \"INNER\" else \"LEFT\") <> \" JOIN LATERAL ( \" <> sub <> \" ) AS \" <> al <> \" ON \" <> cond\n    subquery = readPlanToQuery node\n    aggAlias = pgFmtIdent $ rsAggAlias fld\n    selectSubqAgg = \"SELECT json_agg(\" <> aggAlias <> \")::jsonb AS \" <> aggAlias\n    fromSubqAgg = \" FROM (\" <> subquery <> \" ) AS \" <> aggAlias\n    joinCondition = if relJoinType == Just JTInner then aggAlias <> \" IS NOT NULL\" else \"TRUE\"\n  in\n    case fld of\n      JsonEmbed{rsEmbedMode = JsonObject} ->\n        correlatedSubquery subquery aggAlias \"TRUE\"\n      Spread{rsSpreadSel, rsAggAlias} ->\n        case relSpread of\n          Just (ToManySpread _ sprOrder) ->\n            let selSpread = selectSubqAgg <> (if null rsSpreadSel then mempty else \", \") <> intercalateSnippet \", \" (pgFmtSpreadJoinSelectItem rsAggAlias sprOrder <$> rsSpreadSel)\n            in correlatedSubquery (selSpread <> fromSubqAgg) aggAlias joinCondition\n          _ ->\n            correlatedSubquery subquery aggAlias \"TRUE\"\n      JsonEmbed{rsEmbedMode = JsonArray} ->\n        correlatedSubquery (selectSubqAgg <> fromSubqAgg) aggAlias joinCondition\n\nmutatePlanToQuery :: MutatePlan -> SQL.Snippet\nmutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings _ applyDefaults) =\n  \"INSERT INTO \" <> fromQi mainQi <> (if null iCols then \" \" else \"(\" <> cols <> \") \") <>\n  fromJsonBodyF body iCols True False applyDefaults <>\n  -- Only used for PUT\n  (if null putConditions then mempty else \"WHERE \" <> addConfigPgrstInserted True <> \" AND \" <> intercalateSnippet \" AND \" (pgFmtLogicTree (QualifiedIdentifier mempty \"pgrst_body\") <$> putConditions)) <>\n  (if null putConditions && mergeDups then \"WHERE \" <> addConfigPgrstInserted True else mempty) <>\n  maybe mempty (\\(oncDo, oncCols) ->\n    if null oncCols then\n      mempty\n    else\n      \" ON CONFLICT(\" <> intercalateSnippet \", \" (pgFmtIdent <$> oncCols) <> \") \" <> case oncDo of\n      IgnoreDuplicates ->\n        \"DO NOTHING\"\n      MergeDuplicates  ->\n        if null iCols\n           then \"DO NOTHING\"\n           else \"DO UPDATE SET \" <> intercalateSnippet \", \" ((pgFmtIdent . cfName) <> const \" = EXCLUDED.\" <> (pgFmtIdent . cfName) <$> iCols) <> (if null putConditions && not mergeDups then mempty else \"WHERE \" <> addConfigPgrstInserted False)\n    ) onConflict <> \" \" <>\n    returningF mainQi returnings\n  where\n    cols = intercalateSnippet \", \" $ pgFmtIdent . cfName <$> iCols\n    mergeDups = case onConflict of {Just (MergeDuplicates,_) -> True; _ -> False;}\n\nmutatePlanToQuery (Update mainQi uCols body logicForest returnings applyDefaults)\n  | null uCols =\n    -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax\n    -- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=\n    -- the select has to be based on \"returnings\" to make computed overloaded functions not throw\n    \"SELECT \" <> emptyBodyReturnedColumns <> \" FROM \" <> fromQi mainQi <> \" WHERE false\"\n\n  | otherwise =\n    \"UPDATE \" <> mainTbl <> \" SET \" <> cols <> \" \" <>\n    fromJsonBodyF body uCols False False applyDefaults <>\n    whereLogic <> \" \" <>\n    returningF mainQi returnings\n\n  where\n    whereLogic = if null logicForest then mempty else \" WHERE \" <> intercalateSnippet \" AND \" (pgFmtLogicTree mainQi <$> logicForest)\n    mainTbl = fromQi mainQi\n    emptyBodyReturnedColumns = if null returnings then \"NULL\" else intercalateSnippet \", \" (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)\n    cols = intercalateSnippet \", \" (pgFmtIdent . cfName <> const \" = \" <> pgFmtColumn (QualifiedIdentifier mempty \"pgrst_body\") . cfName <$> uCols)\n\nmutatePlanToQuery (Delete mainQi logicForest returnings) =\n  \"DELETE FROM \" <> fromQi mainQi <> \" \" <>\n  whereLogic <> \" \" <>\n  returningF mainQi returnings\n  where\n    whereLogic = if null logicForest then mempty else \" WHERE \" <> intercalateSnippet \" AND \" (pgFmtLogicTree mainQi <$> logicForest)\n\ncallPlanToQuery :: CallPlan -> SQL.Snippet\ncallPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar filterFields returnings) =\n  \"SELECT \" <> (if returnsScalar || returnsSetOfScalar then \"pgrst_call.pgrst_scalar\" else returnedColumns) <> \" \" <>\n  fromCall\n  where\n    jsonArgs = case arguments of\n      DirectArgs args -> Just $ JSON.encode args\n      JsonArgs json   -> json\n    fromCall = case params of\n      OnePosParam prm -> \"FROM \" <> callIt (singleParameter jsonArgs $ encodeUtf8 $ ppType prm)\n      KeyParams []    -> \"FROM \" <> callIt mempty\n      KeyParams prms  -> case arguments of\n        DirectArgs args -> \"FROM \" <> callIt (fmtArgs prms args)\n        JsonArgs json   -> fromJsonBodyF json ((\\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> \", \" <>\n                         \"LATERAL \" <> callIt (fmtParams prms)\n\n    callIt :: SQL.Snippet -> SQL.Snippet\n    callIt argument | returnsScalar || returnsSetOfScalar = \"(SELECT \" <> fromQi qi <> \"(\" <> argument <> \") pgrst_scalar) pgrst_call\"\n                    | otherwise                           = fromQi qi <> \"(\" <> argument <> \") pgrst_call\"\n\n    fmtParams :: [RoutineParam] -> SQL.Snippet\n    fmtParams prms = intercalateSnippet \", \"\n      ((\\a -> (if ppVar a then \"VARIADIC \" else mempty) <> pgFmtIdent (ppName a) <> \" := pgrst_body.\" <> pgFmtIdent (ppName a)) <$> prms)\n\n    fmtArgs :: [RoutineParam] -> HM.HashMap Text RpcParamValue -> SQL.Snippet\n    fmtArgs prms args = intercalateSnippet \", \" $ fmtArg <$> prms\n      where\n        fmtArg RoutineParam{..} =\n          (if ppVar then \"VARIADIC \" else mempty) <>\n          pgFmtIdent ppName <>\n          \" := \" <>\n          encodeArg (HM.lookup ppName args) <>\n          \"::\" <>\n          SQL.sql (encodeUtf8 ppTypeMaxLength)\n        encodeArg :: Maybe RpcParamValue -> SQL.Snippet\n        encodeArg (Just (Variadic v)) = SQL.encoderAndParam (HE.nonNullable $ HE.foldableArray $ HE.nonNullable HE.text) v\n        encodeArg (Just (Fixed v)) = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 v\n        -- Currently not supported: Calling functions without some of their arguments without DEFAULT.\n        -- We could fallback to providing this NULL value in those cases.\n        encodeArg Nothing = \"NULL\"\n\n    -- the columns here would be the returnings + the columns that would later\n    -- be used by a where clause filter, if they intersect, we remove the duplicates\n    -- and if * is returned then no need to explicitly add filter columns\n    returnedColumns :: SQL.Snippet\n    returnedColumns = case S.toList returnings of\n      []    -> \"*\"\n      [\"*\"] -> pgFmtColumn (QualifiedIdentifier mempty \"pgrst_call\") \"*\"\n      _     -> intercalateSnippet \", \" (pgFmtColumn (QualifiedIdentifier mempty \"pgrst_call\") <$> returnedColumns')\n        where\n          returnedColumns' = S.toList $ returnings <> filterFields\n\n-- | SQL query meant for COUNTing the root node of the Tree.\n-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.\n-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)\n-- inside the FROM target.\n-- If the request contains INNER JOINs, then the COUNT of the root node will change.\n-- For this case, we use a WHERE EXISTS instead of an INNER JOIN on the count query.\n-- See https://github.com/PostgREST/postgrest/issues/2009#issuecomment-977473031\n-- Only for the nodes that have an INNER JOIN linked to the root level.\nreadPlanToCountQuery :: ReadPlanTree -> SQL.Snippet\nreadPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logicForest, relToParent=rel, relJoinConds} forest) =\n  \"SELECT 1 \" <> fromFrag <>\n  (if null logicForest && null relJoinConds && null subQueries\n    then mempty\n    else \" WHERE \" ) <>\n  intercalateSnippet \" AND \" (\n    map (pgFmtLogicTreeCount qi) logicForest ++\n    map pgFmtJoinCondition relJoinConds ++\n    subQueries\n  )\n  where\n    qi = getQualifiedIdentifier rel mainQi tblAlias\n    fromFrag = fromF rel mainQi tblAlias\n    subQueries = foldr existsSubquery [] forest\n    existsSubquery :: ReadPlanTree -> [SQL.Snippet] -> [SQL.Snippet]\n    existsSubquery readReq@(Node ReadPlan{relJoinType=joinType} _) rest =\n      if joinType == Just JTInner\n        then (\"EXISTS (\" <> readPlanToCountQuery readReq <> \" )\"):rest\n        else rest\n    findNullEmbedRel fld = find (\\(Node ReadPlan{relAggAlias} _) -> fld == relAggAlias) forest\n\n    -- https://github.com/PostgREST/postgrest/pull/2930#discussion_r1325293698\n    pgFmtLogicTreeCount :: QualifiedIdentifier -> CoercibleLogicTree -> SQL.Snippet\n    pgFmtLogicTreeCount qiCount (CoercibleExpr hasNot op frst) = SQL.sql notOp <> \" (\" <> intercalateSnippet (opSql op) (pgFmtLogicTreeCount qiCount <$> frst) <> \")\"\n      where\n        notOp =  if hasNot then \"NOT\" else mempty\n        opSql And = \" AND \"\n        opSql Or  = \" OR \"\n    pgFmtLogicTreeCount _ (CoercibleStmnt (CoercibleFilterNullEmbed hasNot fld)) =\n      maybe mempty (\\x -> (if not hasNot then \"NOT \" else mempty) <> \"EXISTS (\" <> readPlanToCountQuery x <> \")\") (findNullEmbedRel fld)\n    pgFmtLogicTreeCount qiCount (CoercibleStmnt flt) = pgFmtFilter qiCount flt\n\nlimitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet\nlimitedQuery query maxRows = query <> SQL.sql (maybe mempty (\\x -> \" LIMIT \" <> BS.pack (show x)) maxRows)\n\n-- TODO refactor so this function is unneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type\ngetQualifiedIdentifier :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> QualifiedIdentifier\ngetQualifiedIdentifier rel mainQi tblAlias = case rel of\n  Just ComputedRelationship{relFunction} -> QualifiedIdentifier mempty $ fromMaybe (qiName relFunction) tblAlias\n  _                                      -> maybe mainQi (QualifiedIdentifier mempty) tblAlias\n\n-- FROM clause plus implicit joins\nfromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet\nfromF rel mainQi tblAlias = \" FROM \" <>\n  (case rel of\n    -- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.\n    -- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386\n    Just ComputedRelationship{relFunction,relTableAlias,relTable} -> fromQi relFunction <> \"(\" <> pgFmtIdent (qiName relTableAlias) <> \"::\" <> fromQi relTable <> \")\"\n    _                                                             -> fromQi mainQi) <>\n  maybe mempty (\\a -> \" AS \" <> pgFmtIdent a) tblAlias <>\n  (case rel of\n    Just Relationship{relCardinality=M2M Junction{junTable=jt}} -> \", \" <> fromQi jt\n    _                                                           -> mempty)\n"
  },
  {
    "path": "src/PostgREST/Query/SqlFragment.hs",
    "content": "{-# LANGUAGE LambdaCase     #-}\n{-# LANGUAGE NamedFieldPuns #-}\n{-# LANGUAGE QuasiQuotes    #-}\n{-|\nModule      : PostgREST.Query.SqlFragment\nDescription : Helper functions for PostgREST.QueryBuilder.\n-}\nmodule PostgREST.Query.SqlFragment\n  ( accessibleFuncs\n  , accessibleTables\n  , addConfigPgrstInserted\n  , countF\n  , currentSettingF\n  , escapeIdent\n  , escapeIdentList\n  , explainF\n  , fromJsonBodyF\n  , fromQi\n  , groupF\n  , handlerF\n  , intercalateSnippet\n  , limitOffsetF\n  , locationF\n  , noLocationF\n  , orderF\n  , pageCountSelectF\n  , pgFmtColumn\n  , pgFmtFilter\n  , pgFmtIdent\n  , pgFmtJoinCondition\n  , pgFmtLogicTree\n  , pgFmtOrderTerm\n  , pgFmtSelectItem\n  , pgFmtSpreadJoinSelectItem\n  , pgFmtSpreadSelectItem\n  , responseHeadersF\n  , responseStatusF\n  , returningF\n  , schemaDescription\n  , setConfigWithConstantName\n  , setConfigWithConstantNameJSON\n  , setConfigWithDynamicName\n  , singleParameter\n  , sourceCTE\n  , sourceCTEName\n  , unknownEncoder\n  ) where\n\nimport qualified Data.Aeson                      as JSON\nimport qualified Data.ByteString.Char8           as BS\nimport qualified Data.ByteString.Lazy            as LBS\nimport qualified Data.HashMap.Strict             as HM\nimport qualified Data.Text                       as T\nimport qualified Data.Text.Encoding              as T\nimport qualified Hasql.DynamicStatements.Snippet as SQL\nimport qualified Hasql.Encoders                  as HE\n\nimport Control.Arrow ((***))\n\nimport Data.Foldable     (foldr1)\nimport NeatInterpolation (trimming)\n\nimport PostgREST.ApiRequest.Types        (AggregateFunction (..),\n                                          Alias, Cast,\n                                          FtsOperator (..),\n                                          IsVal (..),\n                                          JsonOperand (..),\n                                          JsonOperation (..),\n                                          JsonPath,\n                                          LogicOperator (..),\n                                          OpExpr (..),\n                                          OpQuantifier (..),\n                                          Operation (..),\n                                          OrderDirection (..),\n                                          OrderNulls (..),\n                                          QuantOperator (..),\n                                          SimpleOperator (..))\nimport PostgREST.MediaType               (MTVndPlanFormat (..),\n                                          MTVndPlanOption (..))\nimport PostgREST.Plan.ReadPlan           (JoinCondition (..))\nimport PostgREST.Plan.Types              (CoercibleField (..),\n                                          CoercibleFilter (..),\n                                          CoercibleLogicTree (..),\n                                          CoercibleOrderTerm (..),\n                                          CoercibleSelectField (..),\n                                          RelSelectField (..),\n                                          SpreadSelectField (..),\n                                          ToTsVector (..),\n                                          unknownField)\nimport PostgREST.RangeQuery              (NonnegRange, allRange,\n                                          rangeLimit, rangeOffset)\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier (..),\n                                          RelIdentifier (..),\n                                          escapeIdent, trimNullChars)\nimport PostgREST.SchemaCache.Routine     (MediaHandler (..),\n                                          Routine (..),\n                                          funcReturnsScalar,\n                                          funcReturnsSetOfScalar,\n                                          funcReturnsSingle,\n                                          funcReturnsSingleComposite)\n\nimport Protolude hiding (Sum, cast)\n\nsourceCTEName :: Text\nsourceCTEName = \"pgrst_source\"\n\nsourceCTE :: SQL.Snippet\nsourceCTE = \"pgrst_source\"\n\nnoLocationF :: SQL.Snippet\nnoLocationF = \"array[]::text[]\"\n\nsimpleOperator :: SimpleOperator -> SQL.Snippet\nsimpleOperator = \\case\n  OpNotEqual        -> \"<>\"\n  OpContains        -> \"@>\"\n  OpContained       -> \"<@\"\n  OpOverlap         -> \"&&\"\n  OpStrictlyLeft    -> \"<<\"\n  OpStrictlyRight   -> \">>\"\n  OpNotExtendsRight -> \"&<\"\n  OpNotExtendsLeft  -> \"&>\"\n  OpAdjacent        -> \"-|-\"\n\nquantOperator :: QuantOperator -> SQL.Snippet\nquantOperator = \\case\n  OpEqual            -> \"=\"\n  OpGreaterThanEqual -> \">=\"\n  OpGreaterThan      -> \">\"\n  OpLessThanEqual    -> \"<=\"\n  OpLessThan         -> \"<\"\n  OpLike             -> \"like\"\n  OpILike            -> \"ilike\"\n  OpMatch            -> \"~\"\n  OpIMatch           -> \"~*\"\n\nftsOperator :: FtsOperator -> SQL.Snippet\nftsOperator = \\case\n  FilterFts          -> \"@@ to_tsquery\"\n  FilterFtsPlain     -> \"@@ plainto_tsquery\"\n  FilterFtsPhrase    -> \"@@ phraseto_tsquery\"\n  FilterFtsWebsearch -> \"@@ websearch_to_tsquery\"\n\nsingleParameter :: Maybe LBS.ByteString -> ByteString -> SQL.Snippet\nsingleParameter body typ =\n  if typ == \"bytea\"\n    -- TODO: Hasql fails when using HE.unknown with bytea(pg tries to utf8 encode).\n    then SQL.encoderAndParam (HE.nullable HE.bytea) (LBS.toStrict <$> body)\n    else SQL.encoderAndParam (HE.nullable HE.unknown) (LBS.toStrict <$> body) <> \"::\" <> SQL.sql typ\n\n-- Here we build the pg array literal, e.g '{\"Hebdon, John\",\"Other\",\"Another\"}', manually.\n-- This is necessary to pass an \"unknown\" array and let pg infer the type.\n-- There are backslashes here, but since this value is parametrized and is not a string constant\n-- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS\n-- we don't need to use the E'string' form for C-style escapes\n-- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE\npgBuildArrayLiteral :: [Text] -> Text\npgBuildArrayLiteral vals =\n let trimmed = trimNullChars\n     slashed = T.replace \"\\\\\" \"\\\\\\\\\" . trimmed\n     escaped x = \"\\\"\" <> T.replace \"\\\"\" \"\\\\\\\"\" (slashed x) <> \"\\\"\" in\n \"{\" <> T.intercalate \",\" (escaped <$> vals) <> \"}\"\n\n-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833\npgFmtIdent :: Text -> SQL.Snippet\npgFmtIdent x = SQL.sql . encodeUtf8 $ escapeIdent x\n\n-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`\npgFmtLit :: Text -> Text\npgFmtLit x =\n  let trimmed = trimNullChars x\n      escaped = \"'\" <> T.replace \"'\" \"''\" trimmed <> \"'\"\n      slashed = T.replace \"\\\\\" \"\\\\\\\\\" escaped in\n  if \"\\\\\" `T.isInfixOf` escaped\n    then \"E\" <> slashed\n    else slashed\n\n-- |\n-- Format a list of identifiers and separate them by commas.\n--\n-- >>> escapeIdentList [\"schema_1\", \"schema_2\", \"SPECIAL \\\"@/\\\\#~_-\"]\n-- \"\\\"schema_1\\\", \\\"schema_2\\\", \\\"SPECIAL \\\"\\\"@/\\\\#~_-\\\"\"\nescapeIdentList :: [Text] -> ByteString\nescapeIdentList schemas = BS.intercalate \", \" $ encodeUtf8 . escapeIdent <$> schemas\n\nasCsvF :: SQL.Snippet\nasCsvF = asCsvHeaderF <> \" || '\\n' || \" <> asCsvBodyF\n  where\n    asCsvHeaderF =\n      \"(SELECT coalesce(string_agg(a.k, ','), '')\" <>\n      \"  FROM (\" <>\n      \"    SELECT json_object_keys(r)::text as k\" <>\n      \"    FROM ( \" <>\n      \"      SELECT row_to_json(hh) as r from \" <> sourceCTE <> \" as hh limit 1\" <>\n      \"    ) s\" <>\n      \"  ) a\" <>\n      \")\"\n    asCsvBodyF = \"coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\\n'), '')\"\n\naddNullsToSnip :: Bool -> SQL.Snippet -> SQL.Snippet\naddNullsToSnip strip snip =\n  if strip then \"json_strip_nulls(\" <> snip <> \")\" else  snip\n\nasJsonSingleF :: Maybe Routine -> Bool -> SQL.Snippet\nasJsonSingleF rout strip\n  | returnsScalar = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t.pgrst_scalar)->0\"  <> \", 'null')\"\n  | otherwise     = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t)->0\"  <> \", 'null')\"\n  where\n    returnsScalar = maybe False funcReturnsScalar rout\n\nasJsonF :: Maybe Routine -> Bool -> SQL.Snippet\nasJsonF rout strip\n  | returnsSingleComposite    = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t)->0\" <> \", 'null')\"\n  | returnsScalar             = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t.pgrst_scalar)->0\" <> \", 'null')\"\n  | returnsSetOfScalar        = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t.pgrst_scalar)\" <> \", '[]')\"\n  | otherwise                 = \"coalesce(\" <> addNullsToSnip strip \"json_agg(_postgrest_t)\" <> \", '[]')\"\n  where\n    (returnsSingleComposite, returnsScalar, returnsSetOfScalar) = case rout of\n      Just r  -> (funcReturnsSingleComposite r, funcReturnsScalar r, funcReturnsSetOfScalar r)\n      Nothing -> (False, False, False)\n\nasGeoJsonF ::  SQL.Snippet\nasGeoJsonF = \"json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))\"\n\ncustomFuncF :: Maybe Routine -> QualifiedIdentifier -> RelIdentifier -> SQL.Snippet\ncustomFuncF rout funcQi _\n  | (funcReturnsScalar <$> rout) == Just True = fromQi funcQi <> \"(_postgrest_t.pgrst_scalar)\"\ncustomFuncF _ funcQi RelAnyElement            = fromQi funcQi <> \"(_postgrest_t)\"\ncustomFuncF _ funcQi (RelId target)           = fromQi funcQi <> \"(_postgrest_t::\" <> fromQi target <> \")\"\n\nlocationF :: [Text] -> SQL.Snippet\nlocationF pKeys = SQL.sql $ encodeUtf8 [trimming|(\n  WITH data AS (SELECT row_to_json(_) AS row FROM ${sourceCTEName} AS _ LIMIT 1)\n  SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))\n  FROM data CROSS JOIN json_each_text(data.row) AS json_data\n  WHERE json_data.key IN ('${fmtPKeys}')\n)|]\n  where\n    fmtPKeys = T.intercalate \"','\" pKeys\n\nfromQi :: QualifiedIdentifier -> SQL.Snippet\nfromQi t = (if T.null s then mempty else pgFmtIdent s <> \".\") <> pgFmtIdent n\n  where\n    n = qiName t\n    s = qiSchema t\n\npgFmtColumn :: QualifiedIdentifier -> Text -> SQL.Snippet\npgFmtColumn table \"*\" = fromQi table <> \".*\"\npgFmtColumn table c   = fromQi table <> \".\" <> pgFmtIdent c\n\npgFmtCallUnary :: Text -> SQL.Snippet -> SQL.Snippet\npgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> \"(\" <> x <> \")\"\n\npgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet\npgFmtField table cf = case cfToTsVector cf of\n  Just (ToTsVector lang) -> \"to_tsvector(\" <> pgFmtFtsLang lang <> fmtFld <> \")\"\n  _                      -> fmtFld\n  where\n    fmtFld = case cf of\n      CoercibleField{cfFullRow=True}                                          -> pgFmtIdent (qiName table)\n      CoercibleField{cfName=fn, cfJsonPath=[]}                                -> pgFmtColumn table fn\n      CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson  -> \"to_jsonb(\" <> pgFmtColumn table fn <> \")\" <> pgFmtJsonPath jp\n                                                                  | otherwise -> pgFmtColumn table fn <> pgFmtJsonPath jp\n\n-- Select the value of a named element from a table, applying its optional coercion mapping if any.\npgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet\npgFmtTableCoerce table fld@(CoercibleField{cfTransform=(Just formatterProc)}) = pgFmtCallUnary formatterProc (pgFmtField table fld)\npgFmtTableCoerce table f = pgFmtField table f\n\n-- | Like the previous but now we just have a name so no namespace or JSON paths.\npgFmtCoerceNamed :: CoercibleField -> SQL.Snippet\npgFmtCoerceNamed CoercibleField{cfName=fn, cfTransform=(Just formatterProc)} = pgFmtCallUnary formatterProc (pgFmtIdent fn) <> \" AS \" <> pgFmtIdent fn\npgFmtCoerceNamed CoercibleField{cfName=fn} = pgFmtIdent fn\n\npgFmtSelectItem :: QualifiedIdentifier -> CoercibleSelectField -> SQL.Snippet\npgFmtSelectItem table CoercibleSelectField{csField=fld, csAggFunction=agg, csAggCast=aggCast, csCast=cast, csAlias=alias} =\n  pgFmtApplyAggregate agg aggCast (pgFmtApplyCast cast (pgFmtTableCoerce table fld)) <> pgFmtAs alias\n\npgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet\npgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} =\n  pgFmtApplyAggregate ssSelAggFunction ssSelAggCast (pgFmtFullSelName aggAlias ssSelName) <> pgFmtAs ssSelAlias\n\npgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet\npgFmtApplyAggregate Nothing _ snippet = snippet\npgFmtApplyAggregate (Just agg) aggCast snippet =\n  pgFmtApplyCast aggCast aggregatedSnippet\n  where\n    convertAggFunction :: AggregateFunction -> SQL.Snippet\n    -- Convert from e.g. Sum (the data type) to SUM\n    convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show\n    aggregatedSnippet = convertAggFunction agg <> \"(\" <> snippet <> \")\"\n\npgFmtSpreadJoinSelectItem :: Alias -> [CoercibleOrderTerm] -> SpreadSelectField -> SQL.Snippet\npgFmtSpreadJoinSelectItem aggAlias order SpreadSelectField{ssSelName, ssSelAlias} =\n  \"COALESCE(json_agg(\" <> fmtField <> \" \" <> fmtOrder <> \"),'[]')::jsonb\" <> \" AS \" <> fmtAlias\n  where\n    fmtField = pgFmtFullSelName aggAlias ssSelName\n    fmtOrder = orderF (QualifiedIdentifier \"\" aggAlias) order\n    fmtAlias = pgFmtIdent (fromMaybe ssSelName ssSelAlias)\n\npgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet\npgFmtApplyCast Nothing snippet = snippet\n-- Ideally we'd quote the cast with \"pgFmtIdent cast\". However, that would invalidate common casts such as \"int\", \"bigint\", etc.\n-- Try doing: `select 1::\"bigint\"` - it'll err, using \"int8\" will work though. There's some parser magic that pg does that's invalidated when quoting.\n-- Not quoting should be fine, we validate the input on Parsers.\npgFmtApplyCast (Just cast) snippet = \"CAST( \" <> snippet <> \" AS \" <> SQL.sql (encodeUtf8 cast) <> \" )\"\n\npgFmtFullSelName :: Alias -> FieldName -> SQL.Snippet\npgFmtFullSelName aggAlias fieldName = case fieldName of\n  \"*\" -> pgFmtIdent aggAlias <> \".*\"\n  _   -> pgFmtIdent aggAlias <> \".\" <> pgFmtIdent fieldName\n\n-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body\nfromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet\nfromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =\n  selectClause <> fromClause <> defaultsClause <> lateralClause <> \" pgrst_body \"\n  where\n    selectClause = if includeSelect then \"SELECT \" <> namedCols <> \" \" else mempty\n    fromClause = \"FROM (SELECT \" <> jsonPlaceHolder <> \" AS json_data) pgrst_payload, \"\n    defaultsClause\n      | includeDefaults && isJsonObject     = \"LATERAL (SELECT \" <> defsJsonb <> \" || pgrst_payload.json_data AS val) pgrst_json_defs, \"\n      | includeDefaults && not isJsonObject = \"LATERAL (SELECT jsonb_agg(\" <> defsJsonb <> \" || elem) AS val from jsonb_array_elements(pgrst_payload.json_data) elem) pgrst_json_defs, \"\n      | otherwise = mempty\n    lateralClause = \"LATERAL (SELECT \" <> parsedCols <> \" FROM \" <> lateralFieldsSource <> \")\"\n\n    namedCols = intercalateSnippet \", \" $ fromQi  . QualifiedIdentifier \"pgrst_body\" . cfName <$> fields\n    parsedCols = intercalateSnippet \", \" $ pgFmtCoerceNamed <$> fields\n    typedCols = intercalateSnippet \", \" $ pgFmtIdent . cfName <> const \" \" <> SQL.sql . encodeUtf8 . cfIRType <$> fields\n\n    lateralFieldsSource = if null fields then emptyFieldsSource else nonEmptyFieldsSource\n      where\n        limitClause = if includeLimitOne then \"LIMIT 1\" else mempty\n        nonEmptyFieldsSource = jsonToRecordsetF <> \"(\" <> finalBodyF <> \") AS _(\" <> typedCols <> \") \" <> limitClause\n        -- when json keys are empty, e.g. when payload is `{}` or `[{}, {}]`\n        emptyFieldsSource = if isJsonObject\n                              then \"(values(1)) _ \" -- only 1 row for an empty json object '{}'\n                              else jsonArrayElementsF <> \"(\" <> finalBodyF <> \") _ \" -- extract rows of a json array of empty objects `[{}, {}]`\n\n    defsJsonb = SQL.sql $ \"jsonb_build_object(\" <> BS.intercalate \",\" fieldsWDefaults <> \")\"\n    fieldsWDefaults = mapMaybe extractFieldDefault fields\n      where\n        extractFieldDefault CoercibleField{cfName=nam, cfDefault=Just def} = Just $ encodeUtf8 (pgFmtLit nam <> \", \" <> def)\n        extractFieldDefault CoercibleField{cfDefault=Nothing}              = Nothing\n\n    (finalBodyF, jsonArrayElementsF, jsonToRecordsetF) =\n      if includeDefaults\n        then (\"pgrst_json_defs.val\", \"jsonb_array_elements\", if isJsonObject then \"jsonb_to_record\" else \"jsonb_to_recordset\")\n        else (\"pgrst_payload.json_data\", \"json_array_elements\", if isJsonObject then \"json_to_record\" else \"json_to_recordset\")\n\n    jsonPlaceHolder = SQL.encoderAndParam (HE.nullable $ if includeDefaults then HE.jsonbLazyBytes else HE.jsonLazyBytes) body\n    isJsonObject = -- light validation as pg's json_to_record(set) already validates that the body is valid JSON. We just need to know whether the body looks like an object or not.\n      LBS.take 1 (LBS.dropWhile (`elem` insignificantWhitespace) (fromMaybe mempty body)) == \"{\"\n        where\n          insignificantWhitespace = [32,9,10,13] --\" \\t\\n\\r\" [32,9,10,13] https://datatracker.ietf.org/doc/html/rfc8259#section-2\n\npgFmtOrderTerm :: QualifiedIdentifier -> CoercibleOrderTerm -> SQL.Snippet\npgFmtOrderTerm qi ot =\n  fmtOTerm ot <> \" \" <>\n  SQL.sql (BS.unwords [\n    maybe mempty direction $ coDirection ot,\n    maybe mempty nullOrder $ coNullOrder ot])\n  where\n    fmtOTerm = \\case\n      CoercibleOrderTerm{coField=cof}                            -> pgFmtField qi cof\n      CoercibleOrderRelationTerm{coRelation, coRelTerm=(fn, jp)} -> pgFmtField (QualifiedIdentifier mempty coRelation) (unknownField fn jp)\n\n    direction OrderAsc  = \"ASC\"\n    direction OrderDesc = \"DESC\"\n\n    nullOrder OrderNullsFirst = \"NULLS FIRST\"\n    nullOrder OrderNullsLast  = \"NULLS LAST\"\n\n-- | Interpret a literal in the way the planner indicated through the CoercibleField.\npgFmtUnknownLiteralForField :: SQL.Snippet -> CoercibleField -> SQL.Snippet\npgFmtUnknownLiteralForField value CoercibleField{cfTransform=(Just parserProc)} = pgFmtCallUnary parserProc value\n-- But when no transform is requested, we just use the literal as-is.\npgFmtUnknownLiteralForField value _ = value\n\n-- | Array version of the above, used by ANY().\npgFmtArrayLiteralForField :: [Text] -> CoercibleField -> SQL.Snippet\n-- When a transformation is requested, we need to apply the transformation to each element of the array. This could be done by just making a query with `parser(value)` for each value, but may lead to huge query lengths. Imagine `data_representations.color_from_text('...'::text)` for repeated for a hundred values. Instead we use `unnest()` to unpack a standard array literal and then apply the transformation to each element, like a map.\n-- Note the literals will be treated as text since in every case when we use ANY() the parameters are textual (coming from a query string). We want to rely on the `text->domain` parser to do the right thing.\npgFmtArrayLiteralForField values CoercibleField{cfTransform=(Just parserProc)} = SQL.sql \"(SELECT \" <> pgFmtCallUnary parserProc (SQL.sql \"unnest(\" <> unknownLiteral (pgBuildArrayLiteral values) <> \"::text[])\") <> \")\"\n-- When no transformation is requested, we don't need a subquery.\npgFmtArrayLiteralForField values _ = unknownLiteral (pgBuildArrayLiteral values)\n\n\npgFmtFilter :: QualifiedIdentifier -> CoercibleFilter -> SQL.Snippet\npgFmtFilter _ (CoercibleFilterNullEmbed hasNot fld) = pgFmtIdent fld <> \" IS \" <> (if not hasNot then \"NOT \" else mempty) <> \"DISTINCT FROM NULL\"\npgFmtFilter _ (CoercibleFilter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams\npgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> \" \" <> pgFmtField table fld <> case oper of\n   Op op val  -> \" \" <> simpleOperator op <> \" \" <> pgFmtUnknownLiteralForField (unknownLiteral val) fld\n\n   OpQuant op quant val -> \" \" <> quantOperator op <> \" \" <> case op of\n     OpLike  -> fmtQuant quant $ unknownLiteral (T.map star val)\n     OpILike -> fmtQuant quant $ unknownLiteral (T.map star val)\n     _       -> fmtQuant quant $ pgFmtUnknownLiteralForField (unknownLiteral val) fld\n\n   -- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.\n   -- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`\n   -- However that would not accept the TRUE/FALSE/NULL/\"NOT NULL\"/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.\n   -- This is why `IS` operands are whitelisted at the Parsers.hs level\n   Is isVal -> \" IS \" <>\n      case isVal of\n        IsNull       -> \"NULL\"\n        IsNotNull    -> \"NOT NULL\"\n        IsTriTrue    -> \"TRUE\"\n        IsTriFalse   -> \"FALSE\"\n        IsTriUnknown -> \"UNKNOWN\"\n\n   IsDistinctFrom val -> \" IS DISTINCT FROM \" <> unknownLiteral val\n\n   -- We don't use \"IN\", we use \"= ANY\". IN has the following disadvantages:\n   -- + No way to use an empty value on IN: \"col IN ()\" is invalid syntax. With ANY we can do \"= ANY('{}')\"\n   -- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching.\n   In vals -> \" \" <> case vals of\n      [\"\"] -> \"= ANY('{}') \"\n      _    -> \"= ANY (\" <> pgFmtArrayLiteralForField vals fld <> \") \"\n\n   Fts op lang val -> \" \" <> ftsOperator op <> \"(\" <> pgFmtFtsLang lang <> unknownLiteral val <> \") \"\n where\n   notOp = if hasNot then \"NOT\" else mempty\n   star c = if c == '*' then '%' else c\n   fmtQuant q val = case q of\n    Just QuantAny -> \"ANY(\" <> val <> \")\"\n    Just QuantAll -> \"ALL(\" <> val <> \")\"\n    Nothing       -> val\n\npgFmtFtsLang :: Maybe Text -> SQL.Snippet\npgFmtFtsLang = maybe mempty (\\l -> unknownLiteral l <> \", \")\n\npgFmtJoinCondition :: JoinCondition -> SQL.Snippet\npgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =\n  pgFmtColumn qi1 col1 <> \" = \" <> pgFmtColumn qi2 col2\n\npgFmtLogicTree :: QualifiedIdentifier -> CoercibleLogicTree -> SQL.Snippet\npgFmtLogicTree qi (CoercibleExpr hasNot op forest) = SQL.sql notOp <> \" (\" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi <$> forest) <> \")\"\n  where\n    notOp =  if hasNot then \"NOT\" else mempty\n\n    opSql And = \" AND \"\n    opSql Or  = \" OR \"\npgFmtLogicTree qi (CoercibleStmnt flt) = pgFmtFilter qi flt\n\npgFmtJsonPath :: JsonPath -> SQL.Snippet\npgFmtJsonPath = \\case\n  []             -> mempty\n  (JArrow x:xs)  -> \"->\" <> pgFmtJsonOperand x <> pgFmtJsonPath xs\n  (J2Arrow x:xs) -> \"->>\" <> pgFmtJsonOperand x <> pgFmtJsonPath xs\n  where\n    pgFmtJsonOperand (JKey k) = unknownLiteral k\n    pgFmtJsonOperand (JIdx i) = unknownLiteral i <> \"::int\"\n\npgFmtAs :: Maybe Alias -> SQL.Snippet\npgFmtAs Nothing      = mempty\npgFmtAs (Just alias) = \" AS \" <> pgFmtIdent alias\n\ngroupF :: QualifiedIdentifier -> [CoercibleSelectField] -> [RelSelectField] -> SQL.Snippet\ngroupF qi select relSelect\n  | (noSelectsAreAggregated && noRelSelectsAreAggregated) || null groupTerms = mempty\n  | otherwise = \" GROUP BY \" <> intercalateSnippet \", \" groupTerms\n  where\n    noSelectsAreAggregated = null $ [s | s@(CoercibleSelectField { csAggFunction = Just _ }) <- select]\n    noRelSelectsAreAggregated = all (\\case Spread sels _ -> all (isNothing . ssSelAggFunction) sels; _ -> True) relSelect\n    groupTermsFromSelect = mapMaybe (pgFmtGroup qi) select\n    groupTermsFromRelSelect = mapMaybe groupTermFromRelSelectField relSelect\n    groupTerms = groupTermsFromSelect ++ groupTermsFromRelSelect\n\ngroupTermFromRelSelectField :: RelSelectField -> Maybe SQL.Snippet\ngroupTermFromRelSelectField (JsonEmbed { rsSelName }) =\n  Just $ pgFmtIdent rsSelName\ngroupTermFromRelSelectField (Spread { rsSpreadSel, rsAggAlias }) =\n  if null groupTerms\n  then Nothing\n  else\n    Just $ intercalateSnippet \", \" groupTerms\n  where\n    processField :: SpreadSelectField -> Maybe SQL.Snippet\n    processField SpreadSelectField{ssSelAggFunction = Just _} = Nothing\n    processField SpreadSelectField{ssSelName, ssSelAlias} =\n      Just $ pgFmtIdent rsAggAlias <> \".\" <> pgFmtIdent (fromMaybe ssSelName ssSelAlias)\n    groupTerms = mapMaybe processField rsSpreadSel\n\npgFmtGroup :: QualifiedIdentifier -> CoercibleSelectField -> Maybe SQL.Snippet\npgFmtGroup _  CoercibleSelectField{csAggFunction=Just _} = Nothing\npgFmtGroup _  CoercibleSelectField{csAlias=Just alias, csAggFunction=Nothing} = Just $ pgFmtIdent alias\npgFmtGroup qi CoercibleSelectField{csField=fld, csAlias=Nothing, csAggFunction=Nothing} = Just $ pgFmtField qi fld\n\ncountF :: SQL.Snippet -> SQL.Snippet -> Bool -> Maybe Integer -> NonnegRange -> (SQL.Snippet, SQL.Snippet)\ncountF countQuery pageCountSelect shouldCount maxRows range\n  | shouldCount = if isJust maxRows || range /= allRange\n      then ( \", pgrst_source_count AS (\" <> countQuery <> \")\"\n           , \"(SELECT pg_catalog.count(*) FROM pgrst_source_count)\" )\n      -- When there are no db-max-rows and limits/offsets, the total count will be the same as the page count,\n      -- so we use the same page count here to avoid doing a separate aggregated count.\n      else ( mempty, pageCountSelect )\n  | otherwise = ( mempty, \"null::bigint\" )\n\npageCountSelectF :: Maybe Routine -> SQL.Snippet\npageCountSelectF rout =\n  if maybe False funcReturnsSingle rout\n    then \"1\"\n    else \"pg_catalog.count(_postgrest_t)\"\n\nreturningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet\nreturningF qi returnings =\n  if null returnings\n    then \"RETURNING 1\" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified\n    else \"RETURNING \" <> intercalateSnippet \", \" (pgFmtColumn qi <$> returnings)\n\nlimitOffsetF :: NonnegRange -> SQL.Snippet\nlimitOffsetF range =\n  if range == allRange then mempty else \"LIMIT \" <> limit <> \" OFFSET \" <> offset\n  where\n    limit = maybe \"ALL\" (\\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range\n    offset = unknownEncoder (BS.pack . show $ rangeOffset range)\n\nresponseHeadersF :: SQL.Snippet\nresponseHeadersF = currentSettingF \"response.headers\"\n\nresponseStatusF :: SQL.Snippet\nresponseStatusF = currentSettingF \"response.status\"\n\naddConfigPgrstInserted :: Bool -> SQL.Snippet\naddConfigPgrstInserted add =\n  let (symbol, num) =  if add then (\"+\", \"0\") else (\"-\", \"-1\") in\n  \"set_config('pgrst.inserted', (coalesce(\" <> currentSettingF \"pgrst.inserted\" <> \"::int, 0) \" <> symbol <> \" 1)::text, true) <> '\" <> num <> \"'\"\n\ncurrentSettingF :: SQL.Snippet -> SQL.Snippet\ncurrentSettingF setting =\n  -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15\n  \"nullif(current_setting('\" <> setting <> \"', true), '')\"\n\norderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet\norderF _ []    = mempty\norderF qi ordts = \"ORDER BY \" <> intercalateSnippet \", \" (pgFmtOrderTerm qi <$> ordts)\n\n-- Hasql Snippet utilities\nunknownEncoder :: ByteString -> SQL.Snippet\nunknownEncoder = SQL.encoderAndParam (HE.nonNullable HE.unknown)\n\nunknownLiteral :: Text -> SQL.Snippet\nunknownLiteral = unknownEncoder . encodeUtf8\n\nintercalateSnippet :: ByteString -> [SQL.Snippet] -> SQL.Snippet\nintercalateSnippet _ [] = mempty\nintercalateSnippet frag snippets = foldr1 (\\a b -> a <> SQL.sql frag <> b) snippets\n\nexplainF :: MTVndPlanFormat -> [MTVndPlanOption] -> SQL.Snippet -> SQL.Snippet\nexplainF fmt opts snip =\n  \"EXPLAIN (\" <>\n    SQL.sql (BS.intercalate \", \" (fmtPlanFmt fmt : (fmtPlanOpt <$> opts))) <>\n  \") \" <> snip\n  where\n    fmtPlanOpt :: MTVndPlanOption -> BS.ByteString\n    fmtPlanOpt PlanAnalyze  = \"ANALYZE\"\n    fmtPlanOpt PlanVerbose  = \"VERBOSE\"\n    fmtPlanOpt PlanSettings = \"SETTINGS\"\n    fmtPlanOpt PlanBuffers  = \"BUFFERS\"\n    fmtPlanOpt PlanWAL      = \"WAL\"\n\n    fmtPlanFmt PlanText = \"FORMAT TEXT\"\n    fmtPlanFmt PlanJSON = \"FORMAT JSON\"\n\n-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.\nsetConfigLocal :: (SQL.Snippet, ByteString) -> SQL.Snippet\nsetConfigLocal (k, v) =\n  \"set_config(\" <> k <> \", \" <> unknownEncoder v <> \", true)\"\n\n-- | For when the settings are hardcoded and not parameterized\nsetConfigWithConstantName :: (SQL.Snippet, ByteString) -> SQL.Snippet\nsetConfigWithConstantName (k, v) = setConfigLocal (\"'\" <> k <> \"'\", v)\n\n-- | For when the settings need to be parameterized\nsetConfigWithDynamicName :: (ByteString, ByteString) -> SQL.Snippet\nsetConfigWithDynamicName (k, v) =\n  setConfigLocal (unknownEncoder k, v)\n\n-- | Starting from PostgreSQL v14, some characters are not allowed for config names (mostly affecting headers with \"-\").\n-- | A JSON format string is used to avoid this problem. See https://github.com/PostgREST/postgrest/issues/1857\nsetConfigWithConstantNameJSON :: SQL.Snippet -> [(ByteString, ByteString)] -> [SQL.Snippet]\nsetConfigWithConstantNameJSON prefix keyVals = [setConfigWithConstantName (prefix, gucJsonVal keyVals)]\n  where\n    gucJsonVal :: [(ByteString, ByteString)] -> ByteString\n    gucJsonVal = LBS.toStrict . JSON.encode . HM.fromList . arrayByteStringToText\n    arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]\n    arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal\n\nhandlerF :: Maybe Routine -> MediaHandler -> SQL.Snippet\nhandlerF rout = \\case\n  BuiltinAggArrayJsonStrip   -> asJsonF rout True\n  BuiltinAggSingleJson strip -> asJsonSingleF rout strip\n  BuiltinOvAggJson           -> asJsonF rout False\n  BuiltinOvAggGeoJson        -> asGeoJsonF\n  BuiltinOvAggCsv            -> asCsvF\n  CustomFunc funcQi target   -> customFuncF rout funcQi target\n  NoAgg                      -> \"''::text\"\n\nschemaDescription :: Text -> SQL.Snippet\nschemaDescription schema =\n  \"SELECT pg_catalog.obj_description(\" <> encoded <> \"::regnamespace, 'pg_namespace')\"\n  where\n    encoded = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 schema\n\naccessibleTables :: Text -> SQL.Snippet\naccessibleTables schema = SQL.sql (encodeUtf8 [trimming|\n  SELECT\n    n.nspname AS table_schema,\n    c.relname AS table_name\n  FROM pg_class c\n  JOIN pg_namespace n ON n.oid = c.relnamespace\n  WHERE c.relkind IN ('v','r','m','f','p')\n  AND c.relnamespace = |]) <> encodedSchema <> \"::regnamespace \" <> SQL.sql (encodeUtf8 [trimming|\n  AND (\n    pg_has_role(c.relowner, 'USAGE')\n    or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')\n    or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')\n  )\n  AND not c.relispartition\n  ORDER BY table_schema, table_name|])\n  where\n    encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema\n\naccessibleFuncs :: Text -> SQL.Snippet\naccessibleFuncs schema = baseFuncSqlQuery <> \"AND p.pronamespace = \" <> encodedSchema <> \"::regnamespace\"\n  where\n    encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema\n\nbaseFuncSqlQuery :: SQL.Snippet\nbaseFuncSqlQuery = SQL.sql $ encodeUtf8 [trimming|\n  WITH\n  base_types AS (\n    WITH RECURSIVE\n    recurse AS (\n      SELECT\n        oid,\n        typbasetype,\n        typnamespace AS base_namespace,\n        COALESCE(NULLIF(typbasetype, 0), oid) AS base_type\n      FROM pg_type\n      UNION\n      SELECT\n        t.oid,\n        b.typbasetype,\n        b.typnamespace AS base_namespace,\n        COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type\n      FROM recurse t\n      JOIN pg_type b ON t.typbasetype = b.oid\n    )\n    SELECT\n      oid,\n      base_namespace,\n      base_type\n    FROM recurse\n    WHERE typbasetype = 0\n  ),\n  arguments AS (\n    SELECT\n      oid,\n      array_agg((\n        COALESCE(name, ''), -- name\n        type::regtype::text, -- type\n        CASE type\n          WHEN 'bit'::regtype THEN 'bit varying'\n          WHEN 'bit[]'::regtype THEN 'bit varying[]'\n          WHEN 'character'::regtype THEN 'character varying'\n          WHEN 'character[]'::regtype THEN 'character varying[]'\n          ELSE type::regtype::text\n        END, -- convert types that ignore the length and accept any value till maximum size\n        idx <= (pronargs - pronargdefaults), -- is_required\n        COALESCE(mode = 'v', FALSE) -- is_variadic\n      ) ORDER BY idx) AS args,\n      CASE COUNT(*) - COUNT(name) -- number of unnamed arguments\n        WHEN 0 THEN true\n        WHEN 1 THEN (array_agg(type))[1] IN ('bytea'::regtype, 'json'::regtype, 'jsonb'::regtype, 'text'::regtype, 'xml'::regtype)\n        ELSE false\n      END AS callable\n    FROM pg_proc,\n         unnest(proargnames, proargtypes, proargmodes)\n           WITH ORDINALITY AS _ (name, type, mode, idx)\n    WHERE type IS NOT NULL -- only input arguments\n    GROUP BY oid\n  )\n  SELECT\n    pn.nspname AS proc_schema,\n    p.proname AS proc_name,\n    d.description AS proc_description,\n    COALESCE(a.args, '{}') AS args,\n    tn.nspname AS schema,\n    COALESCE(comp.relname, t.typname) AS name,\n    p.proretset AS rettype_is_setof,\n    (t.typtype = 'c'\n     -- if any TABLE, INOUT or OUT arguments present, treat as composite\n     or COALESCE(proargmodes::text[] && '{t,b,o}', false)\n    ) AS rettype_is_composite,\n    bt.oid <> bt.base_type as rettype_is_composite_alias,\n    p.provolatile,\n    p.provariadic > 0 as hasvariadic,\n    'ignored' AS transaction_isolation_level,\n    '{}'::text[] as kvs\n  FROM pg_proc p\n  LEFT JOIN arguments a ON a.oid = p.oid\n  JOIN pg_namespace pn ON pn.oid = p.pronamespace\n  JOIN base_types bt ON bt.oid = p.prorettype\n  JOIN pg_type t ON t.oid = bt.base_type\n  JOIN pg_namespace tn ON tn.oid = t.typnamespace\n  LEFT JOIN pg_class comp ON comp.oid = t.typrelid\n  LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass\n  WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)\n  AND has_function_privilege(p.oid, 'execute')\n  AND prokind = 'f' |]\n"
  },
  {
    "path": "src/PostgREST/Query/Statements.hs",
    "content": "{-# LANGUAGE NamedFieldPuns #-}\n{-|\nModule      : PostgREST.Query.Statements\nDescription : PostgREST main queries\n-}\nmodule PostgREST.Query.Statements\n  ( mainWrite\n  , mainRead\n  , mainCall\n  , postExplain\n  ) where\n\nimport qualified Hasql.DynamicStatements.Snippet as SQL\n\nimport PostgREST.ApiRequest.Preferences\nimport PostgREST.MediaType              (MTVndPlanFormat (..),\n                                         MediaType (..))\nimport PostgREST.Plan.CallPlan\nimport PostgREST.Plan.MutatePlan        as MTPlan\nimport PostgREST.Plan.ReadPlan\nimport PostgREST.Query.QueryBuilder\nimport PostgREST.Query.SqlFragment\nimport PostgREST.RangeQuery             (NonnegRange)\nimport PostgREST.SchemaCache.Routine    (MediaHandler (..), Routine)\n\nimport Protolude\n\nmainWrite :: ReadPlanTree -> MutatePlan -> MediaType -> MediaHandler ->\n             Maybe PreferRepresentation -> Maybe PreferResolution -> SQL.Snippet\nmainWrite rPlan mtplan mt handler rep resolution = mtSnippet mt snippet\n where\n  checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else \"''\"\n  pgrstInsertedF = checkUpsert \"nullif(current_setting('pgrst.inserted', true),'')::int\"\n  snippet =\n    \"WITH \" <> sourceCTE <> \" AS (\" <> mutateQuery <> \") \" <>\n    \"SELECT \" <>\n      \"'' AS total_result_set, \" <>\n      \"pg_catalog.count(_postgrest_t) AS page_total, \" <>\n      locF <> \" AS header, \" <>\n      handlerF Nothing handler <> \" AS body, \" <>\n      responseHeadersF <> \" AS response_headers, \" <>\n      responseStatusF  <> \" AS response_status, \" <>\n      pgrstInsertedF <> \" AS response_inserted \" <>\n    \"FROM (\" <> selectF <> \") _postgrest_t\"\n\n  locF =\n    if isInsert && rep == Just HeadersOnly\n      then\n        \"CASE WHEN pg_catalog.count(_postgrest_t) = 1 \" <>\n          \"THEN coalesce(\" <> locationF pkCols <> \", \" <> noLocationF <> \") \" <>\n          \"ELSE \" <> noLocationF <> \" \" <>\n        \"END\"\n      else noLocationF\n\n  selectF\n    -- prevent using any of the column names in ?select= when no response is returned from the CTE\n    | handler == NoAgg = \"SELECT * FROM \" <> sourceCTE\n    | otherwise        = selectQuery\n\n  selectQuery = readPlanToQuery rPlan\n  mutateQuery = mutatePlanToQuery mtplan\n  (isPut, isInsert, pkCols) = case mtplan of\n    MTPlan.Insert{MTPlan.where_,insPkCols} -> ((not . null) where_, True, insPkCols)\n    _ -> (False,False, mempty);\n\nmainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->\n            NonnegRange -> MediaType -> MediaHandler -> SQL.Snippet\nmainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet\n where\n  snippet =\n    \"WITH \" <> sourceCTE <> \" AS ( \" <> selectQuery <> \" ) \" <>\n    countCTEF <> \" \" <>\n    \"SELECT \" <>\n      countResultF <> \" AS total_result_set, \" <>\n      pageCountSelect <> \" AS page_total, \" <>\n      handlerF Nothing handler <> \" AS body, \" <>\n      responseHeadersF <> \" AS response_headers, \" <>\n      responseStatusF <> \" AS response_status, \" <>\n      \"''\" <> \" AS response_inserted \" <>\n    \"FROM ( SELECT * FROM \" <> sourceCTE <> \" ) _postgrest_t\"\n\n  (countCTEF, countResultF) = countF countQ pageCountSelect (shouldCount pCount) maxRows range\n  selectQuery = readPlanToQuery rPlan\n  pageCountSelect = pageCountSelectF Nothing\n  countQ =\n    if pCount == Just EstimatedCount then\n      -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed\n      limitedQuery countQuery ((+ 1) <$> maxRows)\n    else\n      countQuery\n\nmainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount -> Maybe Integer ->\n            NonnegRange-> MediaType -> MediaHandler -> SQL.Snippet\nmainCall rout cPlan rPlan pCount maxRows range mt handler = mtSnippet mt snippet\n  where\n    snippet =\n      \"WITH \" <> sourceCTE <> \" AS (\" <> callProcQuery <> \") \" <>\n      countCTEF <>\n      \"SELECT \" <>\n        countResultF <> \" AS total_result_set, \" <>\n        pageCountSelect <> \" AS page_total, \" <>\n        handlerF (Just rout) handler <> \" AS body, \" <>\n        responseHeadersF <> \" AS response_headers, \" <>\n        responseStatusF <> \" AS response_status, \" <>\n        \"''\" <> \" AS response_inserted \" <>\n      \"FROM (\" <> selectQuery <> \") _postgrest_t\"\n\n    (countCTEF, countResultF) = countF countQuery pageCountSelect (shouldCount pCount) maxRows range\n    selectQuery = readPlanToQuery rPlan\n    callProcQuery = callPlanToQuery cPlan\n    countQuery = readPlanToCountQuery rPlan\n    pageCountSelect = pageCountSelectF (Just rout)\n\n-- This occurs after the main query runs, that's why it's prefixed with \"post\"\npostExplain :: SQL.Snippet -> SQL.Snippet\npostExplain = explainF PlanJSON mempty\n\nmtSnippet :: MediaType -> SQL.Snippet -> SQL.Snippet\nmtSnippet mediaType snippet = case mediaType of\n  MTVndPlan _ fmt opts -> explainF fmt opts snippet\n  _                    -> snippet\n"
  },
  {
    "path": "src/PostgREST/Query.hs",
    "content": "{-# LANGUAGE RecordWildCards #-}\n{-|\nModule      : PostgREST.Query\nDescription : PostgREST query building\n\nTODO: This module shouldn't depend on SchemaCache: once OpenAPI is removed, this can be done\n-}\nmodule PostgREST.Query\n  ( mainQuery\n  , MainQuery (..)\n  ) where\n\nimport qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)\n\nimport qualified PostgREST.Query.PreQuery     as PreQuery\nimport qualified PostgREST.Query.QueryBuilder as QueryBuilder\nimport qualified PostgREST.Query.SqlFragment  as SqlFragment\nimport qualified PostgREST.Query.Statements   as Statements\n\n\nimport PostgREST.ApiRequest              (ApiRequest (..))\nimport PostgREST.ApiRequest.Preferences  (Preferences (..),\n                                          shouldExplainCount)\nimport PostgREST.Auth.Types              (AuthResult (..))\nimport PostgREST.Config                  (AppConfig (..))\nimport PostgREST.Plan                    (ActionPlan (..),\n                                          CrudPlan (..),\n                                          DbActionPlan (..),\n                                          InspectPlan (..))\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))\n\nimport Protolude hiding (Handler)\n\n-- The Queries that run on every request\ndata MainQuery = MainQuery\n  { mqTxVars  :: SQL.Snippet       -- ^ the transaction variables that always run on each query\n  , mqPreReq  :: Maybe SQL.Snippet -- ^ the pre-request function that runs if enabled\n  -- TODO only one of the following queries actually runs on each request, once OpenAPI is removed from core it will be easier to refactor this\n  , mqMain    :: SQL.Snippet\n  , mqOpenAPI :: (SQL.Snippet, SQL.Snippet, SQL.Snippet)\n  , mqExplain :: Maybe SQL.Snippet     -- ^ the explain query that gets generated for the \"Prefer: count=estimated\" case\n  }\n\nmainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery\nmainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty\nmainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq =\n  let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in\n  case plan of\n    DbCrud _ WrappedReadPlan{..} ->\n      let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan in\n      genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows range pMedia wrHandler) (mempty, mempty, mempty)\n      (if shouldExplainCount preferCount then Just (Statements.postExplain countQuery) else Nothing)\n    DbCrud _ MutateReadPlan{..} ->\n      genQ (Statements.mainWrite mrReadPlan mrMutatePlan pMedia mrHandler preferRepresentation preferResolution) (mempty, mempty, mempty) mempty\n    DbCrud _ CallReadPlan{..} ->\n      genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty\n    MayUseDb InspectPlan{ipSchema=tSchema} ->\n      genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty\n"
  },
  {
    "path": "src/PostgREST/RangeQuery.hs",
    "content": "{-|\nModule      : PostgREST.RangeQuery\nDescription : Logic regarding the `Range`/`Content-Range` headers and `limit`/`offset` querystring arguments.\n-}\nmodule PostgREST.RangeQuery (\n  rangeParse\n, rangeRequested\n, rangeLimit\n, rangeOffset\n, restrictRange\n, rangeGeq\n, allRange\n, limitZeroRange\n, hasLimitZero\n, convertToLimitZeroRange\n, NonnegRange\n, rangeStatusHeader\n, contentRangeH\n) where\n\nimport qualified Data.ByteString.Char8 as BS\n\nimport Data.List       (lookup)\nimport Text.Regex.TDFA ((=~))\n\nimport Control.Applicative\nimport Data.Ranged.Boundaries\nimport Data.Ranged.Ranges\nimport Network.HTTP.Types.Header\nimport Network.HTTP.Types.Status\n\nimport Protolude\n\ntype NonnegRange = Range Integer\n\nrangeParse :: BS.ByteString -> NonnegRange\nrangeParse range = do\n  let rangeRegex = \"^([0-9]+)-([0-9]*)$\" :: BS.ByteString\n\n  case range =~ rangeRegex :: [[BS.ByteString]] of\n    [[_, l, u]] ->\n      let lower  = maybe emptyRange rangeGeq (readInteger l)\n          upper  = maybe allRange rangeLeq (readInteger u) in\n      rangeIntersection lower upper\n    _ -> allRange\n  where\n    readInteger = readMaybe . BS.unpack\n\nrangeRequested :: RequestHeaders -> NonnegRange\nrangeRequested headers = maybe allRange rangeParse $ lookup hRange headers\n\nrestrictRange :: Maybe Integer -> NonnegRange -> NonnegRange\nrestrictRange Nothing r = r\nrestrictRange (Just limit) r =\n   rangeIntersection r $\n     Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)\n\nrangeLimit :: NonnegRange -> Maybe Integer\nrangeLimit range =\n  case [rangeLower range, rangeUpper range] of\n    [BoundaryBelow lower, BoundaryAbove upper] -> Just (1 + upper - lower)\n    _ -> Nothing\n\nrangeOffset :: NonnegRange -> Integer\nrangeOffset range =\n  case rangeLower range of\n    BoundaryBelow lower -> lower\n    _                   -> panic \"range without lower bound\" -- should never happen\n\nrangeGeq :: Integer -> NonnegRange\nrangeGeq n =\n  Range (BoundaryBelow n) BoundaryAboveAll\n\nallRange :: NonnegRange\nallRange = rangeGeq 0\n\nrangeLeq :: Integer -> NonnegRange\nrangeLeq n =\n  Range BoundaryBelowAll (BoundaryAbove n)\n\n-- Special case to allow limit 0 queries\n-- https://github.com/PostgREST/postgrest/issues/1121\n-- 0 <= x <= -1\nlimitZeroRange :: Range Integer\nlimitZeroRange = Range (BoundaryBelow 0) (BoundaryAbove (-1))\n\nhasLimitZero :: Range Integer -> Bool\nhasLimitZero r = rangeUpper r == rangeUpper limitZeroRange\n\n-- Used to convert a range into a special limitZeroRange if it has a\n-- limit=0 in order to bypass validations for empty ranges.\nconvertToLimitZeroRange :: Range Integer -> Range Integer -> Range Integer\nconvertToLimitZeroRange range fallbackRange =\n  if hasLimitZero range then limitZeroRange else fallbackRange\n\nrangeStatusHeader :: NonnegRange -> Int64 -> Maybe Int64 -> (Status, Header)\nrangeStatusHeader topLevelRange queryTotal tableTotal =\n  let lower = rangeOffset topLevelRange\n      upper = lower + toInteger queryTotal - 1\n      contentRange = contentRangeH lower upper (toInteger <$> tableTotal)\n      status = rangeStatus lower upper (toInteger <$> tableTotal)\n  in (status, contentRange)\n  where\n    rangeStatus :: Integer -> Integer -> Maybe Integer -> Status\n    rangeStatus _ _ Nothing = status200\n    rangeStatus lower upper (Just total)\n      | lower > total               = status416 -- 416 Range Not Satisfiable\n      | (1 + upper - lower) < total = status206 -- 206 Partial Content\n      | otherwise                   = status200 -- 200 OK\n\ncontentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header\ncontentRangeH lower upper total =\n    (\"Content-Range\", toUtf8 headerValue)\n    where\n      headerValue   = rangeString <> \"/\" <> totalString :: Text\n      rangeString\n        | totalNotZero && fromInRange = show lower <> \"-\" <> show upper\n        | otherwise = \"*\"\n      totalString   = maybe \"*\" show total\n      totalNotZero  = Just 0 /= total\n      fromInRange   = lower <= upper\n"
  },
  {
    "path": "src/PostgREST/Response/GucHeader.hs",
    "content": "module PostgREST.Response.GucHeader\n  ( GucHeader\n  , unwrapGucHeader\n  ) where\n\nimport qualified Data.Aeson           as JSON\nimport qualified Data.Aeson.Key       as K\nimport qualified Data.Aeson.KeyMap    as KM\nimport qualified Data.CaseInsensitive as CI\n\nimport Network.HTTP.Types.Header (Header)\n\nimport Protolude\n\n\n{-|\n  Custom guc header, it's obtained by parsing the json in a:\n  `SET LOCAL \"response.headers\" = '[{\"Set-Cookie\": \"..\"}]'\n-}\nnewtype GucHeader = GucHeader (CI.CI ByteString, ByteString)\n\ninstance JSON.FromJSON GucHeader where\n  parseJSON (JSON.Object o) =\n    case KM.toList o of\n      [(k, JSON.String s)] -> pure $ GucHeader (CI.mk $ toUtf8 $ K.toText k, toUtf8 s)\n      _ -> mzero\n  parseJSON _ = mzero\n\nunwrapGucHeader :: GucHeader -> Header\nunwrapGucHeader (GucHeader (k, v)) = (k, v)\n"
  },
  {
    "path": "src/PostgREST/Response/OpenAPI.hs",
    "content": "{-|\nModule      : PostgREST.OpenAPI\nDescription : Generates the OpenAPI output\n-}\n{-# LANGUAGE LambdaCase      #-}\n{-# LANGUAGE RecordWildCards #-}\nmodule PostgREST.Response.OpenAPI (encode) where\n\nimport qualified Data.Aeson            as JSON\nimport qualified Data.ByteString.Char8 as BS\nimport qualified Data.ByteString.Lazy  as LBS\nimport qualified Data.HashMap.Strict   as HM\nimport qualified Data.HashSet.InsOrd   as Set\nimport qualified Data.Text             as T\n\nimport Control.Arrow              ((&&&))\nimport Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)\nimport Data.Maybe                 (fromJust)\nimport Data.String                (IsString (..))\nimport Network.URI                (URI (..), URIAuth (..))\n\nimport Control.Lens (at, (.~), (?~))\n\nimport Data.Swagger\n\nimport PostgREST.Config                   (AppConfig (..), Proxy (..),\n                                           isMalformedProxyUri, toURI)\nimport PostgREST.MediaType\nimport PostgREST.Network                  (escapeHostName)\nimport PostgREST.SchemaCache              (SchemaCache (..))\nimport PostgREST.SchemaCache.Identifiers  (QualifiedIdentifier (..))\nimport PostgREST.SchemaCache.Relationship (Cardinality (..),\n                                           Relationship (..),\n                                           RelationshipsMap)\nimport PostgREST.SchemaCache.Routine      (FuncVolatility (..),\n                                           Routine (..),\n                                           RoutineParam (..))\nimport PostgREST.SchemaCache.Table        (Column (..), Table (..),\n                                           TablesMap,\n                                           tableColumnsList)\n\nimport Protolude hiding (Proxy, get)\n\nencode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString\nencode versions conf sCache tables procs schemaDescription =\n  JSON.encode $\n    postgrestSpec\n      versions\n      (dbRelationships sCache)\n      (concat $ HM.elems procs)\n      (snd <$> HM.toList tables)\n      (proxyUri conf)\n      schemaDescription\n      (configOpenApiSecurityActive conf)\n\nmakeMimeList :: [MediaType] -> MimeList\nmakeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs\n\ntoSwaggerType :: Text -> Maybe (SwaggerType t)\ntoSwaggerType \"character varying\" = Just SwaggerString\ntoSwaggerType \"character\"         = Just SwaggerString\ntoSwaggerType \"text\"              = Just SwaggerString\ntoSwaggerType \"boolean\"           = Just SwaggerBoolean\ntoSwaggerType \"smallint\"          = Just SwaggerInteger\ntoSwaggerType \"integer\"           = Just SwaggerInteger\ntoSwaggerType \"bigint\"            = Just SwaggerInteger\ntoSwaggerType \"numeric\"           = Just SwaggerNumber\ntoSwaggerType \"real\"              = Just SwaggerNumber\ntoSwaggerType \"double precision\"  = Just SwaggerNumber\ntoSwaggerType \"json\"              = Nothing\ntoSwaggerType \"jsonb\"             = Nothing\ntoSwaggerType colType             = case T.takeEnd 2 colType of\n  \"[]\" -> Just SwaggerArray\n  _    -> Just SwaggerString\n\ntypeFromArray :: Text -> Text\ntypeFromArray = T.dropEnd 2\n\ntoSwaggerTypeFromArray :: Text -> Maybe (SwaggerType t)\ntoSwaggerTypeFromArray arrType = toSwaggerType $ typeFromArray arrType\n\nmakePropertyItems :: Text -> Maybe (Referenced Schema)\nmakePropertyItems arrType = case toSwaggerType arrType of\n  Just SwaggerArray -> Just $ Inline (mempty & type_ .~ toSwaggerTypeFromArray arrType)\n  _                 -> Nothing\n\nparseDefault :: Text -> Text -> Text\nparseDefault colType colDefault =\n  case toSwaggerType colType of\n    Just SwaggerString -> wrapInQuotations $ case T.stripSuffix (\"::\" <> colType) colDefault of\n      Just def -> T.dropAround (=='\\'')  def\n      Nothing  -> colDefault\n    _ -> colDefault\n  where\n    wrapInQuotations text = \"\\\"\" <> text <> \"\\\"\"\n\nmakeTableDef :: RelationshipsMap -> Table -> (Text, Schema)\nmakeTableDef rels t =\n  let tn = tableName t in\n      (tn, (mempty :: Schema)\n        & description .~ tableDescription t\n        & type_ ?~ SwaggerObject\n        & properties .~ fromList (makeProperty t rels <$> tableColumnsList t)\n        & required .~ fmap colName (filter (not . colNullable) $ tableColumnsList t))\n\nmakeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema)\nmakeProperty tbl rels col = (colName col, Inline s)\n  where\n    e = if null $ colEnum col then Nothing else JSON.decode $ JSON.encode $ colEnum col\n    fk :: Maybe Text\n    fk =\n      let\n        searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels\n        -- Sorts the relationship list to get tables first\n        relsSortedByIsView = sortOn relFTableIsView [ r | r@Relationship{} <- searchedRels]\n        -- Finds the relationship that has a single column foreign key\n        rel = find (\\case\n          Relationship{relCardinality=(M2O _ relColumns)}       -> [colName col] == (fst <$> relColumns)\n          Relationship{relCardinality=(O2O _ relColumns False)} -> [colName col] == (fst <$> relColumns)\n          _                                                     -> False\n          ) relsSortedByIsView\n        fCol = (headMay . (\\r -> snd <$> relColumns (relCardinality r)) =<< rel)\n        fTbl = qiName . relForeignTable <$> rel\n        fTblCol = (,) <$> fTbl <*> fCol\n      in\n        (\\(a, b) -> T.intercalate \"\" [\"This is a Foreign Key to `\", a, \".\", b, \"`.<fk table='\", a, \"' column='\", b, \"'/>\"]) <$> fTblCol\n    pk :: Bool\n    pk = colName col `elem` tablePKCols tbl\n    n = catMaybes\n      [ Just \"Note:\"\n      , if pk then Just \"This is a Primary Key.<pk/>\" else Nothing\n      , fk\n      ]\n    d =\n      if length n > 1 then\n        Just $ T.append (maybe \"\" (`T.append` \"\\n\\n\") $ colDescription col) (T.intercalate \"\\n\" n)\n      else\n        colDescription col\n    s =\n      (mempty :: Schema)\n        & default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)\n        & description .~ d\n        & enum_ .~ e\n        & format ?~ colType col\n        & maxLength .~ (fromIntegral <$> colMaxLen col)\n        & type_ .~ toSwaggerType (colType col)\n        & items .~ (SwaggerItemsObject <$> makePropertyItems (colType col))\n\nmakeProcSchema :: Routine -> Schema\nmakeProcSchema pd =\n  (mempty :: Schema)\n  & description .~ pdDescription pd\n  & type_ ?~ SwaggerObject\n  & properties .~ fromList (fmap makeProcProperty (pdParams pd))\n  & required .~ fmap ppName (filter ppReq (pdParams pd))\n\nmakeProcProperty :: RoutineParam -> (Text, Referenced Schema)\nmakeProcProperty (RoutineParam n t _ _ _) = (n, Inline s)\n  where\n    s = (mempty :: Schema)\n          & type_ .~ toSwaggerType t\n          & items .~ (SwaggerItemsObject <$> makePropertyItems t)\n          & format ?~ t\n\nmakePreferParam :: [Text] -> Param\nmakePreferParam ts =\n  (mempty :: Param)\n  & name        .~ \"Prefer\"\n  & description ?~ \"Preference\"\n  & required    ?~ False\n  & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n    & in_ .~ ParamHeader\n    & type_ ?~ SwaggerString\n    & enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))\n  where\n    enu = foldl (<>) [] (val <$> ts)\n    val :: Text -> [Text]\n    val = \\case\n      \"count\"      -> [\"count=none\"]\n      \"return\"     -> [\"return=representation\", \"return=minimal\", \"return=none\"]\n      \"resolution\" -> [\"resolution=ignore-duplicates\", \"resolution=merge-duplicates\"]\n      _            -> []\n\nmakeProcGetParam :: RoutineParam -> Referenced Param\nmakeProcGetParam (RoutineParam n t _ r v) =\n  Inline $ (mempty :: Param)\n    & name .~ n\n    & required ?~ r\n    & schema .~ ParamOther fullSchema\n  where\n    fullSchema = if v then schemaMulti else schemaNotMulti\n    baseSchema = (mempty :: ParamOtherSchema)\n      & in_ .~ ParamQuery\n    schemaNotMulti = baseSchema\n      & format ?~ t\n      & type_ ?~ toParamType (toSwaggerType t)\n    schemaMulti = baseSchema\n      & type_ ?~ fromMaybe SwaggerString (toSwaggerType t)\n      & items ?~ SwaggerItemsPrimitive (Just CollectionMulti)\n        ((mempty :: ParamSchema x)\n          & type_ .~ toSwaggerTypeFromArray t\n          & format ?~ typeFromArray t)\n    toParamType paramType = case paramType of\n      -- Array uses {} in query params\n      Just SwaggerArray -> SwaggerString\n      -- Type must be specified in query params\n      Nothing           -> SwaggerString\n      _                 -> fromJust paramType\n\nmakeProcGetParams :: [RoutineParam] -> [Referenced Param]\nmakeProcGetParams = fmap makeProcGetParam\n\nmakeProcPostParams :: Routine -> [Referenced Param]\nmakeProcPostParams pd =\n  [ Inline $ (mempty :: Param)\n    & name     .~ \"args\"\n    & required ?~ True\n    & schema   .~ ParamBody (Inline $ makeProcSchema pd)\n  , Ref $ Reference \"preferParams\"\n  ]\n\nmakeParamDefs :: [Table] -> [(Text, Param)]\nmakeParamDefs ti =\n  -- TODO: create Prefer for each method (GET, PATCH, etc.)\n  [ (\"preferParams\", makePreferParam [\"params\"])\n  , (\"preferReturn\", makePreferParam [\"return\"])\n  , (\"preferCount\", makePreferParam [\"count\"])\n  , (\"preferPost\", makePreferParam [\"return\", \"resolution\"])\n  , (\"select\", (mempty :: Param)\n      & name        .~ \"select\"\n      & description ?~ \"Filtering Columns\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamQuery\n        & type_ ?~ SwaggerString))\n  , (\"on_conflict\", (mempty :: Param)\n      & name        .~ \"on_conflict\"\n      & description ?~ \"On Conflict\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamQuery\n        & type_ ?~ SwaggerString))\n  , (\"order\", (mempty :: Param)\n      & name        .~ \"order\"\n      & description ?~ \"Ordering\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamQuery\n        & type_ ?~ SwaggerString))\n  , (\"range\", (mempty :: Param)\n      & name        .~ \"Range\"\n      & description ?~ \"Limiting and Pagination\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamHeader\n        & type_ ?~ SwaggerString))\n  , (\"rangeUnit\", (mempty :: Param)\n      & name        .~ \"Range-Unit\"\n      & description ?~ \"Limiting and Pagination\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamHeader\n        & type_ ?~ SwaggerString\n        & default_ .~ JSON.decode \"\\\"items\\\"\"))\n  , (\"offset\", (mempty :: Param)\n      & name        .~ \"offset\"\n      & description ?~ \"Limiting and Pagination\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamQuery\n        & type_ ?~ SwaggerString))\n  , (\"limit\", (mempty :: Param)\n      & name        .~ \"limit\"\n      & description ?~ \"Limiting and Pagination\"\n      & required    ?~ False\n      & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n        & in_ .~ ParamQuery\n        & type_ ?~ SwaggerString))\n  ]\n  <> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumnsList t)\n            | t <- ti\n            ]\n\nmakeObjectBody :: Text -> (Text, Param)\nmakeObjectBody tn =\n  (\"body.\" <> tn, (mempty :: Param)\n     & name .~ tn\n     & description ?~ tn\n     & required ?~ False\n     & schema .~ ParamBody (Ref (Reference tn)))\n\nmakeRowFilter :: Text -> Column -> (Text, Param)\nmakeRowFilter tn c =\n  (T.intercalate \".\" [\"rowFilter\", tn, colName c], (mempty :: Param)\n    & name .~ colName c\n    & description .~ colDescription c\n    & required ?~ False\n    & schema .~ ParamOther ((mempty :: ParamOtherSchema)\n      & in_ .~ ParamQuery\n      & type_ ?~ SwaggerString))\n\nmakeRowFilters :: Text -> [Column] -> [(Text, Param)]\nmakeRowFilters tn = fmap (makeRowFilter tn)\n\nmakePathItem :: Table -> (FilePath, PathItem)\nmakePathItem t = (\"/\" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)\n  where\n    -- Use first line of table description as summary; rest as description (if present)\n    -- We strip leading newlines from description so that users can include a blank line between summary and description\n    (tSum, tDesc) = fmap fst &&& fmap (T.dropWhile (=='\\n') . snd) $\n                    T.breakOn \"\\n\" <$> tableDescription t\n    tOp = (mempty :: Operation)\n      & tags .~ Set.fromList [tn]\n      & summary .~ tSum\n      & description .~ mfilter (/=\"\") tDesc\n    getOp = tOp\n      & parameters .~ fmap ref (rs <> [\"select\", \"order\", \"range\", \"rangeUnit\", \"offset\", \"limit\", \"preferCount\"])\n      & at 206 ?~ \"Partial Content\"\n      & at 200 ?~ Inline ((mempty :: Response)\n        & description .~ \"OK\"\n        & schema ?~ Inline (mempty\n          & type_ ?~ SwaggerArray\n          & items ?~ SwaggerItemsObject (Ref $ Reference $ tableName t)\n        )\n      )\n    postOp = tOp\n      & parameters .~ fmap ref [\"body.\" <> tn, \"select\", \"preferPost\"]\n      & at 201 ?~ \"Created\"\n    patchOp = tOp\n      & parameters .~ fmap ref (rs <> [\"body.\" <> tn, \"preferReturn\"])\n      & at 204 ?~ \"No Content\"\n    deletOp = tOp\n      & parameters .~ fmap ref (rs <> [\"preferReturn\"])\n      & at 204 ?~ \"No Content\"\n    pr = (mempty :: PathItem) & get ?~ getOp\n    pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp\n    p False = pr\n    p True  = pw\n    tn = tableName t\n    rs = [ T.intercalate \".\" [\"rowFilter\", tn, colName c ] | c <- tableColumnsList t ]\n    ref = Ref . Reference\n\nmakeProcPathItem :: Routine -> (FilePath, PathItem)\nmakeProcPathItem pd = (\"/rpc/\" ++ toS (pdName pd), pe)\n  where\n    -- Use first line of proc description as summary; rest as description (if present)\n    -- We strip leading newlines from description so that users can include a blank line between summary and description\n    (pSum, pDesc) = fmap fst &&& fmap (T.dropWhile (=='\\n') . snd) $\n                    T.breakOn \"\\n\" <$> pdDescription pd\n    procOp = (mempty :: Operation)\n      & summary .~ pSum\n      & description .~ mfilter (/=\"\") pDesc\n      & tags .~ Set.fromList [\"(rpc) \" <> pdName pd]\n      & produces ?~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False]\n      & at 200 ?~ \"OK\"\n    getOp = procOp\n      & parameters .~ makeProcGetParams (pdParams pd)\n    postOp = procOp\n      & parameters .~ makeProcPostParams pd\n    pe = case pdVolatility pd of\n      Volatile -> (mempty :: PathItem) & post ?~ postOp\n      _        -> (mempty :: PathItem) & get ?~ getOp & post ?~ postOp\n\nmakeRootPathItem :: (FilePath, PathItem)\nmakeRootPathItem = (\"/\", p)\n  where\n    getOp = (mempty :: Operation)\n      & tags .~ Set.fromList [\"Introspection\"]\n      & summary ?~ \"OpenAPI description (this document)\"\n      & produces ?~ makeMimeList [MTOpenAPI, MTApplicationJSON]\n      & at 200 ?~ \"OK\"\n    pr = (mempty :: PathItem) & get ?~ getOp\n    p = pr\n\nmakePathItems :: [Routine] -> [Table] -> InsOrdHashMap FilePath PathItem\nmakePathItems pds ti = fromList $ makeRootPathItem :\n  fmap makePathItem ti ++ fmap makeProcPathItem pds\n\nmakeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions\nmakeSecurityDefinitions secName allow\n  | allow = SecurityDefinitions (fromList [(secName, SecurityScheme secSchType secSchDescription)])\n  | otherwise    = mempty\n  where\n    secSchType = SecuritySchemeApiKey (ApiKeyParams \"Authorization\" ApiKeyHeader)\n    secSchDescription = Just \"Add the token prepending \\\"Bearer \\\" (without quotes) to it\"\n\npostgrestSpec :: (Text, Text) -> RelationshipsMap -> [Routine] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger\npostgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)\n  & basePath ?~ T.unpack b\n  & schemes ?~ [s']\n  & info .~ ((mempty :: Info)\n      & version .~ prettyVersion\n      & title .~ fromMaybe \"PostgREST API\" dTitle\n      & description ?~ fromMaybe \"This is a dynamic API generated by PostgREST\" dDesc)\n  & externalDocs ?~ ((mempty :: ExternalDocs)\n    & description ?~ \"PostgREST Documentation\"\n    & url .~ URL (\"https://postgrest.org/en/\" <> docsVersion <> \"/references/api.html\"))\n  & host .~ h'\n  & definitions .~ fromList (makeTableDef rels <$> ti)\n  & parameters .~ fromList (makeParamDefs ti)\n  & paths .~ makePathItems pds ti\n  & produces .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]\n  & consumes .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]\n  & securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef\n  & security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]\n    where\n      s' = if s == \"http\" then Http else Https\n      h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))\n      securityDefName = \"JWT\"\n      (dTitle, dDesc) = fmap fst &&& fmap (T.dropWhile (=='\\n') . snd) $\n                    T.breakOn \"\\n\" <$> sd\n\npickProxy :: Maybe Text -> Maybe Proxy\npickProxy proxy\n  | isNothing proxy = Nothing\n  -- should never happen\n  -- since the request would have been rejected by the middleware if proxy uri\n  -- is malformed\n  | isMalformedProxyUri $ fromMaybe mempty proxy = Nothing\n  | otherwise = Just Proxy {\n    proxyScheme = scheme\n  , proxyHost = host'\n  , proxyPort = port''\n  , proxyPath = path'\n  }\n where\n   uri = toURI $ fromJust proxy\n   scheme = T.init $ T.toLower $ T.pack $ uriScheme uri\n   path URI {uriPath = \"\"} =  \"/\"\n   path URI {uriPath = p}  = p\n   path' = T.pack $ path uri\n   authority = fromJust $ uriAuthority uri\n   host' = T.pack $ uriRegName authority\n   port' = uriPort authority\n   readPort = fromMaybe 80 . readMaybe\n   port'' :: Integer\n   port'' = case (port', scheme) of\n             (\"\", \"http\")  -> 80\n             (\"\", \"https\") -> 443\n             _             -> readPort $ T.unpack $ T.tail $ T.pack port'\n\nproxyUri :: AppConfig -> (Text, Text, Integer, Text)\nproxyUri AppConfig{..} =\n  case pickProxy $ toS <$> configOpenApiServerProxyUri of\n    Just Proxy{..} ->\n      (proxyScheme, proxyHost, proxyPort, proxyPath)\n    Nothing ->\n      (\"http\", configServerHost, toInteger configServerPort, \"/\")\n"
  },
  {
    "path": "src/PostgREST/Response/Performance.hs",
    "content": "module PostgREST.Response.Performance\n  ( ServerTiming (..)\n  , serverTimingHeader\n  )\nwhere\nimport qualified Data.ByteString.Char8 as BS\nimport qualified Network.HTTP.Types    as HTTP\nimport           Numeric               (showFFloat)\nimport           Protolude\n\n-- | ServerTiming represents the timing data for a request, in seconds.\ndata ServerTiming =\n  ServerTiming\n    { jwt         :: Maybe Double\n    , parse       :: Maybe Double\n    , plan        :: Maybe Double\n    , transaction :: Maybe Double\n    , response    :: Maybe Double\n    }\n  deriving (Show)\n\n-- | Render the Server-Timing header from a ServerTimingData\n-- The duration precision is milliseconds, per the docs\n--\n-- >>> serverTimingHeader ServerTiming { plan=Just 0.1, transaction=Just 0.2, response=Just 0.3, jwt=Just 0.4, parse=Just 0.5}\n-- (\"Server-Timing\",\"jwt;dur=0.4, parse;dur=0.5, plan;dur=0.1, transaction;dur=0.2, response;dur=0.3\")\nserverTimingHeader :: ServerTiming -> HTTP.Header\nserverTimingHeader timing =\n  (\"Server-Timing\", renderTiming)\n  where\n    renderMetric metric = maybe \"\" (\\dur -> BS.concat [metric, BS.pack $ \";dur=\" <> showFFloat (Just 1) dur \"\"])\n    renderTiming = BS.intercalate \", \" $ (\\(k, v) -> renderMetric k (v timing)) <$>\n      [ (\"jwt\", jwt)\n      , (\"parse\", parse)\n      , (\"plan\", plan)\n      , (\"transaction\", transaction)\n      , (\"response\", response)\n      ]\n"
  },
  {
    "path": "src/PostgREST/Response.hs",
    "content": "{- |\n   Module      : PostgREST.Response\n   Description : Generate HTTP Response\n-}\n{-# LANGUAGE NamedFieldPuns  #-}\n{-# LANGUAGE RecordWildCards #-}\nmodule PostgREST.Response\n  ( actionResponse\n  , PgrstResponse(..)\n  ) where\n\nimport qualified Data.Aeson                as JSON\nimport qualified Data.ByteString.Char8     as BS\nimport qualified Data.ByteString.Lazy      as LBS\nimport qualified Data.HashMap.Strict       as HM\nimport           Data.Maybe                (fromJust)\nimport           Data.Text.Read            (decimal)\nimport qualified Network.HTTP.Types.Header as HTTP\nimport qualified Network.HTTP.Types.Status as HTTP\nimport qualified Network.HTTP.Types.URI    as HTTP\n\nimport qualified PostgREST.Error            as Error\nimport qualified PostgREST.MediaType        as MediaType\nimport qualified PostgREST.RangeQuery       as RangeQuery\nimport qualified PostgREST.Response.OpenAPI as OpenAPI\n\nimport PostgREST.ApiRequest              (ApiRequest (..))\nimport PostgREST.ApiRequest.Preferences  (PreferRepresentation (..),\n                                          PreferResolution (..),\n                                          Preferences (..),\n                                          prefAppliedHeader,\n                                          shouldCount)\nimport PostgREST.ApiRequest.QueryParams  (QueryParams (..))\nimport PostgREST.ApiRequest.Types        (InvokeMethod (..),\n                                          Mutation (..))\nimport PostgREST.Config                  (AppConfig (..))\nimport PostgREST.MainTx                  (DbResult (..),\n                                          ResultSet (..))\nimport PostgREST.MediaType               (MediaType (..))\nimport PostgREST.Plan                    (CrudPlan (..),\n                                          InfoPlan (..),\n                                          InspectPlan (..))\nimport PostgREST.Plan.MutatePlan         (MutatePlan (..))\nimport PostgREST.Response.GucHeader      (GucHeader, unwrapGucHeader)\nimport PostgREST.SchemaCache             (SchemaCache (..))\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),\n                                          Schema)\nimport PostgREST.SchemaCache.Routine     (FuncVolatility (..),\n                                          Routine (..))\nimport PostgREST.SchemaCache.Table       (Table (..))\n\nimport qualified PostgREST.SchemaCache.Routine as Routine\n\nimport Protolude      hiding (Handler, toS)\nimport Protolude.Conv (toS)\n\ndata PgrstResponse = PgrstResponse {\n  pgrstStatus  :: HTTP.Status\n, pgrstHeaders :: [HTTP.Header]\n, pgrstBody    :: LBS.ByteString\n}\n\nactionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Either Error.Error PgrstResponse\n\nactionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do\n  let\n    (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal\n    cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]\n    prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest\n\n    headers =\n      [ contentRange\n      , ( \"Content-Location\"\n        , \"/\"\n            <> toUtf8 (qiName identifier)\n            <> if BS.null (qsCanonical iQueryParams) then mempty else \"?\" <> qsCanonical iQueryParams\n        )\n      ]\n      ++ cLHeader\n      ++ contentTypeHeaders pMedia ctxApiRequest\n      ++ prefHeader\n    bod | status == HTTP.status416 = Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange $\n                                     Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe \"0\" show rsTableTotal)\n        | headersOnly              = mempty\n        | otherwise                = LBS.fromStrict rsBody\n\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers\n\n  Right $ PgrstResponse ovStatus ovHeaders bod\n\nactionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationCreate, pMedia, crudQi=QualifiedIdentifier{..}} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do\n  let\n    prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest\n\n    headers =\n      catMaybes\n        [ if null rsLocation then\n            Nothing\n          else\n            Just\n              ( HTTP.hLocation\n              , \"/\"\n                  <> toUtf8 qiName\n                  <> HTTP.renderSimpleQuery True rsLocation\n              )\n        , Just . RangeQuery.contentRangeH 1 0 $\n            if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing\n        , prefHeader ]\n\n    isInsertIfGTZero i =\n        if i <= 0 && preferResolution iPreferences == Just MergeDuplicates then\n          HTTP.status200\n        else\n          HTTP.status201\n    status = maybe HTTP.status200 isInsertIfGTZero rsInserted\n    (headers', bod) = case preferRepresentation iPreferences of\n      Just Full -> (headers ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)\n      Just None -> (headers, mempty)\n      Just HeadersOnly -> (headers, mempty)\n      Nothing -> (headers, mempty)\n\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status $ contentLengthHeader bod:headers'\n\n  Right $ PgrstResponse ovStatus ovHeaders bod\n\nactionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationUpdate, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do\n  let\n    contentRangeHeader =\n      Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $\n        if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing\n\n    prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest\n\n    headers = catMaybes [contentRangeHeader, prefHeader]\n    lbsBody = LBS.fromStrict rsBody\n\n  let (status, headers', body) =\n        case preferRepresentation iPreferences of\n          Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)\n          Just None -> (HTTP.status204, headers, mempty)\n          _         -> (HTTP.status204, headers, mempty)\n\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers'\n\n  Right $ PgrstResponse ovStatus ovHeaders body\n\nactionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do\n  let\n    prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest\n    lbsBody = LBS.fromStrict rsBody\n    cLHeader = [contentLengthHeader lbsBody]\n    cTHeader = contentTypeHeaders pMedia ctxApiRequest\n\n  let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200\n      upsertStatus       = isInsertIfGTZero $ fromJust rsInserted\n      (status, headers, body) =\n        case preferRepresentation iPreferences of\n          Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, lbsBody)\n          Just None -> (HTTP.status204,  prefHeader, mempty)\n          _ -> (HTTP.status204, prefHeader, mempty)\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers\n\n  Right $ PgrstResponse ovStatus ovHeaders body\n\nactionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do\n  let\n    contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing\n    prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest\n    headers = contentRangeHeader : prefHeader\n    lbsBody = LBS.fromStrict rsBody\n    (status, headers', body) =\n        case preferRepresentation iPreferences of\n            Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)\n            Just None -> (HTTP.status204, headers, mempty)\n            _ -> (HTTP.status204, headers, mempty)\n\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers'\n\n  Right $ PgrstResponse ovStatus ovHeaders body\n\nactionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do\n  let\n    (status, contentRange) =\n      RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal\n    rsOrErrBody = if status == HTTP.status416\n      then Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange\n        $ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe \"0\" show rsTableTotal)\n      else LBS.fromStrict rsBody\n    isHeadMethod = invMethod == InvRead True\n    prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest\n    cLHeader = if isHeadMethod then mempty else [contentLengthHeader rsOrErrBody]\n    headers = contentRange : prefHeader\n    (status', headers', body) =\n        if Routine.funcReturnsVoid proc then\n            (HTTP.status204, headers, mempty)\n          else\n            (status,\n              headers ++ cLHeader ++ contentTypeHeaders pMedia ctxApiRequest,\n              if isHeadMethod then mempty else rsOrErrBody)\n\n  (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'\n\n  Right $ PgrstResponse ovStatus ovHeaders body\n\nactionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ =\n  let body = LBS.fromStrict plan in\n  Right $ PgrstResponse HTTP.status200 (contentLengthHeader body : contentTypeHeaders media ctxApiRequest) body\n\nactionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) ApiRequest{..} versions conf sCache =\n  let\n    rsBody = maybe mempty (\\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body\n    cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]\n  in\n  Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader iSchema iNegotiatedByProfile)) rsBody\n\nactionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ sc@SchemaCache{dbTables} =\n  case HM.lookup qi dbTables of\n    Just tbl -> respondInfo $ allowH tbl\n    Nothing  -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName sc\n  where\n    allowH table =\n      let hasPK = not . null $ tablePKCols table in\n      BS.intercalate \",\" $\n          [\"OPTIONS,GET,HEAD\"] ++\n          [\"POST\" | tableInsertable table] ++\n          [\"PUT\" | tableInsertable table && tableUpdatable table && hasPK] ++\n          [\"PATCH\" | tableUpdatable table] ++\n          [\"DELETE\" | tableDeletable table]\n\nactionResponse (NoDbResult (RoutineInfoPlan proc)) _ _ _ _\n  | pdVolatility proc == Volatile = respondInfo \"OPTIONS,POST\"\n  | otherwise                     = respondInfo \"OPTIONS,GET,HEAD,POST\"\n\nactionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ = respondInfo \"OPTIONS,GET,HEAD\"\n\nrespondInfo :: ByteString -> Either Error.Error PgrstResponse\nrespondInfo allowHeader =\n  let allOrigins = (\"Access-Control-Allow-Origin\", \"*\") in\n  Right $ PgrstResponse HTTP.status200 [contentLengthHeader mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty\n\n-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers\noverrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])\noverrideStatusHeaders rsGucStatus rsGucHeaders pgrstStatus pgrstHeaders = do\n  gucStatus <- decodeGucStatus rsGucStatus\n  gucHeaders <- decodeGucHeaders rsGucHeaders\n  Right (fromMaybe pgrstStatus gucStatus, addHeadersIfNotIncluded pgrstHeaders $ map unwrapGucHeader gucHeaders)\n\ndecodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]\ndecodeGucHeaders =\n  maybe (Right []) $ first (const . Error.ApiRequestErr $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict\n\ndecodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)\ndecodeGucStatus =\n  maybe (Right Nothing) $ first (const . Error.ApiRequestErr $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal\n\ncontentLengthHeader :: LBS.ByteString -> HTTP.Header\ncontentLengthHeader body = (\"Content-Length\", show (LBS.length body))\n\ncontentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]\ncontentTypeHeaders mediaType ApiRequest{..} =\n  MediaType.toContentType mediaType : maybeToList (profileHeader iSchema iNegotiatedByProfile)\n\nprofileHeader :: Schema -> Bool -> Maybe HTTP.Header\nprofileHeader schema negotiatedByProfile =\n  if negotiatedByProfile\n    then Just $ (,) \"Content-Profile\" (toS schema)\n  else\n    Nothing\n\n-- | Add headers not already included to allow the user to override them instead of duplicating them\naddHeadersIfNotIncluded :: [HTTP.Header] -> [HTTP.Header] -> [HTTP.Header]\naddHeadersIfNotIncluded newHeaders initialHeaders =\n  filter (\\(nk, _) -> isNothing $ find (\\(ik, _) -> ik == nk) initialHeaders) newHeaders ++\n  initialHeaders\n\n-- | Get Preferences for Preference-Applied header per plan\nresponsePreferences :: CrudPlan -> ApiRequest -> Preferences\nresponsePreferences plan ApiRequest{iPreferences=Preferences{..}, iQueryParams=QueryParams{..}} =\n  let\n    -- Only returned on Inserts\n    preferResolution' = case plan of\n      MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan} ->\n        let pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols ; _ -> mempty; }\n        in (if null pkCols && isNothing qsOnConflict then Nothing else preferResolution)\n      _ -> Nothing\n\n    preferRepresentation' = case plan of\n      MutateReadPlan{} -> preferRepresentation\n      _                -> Nothing\n\n    preferMissing' = case plan of\n      MutateReadPlan{mrMutation=MutationCreate} -> preferMissing\n      MutateReadPlan{mrMutation=MutationUpdate} -> preferMissing\n      _                                         -> Nothing\n\n    preferMaxAffected' = case plan of\n      MutateReadPlan{mrMutation=MutationUpdate} -> preferMaxAffected\n      MutateReadPlan{mrMutation=MutationDelete} -> preferMaxAffected\n      CallReadPlan{}                            -> preferMaxAffected\n      _                                         -> Nothing\n\n    in Preferences preferResolution' preferRepresentation' preferCount preferTransaction preferMissing' preferHandling preferTimezone preferMaxAffected' []\n"
  },
  {
    "path": "src/PostgREST/SchemaCache/Identifiers.hs",
    "content": "{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric  #-}\n\nmodule PostgREST.SchemaCache.Identifiers\n  ( FieldName\n  , QualifiedIdentifier(..)\n  , RelIdentifier(..)\n  , Schema\n  , TableName\n  , escapeIdent\n  , isAnyElement\n  , quoteQi\n  , toQi\n  , trimNullChars\n  ) where\n\nimport qualified Data.Aeson as JSON\nimport qualified Data.Text  as T\n\nimport Protolude\n\ndata RelIdentifier = RelId QualifiedIdentifier | RelAnyElement\n  deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey, Show)\ninstance Hashable RelIdentifier\n\n-- | Represents a pg identifier with a prepended schema name \"schema.table\".\n-- When qiSchema is \"\", the schema is defined by the pg search_path.\n-- TODO: Refactor this, we also use QI for procedure names\ndata QualifiedIdentifier = QualifiedIdentifier\n  { qiSchema :: Schema\n  , qiName   :: TableName\n  }\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)\n\ninstance Hashable QualifiedIdentifier\n\nisAnyElement :: QualifiedIdentifier -> Bool\nisAnyElement y = QualifiedIdentifier \"pg_catalog\" \"anyelement\" == y\n\n-- |\n-- Quote the qualified identifier when preparing the SQL. This avoids parse\n-- errors by postgres, for example on pg reserved words like \"true\" or \"select\".\n--\n-- >>> quoteQi (QualifiedIdentifier \"\" \"true\")\n-- \"\\\"true\\\"\"\nquoteQi :: QualifiedIdentifier -> Text\nquoteQi (QualifiedIdentifier s i) =\n  (if T.null s then mempty else escapeIdent s <> \".\") <> escapeIdent i\n\n-- TODO: Handle a case where the QI comes like this: \"my.fav.schema\".\"my.identifier\"\n-- Right now it only handles the schema.identifier case\ntoQi :: Text -> QualifiedIdentifier\ntoQi txt = case T.drop 1 <$> T.breakOn \".\" txt of\n  (i, \"\") -> QualifiedIdentifier mempty i\n  (s, i)  -> QualifiedIdentifier s i\n\nescapeIdent :: Text -> Text\nescapeIdent x = \"\\\"\" <> T.replace \"\\\"\" \"\\\"\\\"\" (trimNullChars x) <> \"\\\"\"\n\ntrimNullChars :: Text -> Text\ntrimNullChars = T.takeWhile (/= '\\x0')\n\ntype Schema = Text\ntype TableName = Text\ntype FieldName = Text\n"
  },
  {
    "path": "src/PostgREST/SchemaCache/Relationship.hs",
    "content": "{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric  #-}\n\nmodule PostgREST.SchemaCache.Relationship\n  ( Cardinality(..)\n  , Relationship(..)\n  , Junction(..)\n  , RelationshipsMap\n  , relIsToOne\n  ) where\n\nimport qualified Data.Aeson          as JSON\nimport qualified Data.HashMap.Strict as HM\n\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier, Schema)\n\nimport Protolude\n\n\n-- | Relationship between two tables.\ndata Relationship = Relationship\n  { relTable        :: QualifiedIdentifier\n  , relForeignTable :: QualifiedIdentifier\n  , relIsSelf       :: Bool -- ^ Whether is a self relationship\n  , relCardinality  :: Cardinality\n  , relTableIsView  :: Bool\n  , relFTableIsView :: Bool\n  }\n  | ComputedRelationship\n  { relFunction     :: QualifiedIdentifier\n  , relTable        :: QualifiedIdentifier\n  , relForeignTable :: QualifiedIdentifier\n  , relTableAlias   :: QualifiedIdentifier\n  , relToOne        :: Bool\n  , relIsSelf       :: Bool\n  }\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\n-- | The relationship cardinality\n-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)\ndata Cardinality\n  = O2M {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}\n  -- ^ one-to-many\n  | M2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}\n  -- ^ many-to-one\n  | O2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)], isParent :: Bool}\n  -- ^ one-to-one, this is a refinement over M2O, operating on it is pretty much the same as M2O when isParent == False\n  | M2M Junction\n  -- ^ many-to-many\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\ntype FKConstraint = Text\n\n-- | Junction table on an M2M relationship\ndata Junction = Junction\n  { junTable       :: QualifiedIdentifier\n  , junConstraint1 :: FKConstraint\n  , junConstraint2 :: FKConstraint\n  , junColsSource  :: [(FieldName, FieldName)]\n  , junColsTarget  :: [(FieldName, FieldName)]\n  }\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\n-- | Key based on the source table and the foreign table schema\ntype RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema)  [Relationship]\n\nrelIsToOne :: Relationship -> Bool\nrelIsToOne rel = case rel of\n  Relationship{relCardinality=M2O {}} -> True\n  Relationship{relCardinality=O2O {}} -> True\n  ComputedRelationship{relToOne=True} -> True\n  _                                   -> False\n"
  },
  {
    "path": "src/PostgREST/SchemaCache/Representations.hs",
    "content": "{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric  #-}\n\nmodule PostgREST.SchemaCache.Representations\n  ( DataRepresentation(..)\n  , RepresentationsMap\n  ) where\n\nimport qualified Data.Aeson          as JSON\nimport qualified Data.HashMap.Strict as HM\n\n\nimport Protolude\n\n-- | Data representations allow user customisation of how to present and receive data through APIs, per field.\n-- This structure is used for the library of available transforms. It answers questions like:\n-- - What function, if any, should be used to present a certain field that's been selected for API output?\n-- - How do we parse incoming data for a certain field type when inserting or updating?\n-- - And similarly, how do we parse textual data in a query string to be used as a filter?\n--\n-- Support for outputting special formats like CSV and binary data would fit into the same system.\ndata DataRepresentation = DataRepresentation\n  { drSourceType :: Text\n  , drTargetType :: Text\n  , drFunction   :: Text\n  } deriving (Eq, Show, Generic, JSON.ToJSON, JSON.FromJSON)\n\n-- The representation map maps from (source type, target type) to a DR.\ntype RepresentationsMap = HM.HashMap (Text, Text) DataRepresentation\n"
  },
  {
    "path": "src/PostgREST/SchemaCache/Routine.hs",
    "content": "{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric  #-}\n\nmodule PostgREST.SchemaCache.Routine\n  ( PgType(..)\n  , Routine(..)\n  , RoutineParam(..)\n  , FuncVolatility(..)\n  , FuncSettings\n  , RoutineMap\n  , RetType(..)\n  , funcReturnsScalar\n  , funcReturnsSetOfScalar\n  , funcReturnsSingleComposite\n  , funcReturnsVoid\n  , funcTableName\n  , funcReturnsSingle\n  , MediaHandlerMap\n  , ResolvedHandler\n  , MediaHandler(..)\n  ) where\n\nimport           Data.Aeson                 ((.=))\nimport qualified Data.Aeson                 as JSON\nimport qualified Data.HashMap.Strict        as HM\nimport qualified Hasql.Transaction.Sessions as SQL\nimport qualified PostgREST.MediaType        as MediaType\n\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),\n                                          RelIdentifier (..), Schema,\n                                          TableName)\n\n\nimport Protolude\n\ndata PgType\n  = Scalar QualifiedIdentifier\n  | Composite QualifiedIdentifier Bool -- True if the composite is a domain alias(used to work around a bug in pg 11 and 12, see QueryBuilder.hs)\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\ndata RetType\n  = Single PgType\n  | SetOf PgType\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\ndata FuncVolatility\n  = Volatile\n  | Stable\n  | Immutable\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\ntype FuncSettings = [(Text,Text)]\n\ndata Routine = Function\n  { pdSchema       :: Schema\n  , pdName         :: Text\n  , pdDescription  :: Maybe Text\n  , pdParams       :: [RoutineParam]\n  , pdReturnType   :: RetType\n  , pdVolatility   :: FuncVolatility\n  , pdHasVariadic  :: Bool\n  , pdIsoLvl       :: Maybe SQL.IsolationLevel\n  , pdFuncSettings :: FuncSettings\n  }\n  deriving (Eq, Show, Generic)\n-- need to define JSON manually bc SQL.IsolationLevel doesn't have a JSON instance(and we can't define one for that type without getting a compiler error)\ninstance JSON.ToJSON Routine where\n  toJSON (Function sch nam desc params ret vol hasVar _ sets) = JSON.object\n    [\n      \"pdSchema\"       .= sch\n    , \"pdName\"         .= nam\n    , \"pdDescription\"  .= desc\n    , \"pdParams\"       .= JSON.toJSON params\n    , \"pdReturnType\"   .= JSON.toJSON ret\n    , \"pdVolatility\"   .= JSON.toJSON vol\n    , \"pdHasVariadic\"  .= JSON.toJSON hasVar\n    , \"pdFuncSettings\" .= JSON.toJSON sets\n    ]\n\ndata RoutineParam = RoutineParam\n  { ppName          :: Text\n  , ppType          :: Text\n  , ppTypeMaxLength :: Text\n  , ppReq           :: Bool\n  , ppVar           :: Bool\n  }\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\n-- Order by least number of params in the case of overloaded functions\ninstance Ord Routine where\n  Function schema1 name1 des1 prms1 rt1 vol1 hasVar1 iso1 sets1 `compare` Function schema2 name2 des2 prms2 rt2 vol2 hasVar2 iso2 sets2\n    | schema1 == schema2 && name1 == name2 && length prms1 < length prms2  = LT\n    | schema1 == schema2 && name1 == name2 && length prms1 > length prms2  = GT\n    | otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1, iso1, sets1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2, iso2, sets2)\n\n-- | A map of all procs, all of which can be overloaded(one entry will have more than one Routine).\n-- | It uses a HashMap for a faster lookup.\ntype RoutineMap = HM.HashMap QualifiedIdentifier [Routine]\n\n-- | A media handler can be an aggregate over a composite type or a function over a scalar\ndata MediaHandler\n   -- non overridable builtins\n   = BuiltinAggSingleJson Bool\n   | BuiltinAggArrayJsonStrip\n   -- these builtins are overridable\n   | BuiltinOvAggJson\n   | BuiltinOvAggGeoJson\n   | BuiltinOvAggCsv\n   -- custom\n   | CustomFunc QualifiedIdentifier RelIdentifier\n   | NoAgg\n   deriving (Eq, Show, Generic, JSON.ToJSON)\n\nfuncReturnsSingle :: Routine -> Bool\nfuncReturnsSingle proc = case proc of\n  Function{pdReturnType = Single _} -> True\n  _                                 -> False\n\nfuncReturnsScalar :: Routine -> Bool\nfuncReturnsScalar proc = case proc of\n  Function{pdReturnType = Single (Scalar{})} -> True\n  _                                          -> False\n\nfuncReturnsSetOfScalar :: Routine -> Bool\nfuncReturnsSetOfScalar proc = case proc of\n  Function{pdReturnType = SetOf (Scalar{})} -> True\n  _                                         -> False\n\nfuncReturnsSingleComposite :: Routine -> Bool\nfuncReturnsSingleComposite proc = case proc of\n  Function{pdReturnType = Single (Composite _ _)} -> True\n  _                                               -> False\n\nfuncReturnsVoid :: Routine -> Bool\nfuncReturnsVoid proc = case proc of\n  Function{pdReturnType = Single (Scalar (QualifiedIdentifier \"pg_catalog\" \"void\"))} -> True\n  _                                                                                  -> False\n\nfuncTableName :: Routine -> Maybe TableName\nfuncTableName proc = case pdReturnType proc of\n  SetOf  (Composite qi _) -> Just $ qiName qi\n  Single (Composite qi _) -> Just $ qiName qi\n  _                       -> Nothing\n\n-- the resolved handler also carries the media type because MTAny (*/*) is resolved to a different media type\ntype ResolvedHandler = (MediaHandler, MediaType.MediaType)\ntype MediaHandlerMap = HM.HashMap (RelIdentifier, MediaType.MediaType) ResolvedHandler\n"
  },
  {
    "path": "src/PostgREST/SchemaCache/Table.hs",
    "content": "{-# LANGUAGE DeriveAnyClass    #-}\n{-# LANGUAGE DeriveGeneric     #-}\n{-# LANGUAGE FlexibleInstances #-}\n\nmodule PostgREST.SchemaCache.Table\n  ( Column(..)\n  , Table(..)\n  , tableColumnsList\n  , TablesMap\n  , ColumnMap\n  ) where\n\nimport qualified Data.Aeson                 as JSON\nimport qualified Data.HashMap.Strict        as HM\nimport qualified Data.HashMap.Strict.InsOrd as HMI\n\nimport PostgREST.SchemaCache.Identifiers (FieldName,\n                                          QualifiedIdentifier (..),\n                                          Schema, TableName)\n\nimport Protolude\n\n\ndata Table = Table\n  { tableSchema      :: Schema\n  , tableName        :: TableName\n  , tableDescription :: Maybe Text\n     -- TODO Find a better way to separate tables and views\n   , tableIsView     :: Bool\n    -- The following fields identify what HTTP verbs can be executed on the table/view, they're not related to the privileges granted to it\n  , tableInsertable  :: Bool\n  , tableUpdatable   :: Bool\n  , tableDeletable   :: Bool\n  , tablePKCols      :: [FieldName]\n  , tableColumns     :: ColumnMap\n  }\n  deriving (Show, Generic, JSON.ToJSON)\n\ntableColumnsList :: Table -> [Column]\ntableColumnsList = HMI.elems . tableColumns\n\ninstance Eq Table where\n  Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2\n\ndata Column = Column\n  { colName        :: FieldName\n  , colDescription :: Maybe Text\n  , colNullable    :: Bool\n  , colType        :: Text\n  , colNominalType :: Text\n  , colMaxLen      :: Maybe Int32\n  , colDefault     :: Maybe Text\n  , colEnum        :: [Text]\n  }\n  deriving (Eq, Show, Ord, Generic, JSON.ToJSON)\n\ntype TablesMap = HM.HashMap QualifiedIdentifier Table\ntype ColumnMap = HMI.InsOrdHashMap FieldName Column\n"
  },
  {
    "path": "src/PostgREST/SchemaCache.hs",
    "content": "{-|\nModule      : PostgREST.SchemaCache\nDescription : PostgREST schema cache\n\nThis module(used to be named DbStructure) contains queries that target PostgreSQL system catalogs, these are used to build the schema cache(SchemaCache).\n\nThe schema cache is necessary for resource embedding, foreign keys are used for inferring the relationships between tables.\n\nThese queries are executed once at startup or when PostgREST is reloaded.\n-}\n{-# LANGUAGE DeriveAnyClass        #-}\n{-# LANGUAGE DeriveGeneric         #-}\n{-# LANGUAGE FlexibleContexts      #-}\n{-# LANGUAGE MultiParamTypeClasses #-}\n{-# LANGUAGE NamedFieldPuns        #-}\n{-# LANGUAGE QuasiQuotes           #-}\n{-# LANGUAGE RecordWildCards       #-}\n{-# LANGUAGE ScopedTypeVariables   #-}\n{-# LANGUAGE TypeSynonymInstances  #-}\n\nmodule PostgREST.SchemaCache\n  ( SchemaCache(..)\n  , TablesFuzzyIndex\n  , querySchemaCache\n  , showSummary\n  , decodeFuncs\n  ) where\n\nimport           Data.Aeson                 ((.=))\nimport qualified Data.Aeson                 as JSON\nimport qualified Data.HashMap.Strict        as HM\nimport qualified Data.HashMap.Strict.InsOrd as HMI\nimport qualified Data.Set                   as S\nimport qualified Data.Text                  as T\nimport qualified Hasql.Decoders             as HD\nimport qualified Hasql.Encoders             as HE\nimport qualified Hasql.Statement            as SQL\nimport qualified Hasql.Transaction          as SQL\n\nimport Data.Functor.Contravariant ((>$<))\nimport NeatInterpolation          (trimming)\n\nimport PostgREST.Config                      (AppConfig (..))\nimport PostgREST.Config.Database             (TimezoneNames,\n                                              toIsolationLevel)\nimport PostgREST.SchemaCache.Identifiers     (FieldName,\n                                              QualifiedIdentifier (..),\n                                              RelIdentifier (..),\n                                              Schema, escapeIdent,\n                                              isAnyElement)\nimport PostgREST.SchemaCache.Relationship    (Cardinality (..),\n                                              Junction (..),\n                                              Relationship (..),\n                                              RelationshipsMap)\nimport PostgREST.SchemaCache.Representations (DataRepresentation (..),\n                                              RepresentationsMap)\nimport PostgREST.SchemaCache.Routine         (FuncVolatility (..),\n                                              MediaHandler (..),\n                                              MediaHandlerMap,\n                                              PgType (..),\n                                              RetType (..),\n                                              Routine (..),\n                                              RoutineMap,\n                                              RoutineParam (..))\nimport PostgREST.SchemaCache.Table           (Column (..), ColumnMap,\n                                              Table (..), TablesMap)\n\nimport qualified PostgREST.MediaType as MediaType\n\nimport           Control.Arrow    ((&&&))\nimport qualified Data.FuzzySet    as Fuzzy\nimport           Protolude\nimport           System.IO.Unsafe (unsafePerformIO)\n\ntype TablesFuzzyIndex = HM.HashMap Schema Fuzzy.FuzzySet\n\ndata SchemaCache = SchemaCache\n  { dbTables           :: TablesMap\n  , dbRelationships    :: RelationshipsMap\n  , dbRoutines         :: RoutineMap\n  , dbRepresentations  :: RepresentationsMap\n  , dbMediaHandlers    :: MediaHandlerMap\n  , dbTimezones        :: TimezoneNames\n  -- Memoized fuzzy index of table names per schema to support approximate matching\n  -- Since index construction can be expensive, we build it once and store in the SchemaCache\n  -- Haskell lazy evaluation ensures it's only built on first use and memoized afterwards\n  , dbTablesFuzzyIndex :: TablesFuzzyIndex\n  } deriving (Show)\n\ninstance JSON.ToJSON SchemaCache where\n  toJSON (SchemaCache tabs rels routs reps hdlers tzs _) = JSON.object [\n      \"dbTables\"          .= JSON.toJSON tabs\n    , \"dbRelationships\"   .= JSON.toJSON rels\n    , \"dbRoutines\"        .= JSON.toJSON routs\n    , \"dbRepresentations\" .= JSON.toJSON reps\n    , \"dbMediaHandlers\"   .= JSON.toJSON hdlers\n    , \"dbTimezones\"       .= JSON.toJSON tzs\n    ]\n\nshowSummary :: SchemaCache -> Text\nshowSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =\n  T.intercalate \", \"\n  [ show (HM.size tbls)       <> \" Relations\"\n  , show (HM.size rels)       <> \" Relationships\"\n  , show (HM.size routs)      <> \" Functions\"\n  , show (HM.size reps)       <> \" Domain Representations\"\n  , show (HM.size mediaHdlrs) <> \" Media Type Handlers\"\n  , show (S.size tzs)         <> \" Timezones\"\n  ]\n\n-- | A view foreign key or primary key dependency detected on its source table\n-- Each column of the key could be referenced multiple times in the view, e.g.\n--\n-- create view projects_view as\n-- select\n--   id as id_1,\n--   id as id_2,\n--   id as id_3,\n--   name\n-- from projects\n--\n-- In this case, the keyDepCols mapping maps projects.id to all three of the columns:\n--\n-- [('id', ['id_1', 'id_2', 'id_3'])]\n--\n-- Depending on key type, we can then choose how to handle this case. Primary keys\n-- can arbitrarily choose one of the columns, but for foreign keys we need to create\n-- relationships for each possible mutations.\n--\n-- Previously, we stored a (FieldName, FieldName) tuple only, but then we had no\n-- way to make a difference between a multi-column-key and a single-column-key with multiple\n-- references in the view. Or even worse in the multi-column-key-multi-reference case...\ndata ViewKeyDependency = ViewKeyDependency {\n  keyDepTable :: QualifiedIdentifier\n, keyDepView  :: QualifiedIdentifier\n, keyDepCons  :: Text\n, keyDepType  :: KeyDep\n, keyDepCols  :: [(FieldName, [FieldName])] -- ^ First element is the table column, second is a list of view columns\n} deriving (Eq)\ndata KeyDep\n  = PKDep    -- ^ PK dependency\n  | FKDep    -- ^ FK dependency\n  | FKDepRef -- ^ FK reference dependency\n  deriving (Eq, Generic, Hashable)\n\n-- | A SQL query that can be executed independently\ntype SqlQuery = ByteString\n\nmaxDbTablesForFuzzySearch :: Int\nmaxDbTablesForFuzzySearch = 500\n\nquerySchemaCache :: AppConfig -> SQL.Transaction SchemaCache\nquerySchemaCache conf@AppConfig{..} = do\n  SQL.sql \"set local schema ''\" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object\n  tabs    <- SQL.statement conf $ allTables prepared\n  keyDeps <- SQL.statement conf $ allViewsKeyDependencies prepared\n  m2oRels <- SQL.statement mempty $ allM2OandO2ORels prepared\n  funcs   <- SQL.statement conf $ allFunctions prepared\n  cRels   <- SQL.statement mempty $ allComputedRels prepared\n  reps    <- SQL.statement conf $ dataRepresentations prepared\n  mHdlers <- SQL.statement conf $ mediaHandlers prepared\n  tzones  <- SQL.statement mempty $ timezones prepared\n  _       <-\n    let sleepCall = SQL.Statement \"select pg_sleep($1 / 1000.0)\" (param HE.int4) HD.noResult prepared in\n    for_ configInternalSCQuerySleep (`SQL.statement` sleepCall) -- only used for testing\n\n  let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps\n      rels          = addInverseRels $ addM2MRels tabsWViewsPks $ addViewM2OAndO2ORels keyDeps m2oRels\n\n  -- Add delay in loading schema cache when internal-schema-cache-load-sleep config is set\n  return $ delayEval configInternalSCLoadSleep $ removeInternal schemas $ SchemaCache {\n      dbTables = tabsWViewsPks\n    -- Add delay in loading relationships when internal-schema-cache-relationship-load-sleep config is set\n    , dbRelationships = delayEval configInternalSCRelLoadSleep $ getOverrideRelationshipsMap rels cRels\n    , dbRoutines = funcs\n    , dbRepresentations = reps\n    , dbMediaHandlers = HM.union mHdlers initialMediaHandlers -- the custom handlers will override the initial ones\n    , dbTimezones = tzones\n\n    , dbTablesFuzzyIndex =\n        -- Only build fuzzy index for schemas with a reasonable number of tables\n        -- Fuzzy.FuzzySet is memory heavy we just don't use it for large schemas\n        Fuzzy.fromList <$> HM.filter ((< maxDbTablesForFuzzySearch) . length) (HM.fromListWith (<>) ((qiSchema &&& pure . qiName) <$> HM.keys tabsWViewsPks))\n    }\n  where\n    schemas = toList configDbSchemas\n    prepared = configDbPreparedStatements\n    delayEval confDelay result = maybe result (unsafePerformIO . (($> result) . (threadDelay . (1000 *) . fromIntegral))) confDelay\n\n-- | overrides detected relationships with the computed relationships and gets the RelationshipsMap\ngetOverrideRelationshipsMap :: [Relationship] -> [Relationship] -> RelationshipsMap\ngetOverrideRelationshipsMap rels cRels =\n  sort <$> deformedRelMap patchedRels\n  where\n    -- there can only be a single (table_type, func_name) pair in a function definition `test.function(table_type)`, so we use HM.fromList to disallow duplicates\n    computedRels  = HM.fromList $ relMapKey <$> cRels\n    -- here we override the detected relationships with the user computed relationships, HM.union makes sure computedRels prevail\n    patchedRels   = HM.union computedRels (relsMap rels)\n    relsMap = HM.fromListWith (++) . fmap relMapKey\n    relMapKey rel = case rel of\n      Relationship{relTable,relForeignTable} -> ((relTable, relForeignTable), [rel])\n      -- we use (relTable, relFunction) as key to override detected relationships with the function name\n      ComputedRelationship{relTable,relFunction} -> ((relTable, relFunction), [rel])\n    -- Since a relationship is between a table and foreign table, the logical way to index/search is by their table/ftable QualifiedIdentifier\n    -- However, because we allow searching a relationship by the columns of the foreign key(using the \"column as target\" disambiguation) we lose the\n    -- ability to index by the foreign table name, so we deform the key. TODO remove once support for \"column as target\" is gone.\n    deformedRelMap = HM.fromListWith (++) . fmap addDeformedRelKey . HM.toList\n    addDeformedRelKey ((relT, relFT), rls) = ((relT, qiSchema relFT), rls)\n\n-- | Remove db objects that belong to an internal schema(not exposed through the API) from the SchemaCache.\nremoveInternal :: [Schema] -> SchemaCache -> SchemaCache\nremoveInternal schemas dbStruct =\n  SchemaCache {\n      dbTables          = HM.filterWithKey (\\(QualifiedIdentifier sch _) _ -> sch `elem` schemas) $ dbTables dbStruct\n    , dbRelationships   = filter (\\r -> qiSchema (relForeignTable r) `elem` schemas && not (hasInternalJunction r)) <$>\n                          HM.filterWithKey (\\(QualifiedIdentifier sch _, _) _ -> sch `elem` schemas ) (dbRelationships dbStruct)\n    , dbRoutines        = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.\n    , dbRepresentations = dbRepresentations dbStruct -- no need to filter, not directly exposed through the API\n    , dbMediaHandlers   = dbMediaHandlers dbStruct\n    , dbTimezones       = dbTimezones dbStruct\n    , dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct\n    }\n  where\n    hasInternalJunction ComputedRelationship{} = False\n    hasInternalJunction Relationship{relCardinality=card} = case card of\n      M2M Junction{junTable} -> qiSchema junTable `notElem` schemas\n      _                      -> False\n\ndecodeTables :: HD.Result TablesMap\ndecodeTables =\n HM.fromList . map (\\tbl@Table{tableSchema, tableName} -> (QualifiedIdentifier tableSchema tableName, tbl)) <$> HD.rowList tblRow\n where\n  tblRow = Table\n    <$> column HD.text\n    <*> column HD.text\n    <*> nullableColumn HD.text\n    <*> column HD.bool\n    <*> column HD.bool\n    <*> column HD.bool\n    <*> column HD.bool\n    <*> arrayColumn HD.text\n    <*> parseCols (compositeArrayColumn\n      (Column\n        <$> compositeField HD.text\n        <*> nullableCompositeField HD.text\n        <*> compositeField HD.bool\n        <*> compositeField HD.text\n        <*> compositeField HD.text\n        <*> nullableCompositeField HD.int4\n        <*> nullableCompositeField HD.text\n        <*> compositeFieldArray HD.text))\n\n\nparseCols :: HD.Row [Column] -> HD.Row ColumnMap\nparseCols = fmap (HMI.fromList . map (\\col@Column{colName} -> (colName, col)))\n\ndecodeRels :: HD.Result [Relationship]\ndecodeRels =\n HD.rowList relRow\n where\n  relRow = (\\(qi1, qi2, isSelf, constr, cols, isOneToOne)-> Relationship qi1 qi2 isSelf (if isOneToOne then O2O constr cols False else M2O constr cols) False False) <$> row\n  row =\n    (,,,,,) <$>\n    (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>\n    (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>\n    column HD.bool <*>\n    column HD.text <*>\n    compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text) <*>\n    column HD.bool\n\ndecodeViewKeyDeps :: HD.Result [ViewKeyDependency]\ndecodeViewKeyDeps =\n  map viewKeyDepFromRow <$> HD.rowList row\n where\n  row = (,,,,,,)\n    <$> column HD.text <*> column HD.text\n    <*> column HD.text <*> column HD.text\n    <*> column HD.text <*> column HD.text\n    <*> compositeArrayColumn\n        ((,)\n        <$> compositeField HD.text\n        <*> compositeFieldArray HD.text)\n\nviewKeyDepFromRow :: (Text,Text,Text,Text,Text,Text,[(Text, [Text])]) -> ViewKeyDependency\nviewKeyDepFromRow (s1,t1,s2,v2,cons,consType,sCols) = ViewKeyDependency (QualifiedIdentifier s1 t1) (QualifiedIdentifier s2 v2) cons keyDep sCols\n  where\n    keyDep | consType == \"p\" = PKDep\n           | consType == \"f\" = FKDep\n           | otherwise       = FKDepRef -- f_ref, we build this type in the query\n\ndecodeFuncs :: HD.Result RoutineMap\ndecodeFuncs =\n  -- Duplicate rows for a function means they're overloaded, order these by least args according to Routine Ord instance\n  map sort . HM.fromListWith (++) . map ((\\(x,y) -> (x, [y])) . addKey) <$> HD.rowList funcRow\n  where\n    funcRow = Function\n              <$> column HD.text\n              <*> column HD.text\n              <*> nullableColumn HD.text\n              <*> compositeArrayColumn\n                  (RoutineParam\n                  <$> compositeField HD.text\n                  <*> compositeField HD.text\n                  <*> compositeField HD.text\n                  <*> compositeField HD.bool\n                  <*> compositeField HD.bool)\n              <*> (parseRetType\n                  <$> column HD.text\n                  <*> column HD.text\n                  <*> column HD.bool\n                  <*> column HD.bool\n                  <*> column HD.bool)\n              <*> (parseVolatility <$> column HD.char)\n              <*> column HD.bool\n              <*> nullableColumn (toIsolationLevel <$> HD.text)\n              <*> compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text) -- function setting\n\n    addKey :: Routine -> (QualifiedIdentifier, Routine)\n    addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)\n\n    parseRetType :: Text -> Text -> Bool -> Bool -> Bool -> RetType\n    parseRetType schema name isSetOf isComposite isCompositeAlias\n      | isSetOf   = SetOf pgType\n      | otherwise = Single pgType\n      where\n        qi = QualifiedIdentifier schema name\n        pgType\n          | isComposite = Composite qi isCompositeAlias\n          | otherwise   = Scalar qi\n\n    parseVolatility :: Char -> FuncVolatility\n    parseVolatility v | v == 'i' = Immutable\n                      | v == 's' = Stable\n                      | otherwise = Volatile -- only 'v' can happen here\n\ndecodeRepresentations :: HD.Result RepresentationsMap\ndecodeRepresentations =\n  HM.fromList . map (\\rep@DataRepresentation{drSourceType, drTargetType} -> ((drSourceType, drTargetType), rep)) <$> HD.rowList row\n  where\n    row = DataRepresentation\n      <$> column HD.text\n      <*> column HD.text\n      <*> column HD.text\n\n-- Selects all potential data representation transformations. To qualify the cast must be\n-- 1. to or from a domain\n-- 2. implicit\n-- For the time being it must also be to/from JSON or text, although one can imagine a future where we support special\n-- cases like CSV specific representations.\ndataRepresentations :: Bool -> SQL.Statement AppConfig RepresentationsMap\ndataRepresentations = SQL.Statement sql mempty decodeRepresentations\n  where\n    sql = encodeUtf8 [trimming|\n    SELECT\n      c.castsource::regtype::text,\n      c.casttarget::regtype::text,\n      c.castfunc::regproc::text\n    FROM\n      pg_catalog.pg_cast c\n    JOIN pg_catalog.pg_type src_t\n      ON c.castsource::oid = src_t.oid\n    JOIN pg_catalog.pg_type dst_t\n      ON c.casttarget::oid = dst_t.oid\n    WHERE\n      c.castcontext = 'i'\n      AND c.castmethod = 'f'\n      AND has_function_privilege(c.castfunc, 'execute')\n      AND ((src_t.typtype = 'd' AND c.casttarget IN ('json'::regtype::oid , 'text'::regtype::oid))\n       OR (dst_t.typtype = 'd' AND c.castsource IN ('json'::regtype::oid , 'text'::regtype::oid)))\n    |]\n\nallFunctions :: Bool -> SQL.Statement AppConfig RoutineMap\nallFunctions = SQL.Statement funcsSqlQuery params decodeFuncs\n  where\n    params =\n      (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>\n      (configDbHoistedTxSettings >$< arrayParam HE.text)\n\nbaseTypesCte :: Text\nbaseTypesCte = [trimming|\n  -- Recursively get the base types of domains\n  base_types AS (\n    WITH RECURSIVE\n    recurse AS (\n      SELECT\n        oid,\n        typbasetype,\n        typnamespace AS base_namespace,\n        COALESCE(NULLIF(typbasetype, 0), oid) AS base_type\n      FROM pg_type\n      UNION\n      SELECT\n        t.oid,\n        b.typbasetype,\n        b.typnamespace AS base_namespace,\n        COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type\n      FROM recurse t\n      JOIN pg_type b ON t.typbasetype = b.oid\n    )\n    SELECT\n      oid,\n      base_namespace,\n      base_type\n    FROM recurse\n    WHERE typbasetype = 0\n  )\n|]\n\nfuncsSqlQuery :: SqlQuery\nfuncsSqlQuery = encodeUtf8 [trimming|\n  WITH\n  $baseTypesCte,\n  arguments AS (\n    SELECT\n      oid,\n      array_agg((\n        COALESCE(name, ''), -- name\n        type::regtype::text, -- type\n        CASE type\n          WHEN 'bit'::regtype THEN 'bit varying'\n          WHEN 'bit[]'::regtype THEN 'bit varying[]'\n          WHEN 'character'::regtype THEN 'character varying'\n          WHEN 'character[]'::regtype THEN 'character varying[]'\n          ELSE type::regtype::text\n        END, -- convert types that ignore the length and accept any value till maximum size\n        idx <= (pronargs - pronargdefaults), -- is_required\n        COALESCE(mode = 'v', FALSE) -- is_variadic\n      ) ORDER BY idx) AS args,\n      CASE COUNT(*) - COUNT(name) -- number of unnamed arguments\n        WHEN 0 THEN true\n        WHEN 1 THEN (array_agg(type))[1] IN ('bytea'::regtype, 'json'::regtype, 'jsonb'::regtype, 'text'::regtype, 'xml'::regtype)\n        ELSE false\n      END AS callable\n    FROM pg_proc,\n         unnest(proargnames, proargtypes, proargmodes)\n           WITH ORDINALITY AS _ (name, type, mode, idx)\n    WHERE type IS NOT NULL -- only input arguments\n    GROUP BY oid\n  )\n  SELECT\n    pn.nspname AS proc_schema,\n    p.proname AS proc_name,\n    d.description AS proc_description,\n    COALESCE(a.args, '{}') AS args,\n    tn.nspname AS schema,\n    COALESCE(comp.relname, t.typname) AS name,\n    p.proretset AS rettype_is_setof,\n    (t.typtype = 'c'\n     -- if any TABLE, INOUT or OUT arguments present, treat as composite\n     or COALESCE(proargmodes::text[] && '{t,b,o}', false)\n    ) AS rettype_is_composite,\n    bt.oid <> bt.base_type as rettype_is_composite_alias,\n    p.provolatile,\n    p.provariadic > 0 as hasvariadic,\n    lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,\n    coalesce(func_settings.kvs, '{}') as kvs\n  FROM pg_proc p\n  LEFT JOIN arguments a ON a.oid = p.oid\n  JOIN pg_namespace pn ON pn.oid = p.pronamespace\n  JOIN base_types bt ON bt.oid = p.prorettype\n  JOIN pg_type t ON t.oid = bt.base_type\n  JOIN pg_namespace tn ON tn.oid = t.typnamespace\n  LEFT JOIN pg_class comp ON comp.oid = t.typrelid\n  LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass\n  LEFT JOIN LATERAL unnest(proconfig) iso_config ON iso_config LIKE 'default_transaction_isolation%'\n  LEFT JOIN LATERAL (\n    SELECT\n      array_agg(row(\n        substr(setting, 1, strpos(setting, '=') - 1),\n        substr(setting, strpos(setting, '=') + 1)\n      )) as kvs\n    FROM unnest(proconfig) setting\n    WHERE setting ~ ANY($$2)\n  ) func_settings ON TRUE\n  WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)\n  AND prokind = 'f'\n  AND p.pronamespace = ANY($$1::regnamespace[]) |]\n{-\nAdds M2O and O2O relationships for views to tables, tables to views, and views to views. The example below is taken from the test fixtures, but the views names/colnames were modified.\n\n--allM2OandO2ORels sample query result--\nprivate      | personnages          | private    | actors           | personnages_role_id_fkey   | {\"(role_id,id)\"}\n\n--allViewsKeyDependencies sample query result--\nprivate      | personnages          | test       | personnages_view | personnages_role_id_fkey   | f       | {\"(role_id,roleId)\"}\nprivate      | actors               | test       | actors_view      | personnages_role_id_fkey   | f_ref   | {\"(id,actorId)\"}\n\n--this function result--\ntest         | personnages_view     | private    | actors           | personnages_role_id_fkey   | f        | {\"(roleId,id)\"}       | viewTableM2O\nprivate      | personnages          | test       | actors_view      | personnages_role_id_fkey   | f_ref    | {\"(role_id,actorId)\"} | tableViewM2O\ntest         | personnages_view     | test       | actors_view      | personnages_role_id_fkey   | f,r_ref  | {\"(roleId,actorId)\"}  | viewViewM2O\n-}\naddViewM2OAndO2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]\naddViewM2OAndO2ORels keyDeps rels =\n  rels ++ concatMap viewRels rels\n  where\n    isM2O card = case card of {M2O _ _       -> True; _ -> False;}\n    isO2O card = case card of {O2O _ _ False -> True; _ -> False;}\n    viewRels Relationship{relTable,relForeignTable,relCardinality=card} | isM2O card || isO2O card =\n      let\n        cons = relCons card\n        relCols = relColumns card\n        buildCard cns cls = if isM2O card then M2O cns cls else O2O cns cls False\n        viewTableRels = fold $ HM.lookup (relTable, (cons, FKDep)) indexedKeyDeps\n        tableViewRels = fold $ HM.lookup (relForeignTable, (cons, FKDepRef)) indexedKeyDeps\n      in\n        [ Relationship\n            (keyDepView vwTbl)\n            relForeignTable\n            False\n            (buildCard cons $ zipWith (\\(_, vCol) (_, fCol)-> (vCol, fCol)) keyDepColsVwTbl relCols)\n            True\n            False\n        | vwTbl <- viewTableRels\n        , keyDepColsVwTbl <- expandKeyDepCols $ keyDepCols vwTbl ]\n        ++\n        [ Relationship\n            relTable\n            (keyDepView tblVw)\n            False\n            (buildCard cons $ zipWith (\\(tCol, _) (_, vCol) -> (tCol, vCol)) relCols keyDepColsTblVw)\n            False\n            True\n        | tblVw <- tableViewRels\n        , keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]\n        ++\n        [\n          let\n            vw1 = keyDepView vwTbl\n            vw2 = keyDepView tblVw\n          in\n          Relationship\n            vw1\n            vw2\n            (vw1 == vw2)\n            (buildCard cons $ zipWith (\\(_, vcol1) (_, vcol2) -> (vcol1, vcol2)) keyDepColsVwTbl keyDepColsTblVw)\n            True\n            True\n        | vwTbl <- viewTableRels\n        , keyDepColsVwTbl <- expandKeyDepCols $ keyDepCols vwTbl\n        , tblVw <- tableViewRels\n        , keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]\n    viewRels _ = []\n    expandKeyDepCols kdc = zip (fst <$> kdc) <$> traverse snd kdc\n    indexedKeyDeps = HM.fromListWith (<>) $ fmap ((keyDepTable &&& keyDepCons &&& keyDepType) &&& pure) keyDeps\n\naddInverseRels :: [Relationship] -> [Relationship]\naddInverseRels rels =\n  rels ++\n  [ Relationship ft t isSelf (O2M cons (swap <$> cols)) fTableIsView tableIsView | Relationship t ft isSelf (M2O cons cols) tableIsView fTableIsView <- rels ] ++\n  [ Relationship ft t isSelf (O2O cons (swap <$> cols) (not isParent)) fTableIsView tableIsView | Relationship t ft isSelf (O2O cons cols isParent) tableIsView fTableIsView <- rels ]\n\n-- | Adds a m2m relationship if a table has FKs to two other tables and the FK columns are part of the PK columns\naddM2MRels :: TablesMap -> [Relationship] -> [Relationship]\naddM2MRels tbls rels = rels ++ catMaybes\n  [ let\n      jtCols = S.fromList $ (fst <$> cols) ++ (fst <$> fcols)\n      pkCols = S.fromList $ maybe mempty tablePKCols $ HM.lookup jt1 tbls\n    in if S.isSubsetOf jtCols pkCols\n      then Just $ Relationship t ft (t == ft) (M2M $ Junction jt1 cons1 cons2 (swap <$> cols) (swap <$> fcols)) tblIsView fTblisView\n      else Nothing\n  | Relationship jt1 t  _ (M2O cons1 cols)  _ tblIsView <- rels\n  , Relationship _ ft _ (M2O cons2 fcols) _ fTblisView <- fold $ HM.lookup jt1 indexedRels\n  , cons1 /= cons2]\n  where\n    indexedRels = HM.fromListWith (<>) $ fmap (relTable &&& pure) rels\n\naddViewPrimaryKeys :: TablesMap -> [ViewKeyDependency] -> TablesMap\naddViewPrimaryKeys tabs keyDeps =\n  (\\tbl@Table{tableSchema, tableName, tableIsView}-> if tableIsView\n    then tbl{tablePKCols=findViewPKCols tableSchema tableName}\n    else tbl) <$> tabs\n  where\n    findViewPKCols sch vw =\n      concatMap (\\(ViewKeyDependency _ _ _ _ pkCols) -> takeFirstPK pkCols) $\n      fold $ HM.lookup (PKDep, QualifiedIdentifier sch vw) indexedDeps\n    -- In the case of multiple reference to the same PK (see comment for ViewKeyDependency) we take the first reference available.\n    -- We assume this to be safe to do, because:\n    -- * We don't have any logic that requires the client to name a PK column (compared to the column hints in embedding for FKs),\n    --   so we don't need to know about the other references.\n    -- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc.\n    takeFirstPK = mapMaybe (head . snd)\n    indexedDeps = HM.fromListWith (++) $ fmap ((keyDepType &&& keyDepView) &&& pure) keyDeps\n\nallTables :: Bool -> SQL.Statement AppConfig TablesMap\nallTables = SQL.Statement tablesSqlQuery params decodeTables\n  where\n    params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text\n\n-- | Gets tables with their PK cols\ntablesSqlQuery :: SqlQuery\ntablesSqlQuery =\n  -- the tbl_constraints/key_col_usage CTEs are based on the standard \"information_schema.table_constraints\"/\"information_schema.key_column_usage\" views,\n  -- we cannot use those directly as they include the following privilege filter:\n  -- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));\n  -- on the \"columns\" CTE, left joining on pg_depend and pg_class is used to obtain the sequence name as a column default in case there are GENERATED .. AS IDENTITY,\n  -- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs\n  encodeUtf8 [trimming|\n  WITH\n  $baseTypesCte,\n  columns AS (\n      SELECT\n          c.oid AS relid,\n          a.attname::name AS column_name,\n          d.description AS description,\n          -- typbasetype and typdefaultbin handles `CREATE DOMAIN .. DEFAULT val`,  attidentity/attgenerated handles generated columns, pg_get_expr gets the default of a column\n          CASE\n            WHEN (t.typbasetype != 0) AND (ad.adbin IS NULL) THEN pg_get_expr(t.typdefaultbin, 0)\n            WHEN a.attidentity  = 'd' THEN format('nextval(%L)', seq.objid::regclass)\n            WHEN a.attgenerated = 's' THEN null\n            ELSE pg_get_expr(ad.adbin, ad.adrelid)::text\n          END AS column_default,\n          not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable,\n          CASE\n              WHEN t.typtype = 'd' THEN\n              CASE\n                  WHEN bt.base_namespace = 'pg_catalog'::regnamespace THEN format_type(bt.base_type, NULL::integer)\n                  ELSE format_type(a.atttypid, a.atttypmod)\n              END\n              ELSE\n              CASE\n                  WHEN t.typnamespace = 'pg_catalog'::regnamespace THEN format_type(a.atttypid, NULL::integer)\n                  ELSE format_type(a.atttypid, a.atttypmod)\n              END\n          END::text AS data_type,\n          format_type(a.atttypid, a.atttypmod)::text AS nominal_data_type,\n          information_schema._pg_char_max_length(\n              information_schema._pg_truetypid(a.*, t.*),\n              information_schema._pg_truetypmod(a.*, t.*)\n          )::integer AS character_maximum_length,\n          bt.base_type,\n          a.attnum::integer AS position\n      FROM pg_attribute a\n          LEFT JOIN pg_description AS d\n              ON d.objoid = a.attrelid and d.objsubid = a.attnum and d.classoid = 'pg_class'::regclass\n          LEFT JOIN pg_attrdef ad\n              ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum\n          JOIN pg_class c\n              ON a.attrelid = c.oid\n          JOIN pg_type t\n              ON a.atttypid = t.oid\n          LEFT JOIN base_types bt\n              ON t.oid = bt.oid\n          LEFT JOIN pg_depend seq\n              ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i'\n      WHERE\n          NOT pg_is_other_temp_schema(c.relnamespace)\n          AND a.attnum > 0\n          AND NOT a.attisdropped\n          AND c.relkind in ('r', 'v', 'f', 'm', 'p')\n          AND c.relnamespace = ANY($$1::regnamespace[])\n  ),\n  columns_agg AS (\n    SELECT\n      relid,\n      array_agg(row(\n        column_name,\n        description,\n        is_nullable::boolean,\n        data_type,\n        nominal_data_type,\n        character_maximum_length,\n        column_default,\n        coalesce(\n          (SELECT array_agg(enumlabel ORDER BY enumsortorder) FROM pg_enum WHERE enumtypid = base_type),\n          '{}'\n        )\n      ) order by position) as columns\n    FROM columns\n    GROUP BY relid\n  ),\n  tbl_pk_cols AS (\n    SELECT\n      r.oid AS relid,\n      array_agg(a.attname ORDER BY a.attname) AS pk_cols\n    FROM pg_class r\n    JOIN pg_constraint c\n      ON r.oid = c.conrelid\n    JOIN pg_attribute a\n      ON a.attrelid = r.oid AND a.attnum = ANY (c.conkey)\n    WHERE\n      c.contype in ('p')\n      AND r.relkind IN ('r', 'p')\n      AND r.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)\n      AND NOT pg_is_other_temp_schema(r.relnamespace)\n      AND NOT a.attisdropped\n    GROUP BY r.oid\n  )\n  SELECT\n    n.nspname AS table_schema,\n    c.relname AS table_name,\n    d.description AS table_description,\n    c.relkind IN ('v','m') as is_view,\n    (\n      c.relkind IN ('r','p')\n      OR (\n        c.relkind in ('v','f')\n        -- The function `pg_relation_is_updateable` returns a bitmask where 8\n        -- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.\n        -- it's possible to insert into the relation.\n        AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 8) = 8\n      )\n    ) AS insertable,\n    (\n      c.relkind IN ('r','p')\n      OR (\n        c.relkind in ('v','f')\n        -- CMD_UPDATE\n        AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 4) = 4\n      )\n    ) AS updatable,\n    (\n      c.relkind IN ('r','p')\n      OR (\n        c.relkind in ('v','f')\n        -- CMD_DELETE\n        AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 16) = 16\n      )\n    ) AS deletable,\n    coalesce(tpks.pk_cols, '{}') as pk_cols,\n    coalesce(cols_agg.columns, '{}') as columns\n  FROM pg_class c\n  JOIN pg_namespace n ON n.oid = c.relnamespace\n  LEFT JOIN pg_description d on d.objoid = c.oid and d.objsubid = 0 and d.classoid = 'pg_class'::regclass\n  LEFT JOIN tbl_pk_cols tpks ON c.oid = tpks.relid\n  LEFT JOIN columns_agg cols_agg ON c.oid = cols_agg.relid\n  WHERE c.relkind IN ('v','r','m','f','p')\n  AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)\n  AND not c.relispartition\n  ORDER BY table_schema, table_name|]\n\n-- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's\nallM2OandO2ORels :: Bool -> SQL.Statement () [Relationship]\nallM2OandO2ORels =\n  SQL.Statement sql HE.noParams decodeRels\n where\n  -- We use jsonb_agg for comparing the uniques/pks instead of array_agg to avoid the ERROR:  cannot accumulate arrays of different dimensionality\n  sql = encodeUtf8 [trimming|\n    WITH\n    pks_uniques_cols AS (\n      SELECT\n        conrelid,\n        array_agg(key order by key) as cols\n      FROM pg_constraint,\n      LATERAL unnest(conkey) AS _(key)\n      WHERE\n        contype IN ('p', 'u')\n        AND connamespace <> 'pg_catalog'::regnamespace\n      GROUP BY oid, conrelid\n    )\n    SELECT\n      ns1.nspname AS table_schema,\n      tab.relname AS table_name,\n      ns2.nspname AS foreign_table_schema,\n      other.relname AS foreign_table_name,\n      traint.conrelid = traint.confrelid AS is_self,\n      traint.conname  AS constraint_name,\n      column_info.cols_and_fcols,\n      (column_info.cols IN (SELECT cols FROM pks_uniques_cols WHERE conrelid = traint.conrelid)) AS one_to_one\n    FROM pg_constraint traint\n    JOIN LATERAL (\n      SELECT\n        array_agg(row(cols.attname, refs.attname) order by ord) AS cols_and_fcols,\n        array_agg(cols.attnum order by cols.attnum) AS cols\n      FROM unnest(traint.conkey, traint.confkey) WITH ORDINALITY AS _(col, ref, ord)\n      JOIN pg_attribute cols ON cols.attrelid = traint.conrelid AND cols.attnum = col\n      JOIN pg_attribute refs ON refs.attrelid = traint.confrelid AND refs.attnum = ref\n    ) AS column_info ON TRUE\n    JOIN pg_namespace ns1 ON ns1.oid = traint.connamespace\n    JOIN pg_class tab ON tab.oid = traint.conrelid\n    JOIN pg_class other ON other.oid = traint.confrelid\n    JOIN pg_namespace ns2 ON ns2.oid = other.relnamespace\n    WHERE traint.contype = 'f'\n    AND traint.conparentid = 0\n    ORDER BY traint.conrelid, traint.conname|]\n\nallComputedRels :: Bool -> SQL.Statement () [Relationship]\nallComputedRels =\n  SQL.Statement sql HE.noParams (HD.rowList cRelRow)\n where\n  sql = encodeUtf8 [trimming|\n    with\n    all_relations as (\n      select reltype\n      from pg_class\n      where relkind in ('v','r','m','f','p')\n    ),\n    computed_rels as (\n      select\n        (parse_ident(p.pronamespace::regnamespace::text))[1] as schema,\n        p.proname::text                  as name,\n        arg_schema.nspname::text         as rel_table_schema,\n        arg_name.typname::text           as rel_table_name,\n        ret_schema.nspname::text         as rel_ftable_schema,\n        ret_name.typname::text           as rel_ftable_name,\n        not p.proretset or p.prorows = 1 as single_row\n      from pg_proc p\n        join pg_type      arg_name   on arg_name.oid = p.proargtypes[0]\n        join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace\n        join pg_type      ret_name   on ret_name.oid = p.prorettype\n        join pg_namespace ret_schema on ret_schema.oid = ret_name.typnamespace\n      where\n        p.pronargs = 1\n        and p.proargtypes[0] in (select reltype from all_relations)\n        and p.prorettype in (select reltype from all_relations)\n    )\n    select\n      *,\n      row(rel_table_schema, rel_table_name) = row(rel_ftable_schema, rel_ftable_name) as is_self\n    from computed_rels;\n  |]\n\n  cRelRow =\n    ComputedRelationship <$>\n    (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>\n    (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>\n    (QualifiedIdentifier <$> column HD.text <*> column HD.text) <*>\n    pure (QualifiedIdentifier mempty mempty) <*>\n    column HD.bool <*>\n    column HD.bool\n\n-- | Returns all the views' primary keys and foreign keys dependencies\nallViewsKeyDependencies :: Bool -> SQL.Statement AppConfig [ViewKeyDependency]\nallViewsKeyDependencies =\n  SQL.Statement sql params decodeViewKeyDeps\n  -- query explanation at:\n  --  * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19\n  --  * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089\n  where\n    params =\n      (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>\n      (map escapeIdent . toList . configDbExtraSearchPath >$< arrayParam HE.text)\n    sql = encodeUtf8 [trimming|\n      with recursive\n      pks_fks as (\n        -- pk + fk referencing col\n        select\n          contype::text as contype,\n          conname,\n          array_length(conkey, 1) as ncol,\n          conrelid as resorigtbl,\n          col as resorigcol,\n          ord\n        from pg_constraint\n        left join lateral unnest(conkey) with ordinality as _(col, ord) on true\n        where contype IN ('p', 'f')\n        union\n        -- fk referenced col\n        select\n          concat(contype, '_ref') as contype,\n          conname,\n          array_length(confkey, 1) as ncol,\n          confrelid,\n          col,\n          ord\n        from pg_constraint\n        left join lateral unnest(confkey) with ordinality as _(col, ord) on true\n        where contype='f'\n      ),\n      views as (\n        select\n          c.oid          as view_id,\n          c.relnamespace as view_schema_id,\n          n.nspname      as view_schema,\n          c.relname      as view_name,\n          r.ev_action    as view_definition\n        from pg_class c\n        join pg_namespace n on n.oid = c.relnamespace\n        join pg_rewrite r on r.ev_class = c.oid\n        where c.relkind in ('v', 'm') and c.relnamespace = ANY($$1::regnamespace[] || $$2::regnamespace[])\n      ),\n      transform_json as (\n        select\n          view_id, view_schema_id, view_schema, view_name,\n          -- the following formatting is without indentation on purpose\n          -- to allow simple diffs, with less whitespace noise\n          replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            regexp_replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n            replace(\n              view_definition::text,\n            -- This conversion to json is heavily optimized for performance.\n            -- The general idea is to use as few regexp_replace() calls as possible.\n            -- Simple replace() is a lot faster, so we jump through some hoops\n            -- to be able to use regexp_replace() only once.\n            -- This has been tested against a huge schema with 250+ different views.\n            -- The unit tests do NOT reflect all possible inputs. Be careful when changing this!\n            -- -----------------------------------------------\n            -- pattern           | replacement         | flags\n            -- -----------------------------------------------\n            -- `<>` in pg_node_tree is the same as `null` in JSON, but due to very poor performance of json_typeof\n            -- we need to make this an empty array here to prevent json_array_elements from throwing an error\n            -- when the targetList is null.\n            -- We'll need to put it first, to make the node protection below work for node lists that start with\n            -- null: `(<> ...`, too. This is the case for coldefexprs, when the first column does not have a default value.\n               '<>'              , '()'\n            -- `,` is not part of the pg_node_tree format, but used in the regex.\n            -- This removes all `,` that might be part of column names.\n            ), ','               , ''\n            -- The same applies for `{` and `}`, although those are used a lot in pg_node_tree.\n            -- We remove the escaped ones, which might be part of column names again.\n            ), E'\\\\{'            , ''\n            ), E'\\\\}'            , ''\n            -- The fields we need are formatted as json manually to protect them from the regex.\n            ), ' :targetList '   , ',\"targetList\":'\n            ), ' :resno '        , ',\"resno\":'\n            ), ' :resorigtbl '   , ',\"resorigtbl\":'\n            ), ' :resorigcol '   , ',\"resorigcol\":'\n            -- Make the regex also match the node type, e.g. `{QUERY ...`, to remove it in one pass.\n            ), '{'               , '{ :'\n            -- Protect node lists, which start with `({` or `((` from the greedy regex.\n            -- The extra `{` is removed again later.\n            ), '(('              , '{(('\n            ), '({'              , '{({'\n            -- This regex removes all unused fields to avoid the need to format all of them correctly.\n            -- This leads to a smaller json result as well.\n            -- Removal stops at `,` for used fields (see above) and `}` for the end of the current node.\n            -- Nesting can't be parsed correctly with a regex, so we stop at `{` as well and\n            -- add an empty key for the following node.\n            ), ' :[^}{,]+'       , ',\"\":'              , 'g'\n            -- For performance, the regex also added those empty keys when hitting a `,` or `}`.\n            -- Those are removed next.\n            ), ',\"\":}'           , '}'\n            ), ',\"\":,'           , ','\n            -- This reverses the \"node list protection\" from above.\n            ), '{('              , '('\n            -- Every key above has been added with a `,` so far. The first key in an object doesn't need it.\n            ), '{,'              , '{'\n            -- pg_node_tree has `()` around lists, but JSON uses `[]`\n            ), '('               , '['\n            ), ')'               , ']'\n            -- pg_node_tree has ` ` between list items, but JSON uses `,`\n            ), ' '             , ','\n          )::json as view_definition\n        from views\n      ),\n      target_entries as(\n        select\n          view_id, view_schema_id, view_schema, view_name,\n          json_array_elements(view_definition->0->'targetList') as entry\n        from transform_json\n      ),\n      results as(\n        select\n          view_id, view_schema_id, view_schema, view_name,\n          (entry->>'resno')::int as view_column,\n          (entry->>'resorigtbl')::oid as resorigtbl,\n          (entry->>'resorigcol')::int as resorigcol\n        from target_entries\n      ),\n      -- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE\n      -- Can be replaced with CYCLE clause once PG v13 is EOL.\n      recursion(view_id, view_schema_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(\n        select\n          r.*,\n          false,\n          ARRAY[resorigtbl]\n        from results r\n        where view_schema_id = ANY ($$1::regnamespace[])\n        union all\n        select\n          view.view_id,\n          view.view_schema_id,\n          view.view_schema,\n          view.view_name,\n          view.view_column,\n          tab.resorigtbl,\n          tab.resorigcol,\n          tab.resorigtbl = ANY(path),\n          path || tab.resorigtbl\n        from recursion view\n        join results tab on view.resorigtbl=tab.view_id and view.resorigcol=tab.view_column\n        where not is_cycle\n      ),\n      repeated_references as(\n        select\n          view_id,\n          view_schema,\n          view_name,\n          resorigtbl,\n          resorigcol,\n          array_agg(attname) as view_columns\n        from recursion\n        join pg_attribute vcol on vcol.attrelid = view_id and vcol.attnum = view_column\n        group by\n          view_id,\n          view_schema,\n          view_name,\n          resorigtbl,\n          resorigcol\n      )\n      select\n        sch.nspname as table_schema,\n        tbl.relname as table_name,\n        rep.view_schema,\n        rep.view_name,\n        pks_fks.conname as constraint_name,\n        pks_fks.contype as constraint_type,\n        array_agg(row(col.attname, view_columns) order by pks_fks.ord) as column_dependencies\n      from repeated_references rep\n      join pks_fks using (resorigtbl, resorigcol)\n      join pg_class tbl on tbl.oid = rep.resorigtbl\n      join pg_attribute col on col.attrelid = tbl.oid and col.attnum = rep.resorigcol\n      join pg_namespace sch on sch.oid = tbl.relnamespace\n      group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conname, pks_fks.contype, pks_fks.ncol\n      -- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs\n      having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)\n      |]\n\ninitialMediaHandlers :: MediaHandlerMap\ninitialMediaHandlers =\n  HM.insert (RelAnyElement, MediaType.MTAny            ) (BuiltinOvAggJson,    MediaType.MTApplicationJSON) $\n  HM.insert (RelAnyElement, MediaType.MTApplicationJSON) (BuiltinOvAggJson,    MediaType.MTApplicationJSON) $\n  HM.insert (RelAnyElement, MediaType.MTTextCSV        ) (BuiltinOvAggCsv,     MediaType.MTTextCSV) $\n  HM.insert (RelAnyElement, MediaType.MTGeoJSON        ) (BuiltinOvAggGeoJson, MediaType.MTGeoJSON)\n  HM.empty\n\nmediaHandlers :: Bool -> SQL.Statement AppConfig MediaHandlerMap\nmediaHandlers =\n  SQL.Statement sql params decodeMediaHandlers\n  where\n    params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text\n    sql = encodeUtf8 [trimming|\n      with\n      all_relations as (\n        select reltype\n        from pg_class\n        where relkind in ('v','r','m','f','p')\n        union\n        select oid\n        from pg_type\n        where typname = 'anyelement'\n      ),\n      media_types as (\n          SELECT\n            t.oid,\n            lower(t.typname) as typname,\n            t.typnamespace,\n            case t.typname\n              when '*/*' then 'application/octet-stream'\n              else t.typname\n            end as resolved_media_type\n          FROM pg_type t\n          JOIN pg_type b ON t.typbasetype = b.oid\n          WHERE\n            t.typbasetype <> 0 and\n            (t.typname ~* '^[A-Za-z0-9.-]+/[A-Za-z0-9.\\+-]+$$' or t.typname = '*/*')\n      )\n      select\n        proc_schema.nspname           as handler_schema,\n        proc.proname                  as handler_name,\n        arg_schema.nspname::text      as target_schema,\n        arg_name.typname::text        as target_name,\n        media_types.typname           as media_type,\n        media_types.resolved_media_type\n      from media_types\n        join pg_proc      proc         on proc.prorettype = media_types.oid\n        join pg_namespace proc_schema  on proc_schema.oid = proc.pronamespace\n        join pg_aggregate agg          on agg.aggfnoid = proc.oid\n        join pg_type      arg_name     on arg_name.oid = proc.proargtypes[0]\n        join pg_namespace arg_schema   on arg_schema.oid = arg_name.typnamespace\n      where\n        proc.pronamespace = ANY($$1::regnamespace[]) and\n        proc.pronargs = 1 and\n        arg_name.oid in (select reltype from all_relations)\n      union\n      select\n          typ_sch.nspname as handler_schema,\n          mtype.typname   as handler_name,\n          pro_sch.nspname as target_schema,\n          proname         as target_name,\n          mtype.typname   as media_type,\n          mtype.resolved_media_type\n      from pg_proc proc\n        join pg_namespace pro_sch on pro_sch.oid = proc.pronamespace\n        join media_types mtype on proc.prorettype = mtype.oid\n        join pg_namespace typ_sch     on typ_sch.oid = mtype.typnamespace\n      where\n        proc.pronamespace = ANY($$1::regnamespace[]) and NOT proretset\n        and prokind = 'f'|]\n\ndecodeMediaHandlers :: HD.Result MediaHandlerMap\ndecodeMediaHandlers =\n  HM.fromList . fmap (\\(x, y, z, w) ->\n    let rel = if isAnyElement y then RelAnyElement else RelId y\n    in ((rel, z), (CustomFunc x rel, w)) ) <$> HD.rowList caggRow\n  where\n    caggRow = (,,,)\n              <$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)\n              <*> (QualifiedIdentifier <$> column HD.text <*> column HD.text)\n              <*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)\n              <*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)\n\ntimezones :: Bool -> SQL.Statement () TimezoneNames\ntimezones = SQL.Statement sql HE.noParams decodeTimezones\n  where\n    sql = \"SELECT name FROM pg_timezone_names\"\n    decodeTimezones :: HD.Result TimezoneNames\n    decodeTimezones = S.fromList <$> HD.rowList (column HD.text)\n\nparam :: HE.Value a -> HE.Params a\nparam = HE.param . HE.nonNullable\n\narrayParam :: HE.Value a -> HE.Params [a]\narrayParam = param . HE.foldableArray . HE.nonNullable\n\ncompositeArrayColumn :: HD.Composite a -> HD.Row [a]\ncompositeArrayColumn = arrayColumn . HD.composite\n\ncompositeField :: HD.Value a -> HD.Composite a\ncompositeField = HD.field . HD.nonNullable\n\nnullableCompositeField :: HD.Value a -> HD.Composite (Maybe a)\nnullableCompositeField = HD.field . HD.nullable\n\ncompositeFieldArray :: HD.Value a -> HD.Composite [a]\ncompositeFieldArray = HD.field . HD.nonNullable . HD.listArray . HD.nonNullable\n\ncolumn :: HD.Value a -> HD.Row a\ncolumn = HD.column . HD.nonNullable\n\nnullableColumn :: HD.Value a -> HD.Row (Maybe a)\nnullableColumn = HD.column . HD.nullable\n\narrayColumn :: HD.Value a -> HD.Row [a]\narrayColumn = column . HD.listArray . HD.nonNullable\n"
  },
  {
    "path": "src/PostgREST/TimeIt.hs",
    "content": "module PostgREST.TimeIt\n  ( timeItT\n  ) where\n\nimport GHC.Clock\nimport Protolude\n\n{-\n - The signature is the same as https://hackage.haskell.org/package/timeit-2.0/docs/src/System-TimeIt.html#timeIt,\n - we vendor this functionality because it gave errors as shown on https://github.com/PostgREST/postgrest/issues/4522 plus\n - the function is small enough. This vendored function is different in that the result is in milliseconds.\n -}\ntimeItT :: MonadIO m => m a -> m (Double, a)\ntimeItT p = do\n  s <- liftIO getMonotonicTime\n  x <- p\n  e <- liftIO getMonotonicTime\n  let time = (e - s) * 1000\n  return (time, x)\n\n"
  },
  {
    "path": "src/PostgREST/Unix.hs",
    "content": "{-# LANGUAGE CPP #-}\n\nmodule PostgREST.Unix\n  ( installSignalHandlers\n  , createAndBindDomainSocket\n  ) where\n\n#ifndef mingw32_HOST_OS\nimport qualified System.Posix.Signals as Signals\n#endif\nimport System.Posix.Types       (FileMode)\nimport System.PosixCompat.Files (setFileMode)\n\nimport           Data.String      (String)\nimport qualified Network.Socket   as NS\nimport           Protolude\nimport           System.Directory (removeFile)\nimport           System.IO.Error  (isDoesNotExistError)\n\n-- | Set signal handlers, only for systems with signals\ninstallSignalHandlers :: ThreadId -> IO () -> IO () -> IO ()\n#ifndef mingw32_HOST_OS\ninstallSignalHandlers tid usr1 usr2 = do\n  let interrupt = throwTo tid UserInterrupt\n  install Signals.sigINT interrupt\n  install Signals.sigTERM interrupt\n  install Signals.sigUSR1 usr1\n  install Signals.sigUSR2 usr2\n  where\n    install signal handler =\n      void $ Signals.installHandler signal (Signals.Catch handler) Nothing\n#else\ninstallSignalHandlers _ _ _ = pass\n#endif\n\n-- | Create a unix domain socket and bind it to the given path.\n-- | The socket file will be deleted if it already exists.\ncreateAndBindDomainSocket :: String -> FileMode -> IO NS.Socket\ncreateAndBindDomainSocket path mode = do\n  unless NS.isUnixDomainSocketAvailable $\n    panic \"Cannot run with unix socket on non-unix platforms. Consider deleting the `server-unix-socket` config entry in order to continue.\"\n  deleteSocketFileIfExist path\n  sock <- NS.socket NS.AF_UNIX NS.Stream NS.defaultProtocol\n  NS.bind sock $ NS.SockAddrUnix path\n  NS.listen sock (max 2048 NS.maxListenQueue)\n  setFileMode path mode\n  return sock\n  where\n    deleteSocketFileIfExist path' =\n      removeFile path' `catch` handleDoesNotExist\n    handleDoesNotExist e\n      | isDoesNotExistError e = return ()\n      | otherwise = throwIO e\n"
  },
  {
    "path": "src/PostgREST/Version.hs",
    "content": "{-# LANGUAGE CPP #-}\nmodule PostgREST.Version\n  ( docsVersion\n  , prettyVersion\n  ) where\n\nimport qualified Data.Text as T\n\nimport Protolude\n\nversion :: [Text]\nversion = T.splitOn \".\" VERSION_postgrest\n\n-- | User friendly version number such as '14.0'.\n-- Pre-release versions are tagged as such, e.g., '15 (pre-release)'.\nprettyVersion :: ByteString\nprettyVersion =\n  (encodeUtf8 . T.intercalate \".\" $ take 2 version) <> preRelease\n  where\n    preRelease = if isPreRelease then \" (pre-release)\" else mempty\n\n\n-- | Version number used in docs.\n-- Pre-release versions link to the latest docs\n-- Uses only the first component of the version. Example: 'v1'\ndocsVersion :: Text\ndocsVersion\n  | isPreRelease = \"latest\"\n  | otherwise    =  \"v\" <> T.intercalate \".\" (take 1 version)\n\n\n-- | Versions with one components (e.g., '15') are treated as pre-releases.\nisPreRelease :: Bool\nisPreRelease =\n  length version == 1\n"
  },
  {
    "path": "stack.yaml",
    "content": "resolver: lts-22.44 # 2025-05-02, GHC 9.6.7\n\nnix:\n  packages:\n    - libpq\n    - pkg-config\n    - zlib\n  # disable pure by default so that the test environment can be passed\n  pure: false\n\nextra-deps:\n  - configurator-pg-0.2.11\n  - fuzzyset-0.2.4\n  - hasql-pool-1.0.1\n  - jose-jwt-0.10.0\n  - postgresql-libpq-0.10.1.0\n  - streaming-commons-0.2.3.1\n\n  # fix build with GCC 15-ish; https://github.com/gregorycollins/hashtables/issues/98 for details\n  - hashtables-1.4.2@sha256:4940cab94a15d469845ccf5225f9cb3d354c15e8127ebb58425c8b681f7721d9,10386\n"
  },
  {
    "path": "test/coverage.overlay",
    "content": ""
  },
  {
    "path": "test/doc/Main.hs",
    "content": "module Main (main) where\n\nimport Test.DocTest (doctest)\n\nimport Protolude\n\n\nmain :: IO ()\nmain =\n  doctest\n    [ \"-XOverloadedStrings\"\n    , \"-XNoImplicitPrelude\"\n    , \"-XStandaloneDeriving\"\n    , \"-XDuplicateRecordFields\"\n    , \"-isrc\"\n    , \"src/PostgREST/ApiRequest/Preferences.hs\"\n    , \"src/PostgREST/ApiRequest/QueryParams.hs\"\n    , \"src/PostgREST/Config.hs\"\n    , \"src/PostgREST/Error.hs\"\n    , \"src/PostgREST/MediaType.hs\"\n    , \"src/PostgREST/Network.hs\"\n    , \"src/PostgREST/Plan.hs\"\n    , \"src/PostgREST/Query/SqlFragment.hs\"\n    , \"src/PostgREST/Response.hs\"\n    , \"src/PostgREST/Response/Performance.hs\"\n    , \"src/PostgREST/SchemaCache/Identifiers.hs\"\n    ]\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbMediaHandlers].yaml",
    "content": "- - - tag: RelAnyElement\n    - tag: MTTextCSV\n  - - tag: BuiltinOvAggCsv\n    - tag: MTTextCSV\n\n- - - tag: RelAnyElement\n    - tag: MTGeoJSON\n  - - tag: BuiltinOvAggGeoJson\n    - tag: MTGeoJSON\n\n- - - tag: RelAnyElement\n    - tag: MTApplicationJSON\n  - - tag: BuiltinOvAggJson\n    - tag: MTApplicationJSON\n\n- - - tag: RelAnyElement\n    - tag: MTAny\n  - - tag: BuiltinOvAggJson\n    - tag: MTApplicationJSON\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRelationships].yaml",
    "content": "- - - qiName: directors\n      qiSchema: public\n    - public\n  - - relCardinality:\n        relColumns:\n        - - id\n          - director_id\n        relCons: fk_director\n        tag: O2M\n      relFTableIsView: false\n      relForeignTable:\n        qiName: films\n        qiSchema: public\n      relIsSelf: false\n      relTable:\n        qiName: directors\n        qiSchema: public\n      relTableIsView: false\n      tag: Relationship\n\n- - - qiName: films\n      qiSchema: public\n    - public\n  - - relCardinality:\n        relColumns:\n        - - director_id\n          - id\n        relCons: fk_director\n        tag: M2O\n      relFTableIsView: false\n      relForeignTable:\n        qiName: directors\n        qiSchema: public\n      relIsSelf: false\n      relTable:\n        qiName: films\n        qiSchema: public\n      relTableIsView: false\n      tag: Relationship\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRepresentations].yaml",
    "content": "[]\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRoutines].yaml",
    "content": "- - qiName: rpc_with_two_hoisted\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - statement_timeout\n        - 10s\n      pdHasVariadic: false\n      pdName: rpc_with_two_hoisted\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n          - qiName: items\n            qiSchema: public\n          - false\n          tag: Composite\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: terminate_pgrst\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: terminate_pgrst\n      pdParams:\n      - ppName: appname\n        ppReq: true\n        ppType: text\n        ppTypeMaxLength: text\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: record\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: SetOf\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: default_isolation_level\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: default_isolation_level\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: change_db_schema_and_full_reload\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: change_db_schema_and_full_reload\n      pdParams:\n      - ppName: schemas\n        ppReq: true\n        ppType: text\n        ppTypeMaxLength: text\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: set_statement_timeout\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: set_statement_timeout\n      pdParams:\n      - ppName: role\n        ppReq: true\n        ppType: text\n        ppTypeMaxLength: text\n        ppVar: false\n      - ppName: milliseconds\n        ppReq: true\n        ppType: integer\n        ppTypeMaxLength: integer\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: notify_pgrst\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: notify_pgrst\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: migrate_function\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: migrate_function\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: change_role_statement_timeout\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: change_role_statement_timeout\n      pdParams:\n      - ppName: timeout\n        ppReq: true\n        ppType: text\n        ppTypeMaxLength: text\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: reset_max_rows_config\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: reset_max_rows_config\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: reset_invalid_role_claim_key\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: reset_invalid_role_claim_key\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: custom_vary_hdr\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: custom_vary_hdr\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: get_postgres_version\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: get_postgres_version\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: int4\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: do_nothing\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: do_nothing\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: get_guc_value\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: get_guc_value\n      pdParams:\n      - ppName: name\n        ppReq: true\n        ppType: text\n        ppTypeMaxLength: text\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: one_sec_timeout\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - statement_timeout\n        - 1s\n      pdHasVariadic: false\n      pdName: one_sec_timeout\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: reload_pgrst_config\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: reload_pgrst_config\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: rpc_with_one_hoisted\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - statement_timeout\n        - 7s\n      pdHasVariadic: false\n      pdName: rpc_with_one_hoisted\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n          - qiName: items\n            qiSchema: public\n          - false\n          tag: Composite\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: hello\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: hello\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: change_db_schemas_config\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: change_db_schemas_config\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: sleep\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: sleep\n      pdParams:\n      - ppName: seconds\n        ppReq: true\n        ppType: double precision\n        ppTypeMaxLength: double precision\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: serializable_isolation_level\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - default_transaction_isolation\n        - serializable\n      pdHasVariadic: false\n      pdName: serializable_isolation_level\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: drop_change_cats\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: drop_change_cats\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: change_max_rows_config\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: change_max_rows_config\n      pdParams:\n      - ppName: val\n        ppReq: true\n        ppType: integer\n        ppTypeMaxLength: integer\n        ppVar: false\n      - ppName: notify\n        ppReq: false\n        ppType: boolean\n        ppTypeMaxLength: boolean\n        ppVar: false\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: invalid_role_claim_key_reload\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: invalid_role_claim_key_reload\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: reset_db_schemas_config\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: reset_db_schemas_config\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: root\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: root\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: json\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: 'true'\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: 'true'\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: bool\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: create_function\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: create_function\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: get_pgrst_version\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: get_pgrst_version\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: uses_prepared_statements\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: uses_prepared_statements\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: bool\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: repeatable_read_isolation_level\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - default_transaction_isolation\n        - REPEATABLE READ\n      pdHasVariadic: false\n      pdName: repeatable_read_isolation_level\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: text\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: notify_do_nothing\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: notify_do_nothing\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: rpc_work_mem\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings: []\n      pdHasVariadic: false\n      pdName: rpc_work_mem\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n          - qiName: items\n            qiSchema: public\n          - false\n          tag: Composite\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n\n- - qiName: four_sec_timeout\n    qiSchema: public\n  - - pdDescription: null\n      pdFuncSettings:\n      - - statement_timeout\n        - 4s\n      pdHasVariadic: false\n      pdName: four_sec_timeout\n      pdParams: []\n      pdReturnType:\n        contents:\n          contents:\n            qiName: void\n            qiSchema: pg_catalog\n          tag: Scalar\n        tag: Single\n      pdSchema: public\n      pdVolatility: Volatile\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTables].yaml",
    "content": "- - qiName: authors_only\n    qiSchema: public\n  - tableColumns: {}\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: authors_only\n    tablePKCols: []\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: cats\n    qiSchema: public\n  - tableColumns:\n      id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: id\n        colNominalType: uuid\n        colNullable: false\n        colType: uuid\n      name:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: name\n        colNominalType: text\n        colNullable: true\n        colType: text\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: cats\n    tablePKCols:\n    - id\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: items_w_isolation_level\n    qiSchema: public\n  - tableColumns:\n      id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: id\n        colNominalType: integer\n        colNullable: true\n        colType: integer\n      isolation_level:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: isolation_level\n        colNominalType: text\n        colNullable: true\n        colType: text\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: true\n    tableName: items_w_isolation_level\n    tablePKCols: []\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: directors\n    qiSchema: public\n  - tableColumns:\n      id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: id\n        colNominalType: integer\n        colNullable: false\n        colType: integer\n      name:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: name\n        colNominalType: text\n        colNullable: true\n        colType: text\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: directors\n    tablePKCols:\n    - id\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: projects\n    qiSchema: public\n  - tableColumns: {}\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: projects\n    tablePKCols: []\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: infinite_recursion\n    qiSchema: public\n  - tableColumns: {}\n    tableDeletable: false\n    tableDescription: null\n    tableInsertable: false\n    tableIsView: true\n    tableName: infinite_recursion\n    tablePKCols: []\n    tableSchema: public\n    tableUpdatable: false\n\n- - qiName: films\n    qiSchema: public\n  - tableColumns:\n      director_id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: director_id\n        colNominalType: integer\n        colNullable: true\n        colType: integer\n      id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: id\n        colNominalType: integer\n        colNullable: false\n        colType: integer\n      title:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: title\n        colNominalType: text\n        colNullable: true\n        colType: text\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: films\n    tablePKCols:\n    - id\n    tableSchema: public\n    tableUpdatable: true\n\n- - qiName: items\n    qiSchema: public\n  - tableColumns:\n      id:\n        colDefault: null\n        colDescription: null\n        colEnum: []\n        colMaxLen: null\n        colName: id\n        colNominalType: integer\n        colNullable: true\n        colType: integer\n    tableDeletable: true\n    tableDescription: null\n    tableInsertable: true\n    tableIsView: false\n    tableName: items\n    tablePKCols: []\n    tableSchema: public\n    tableUpdatable: true\n"
  },
  {
    "path": "test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTimezones].yaml",
    "content": "- Africa/Abidjan\n- Africa/Accra\n- Africa/Addis_Ababa\n- Africa/Algiers\n- Africa/Asmara\n- Africa/Asmera\n- Africa/Bamako\n- Africa/Bangui\n- Africa/Banjul\n- Africa/Bissau\n- Africa/Blantyre\n- Africa/Brazzaville\n- Africa/Bujumbura\n- Africa/Cairo\n- Africa/Casablanca\n- Africa/Ceuta\n- Africa/Conakry\n- Africa/Dakar\n- Africa/Dar_es_Salaam\n- Africa/Djibouti\n- Africa/Douala\n- Africa/El_Aaiun\n- Africa/Freetown\n- Africa/Gaborone\n- Africa/Harare\n- Africa/Johannesburg\n- Africa/Juba\n- Africa/Kampala\n- Africa/Khartoum\n- Africa/Kigali\n- Africa/Kinshasa\n- Africa/Lagos\n- Africa/Libreville\n- Africa/Lome\n- Africa/Luanda\n- Africa/Lubumbashi\n- Africa/Lusaka\n- Africa/Malabo\n- Africa/Maputo\n- Africa/Maseru\n- Africa/Mbabane\n- Africa/Mogadishu\n- Africa/Monrovia\n- Africa/Nairobi\n- Africa/Ndjamena\n- Africa/Niamey\n- Africa/Nouakchott\n- Africa/Ouagadougou\n- Africa/Porto-Novo\n- Africa/Sao_Tome\n- Africa/Timbuktu\n- Africa/Tripoli\n- Africa/Tunis\n- Africa/Windhoek\n- America/Adak\n- America/Anchorage\n- America/Anguilla\n- America/Antigua\n- America/Araguaina\n- America/Argentina/Buenos_Aires\n- America/Argentina/Catamarca\n- America/Argentina/ComodRivadavia\n- America/Argentina/Cordoba\n- America/Argentina/Jujuy\n- America/Argentina/La_Rioja\n- America/Argentina/Mendoza\n- America/Argentina/Rio_Gallegos\n- America/Argentina/Salta\n- America/Argentina/San_Juan\n- America/Argentina/San_Luis\n- America/Argentina/Tucuman\n- America/Argentina/Ushuaia\n- America/Aruba\n- America/Asuncion\n- America/Atikokan\n- America/Atka\n- America/Bahia\n- America/Bahia_Banderas\n- America/Barbados\n- America/Belem\n- America/Belize\n- America/Blanc-Sablon\n- America/Boa_Vista\n- America/Bogota\n- America/Boise\n- America/Buenos_Aires\n- America/Cambridge_Bay\n- America/Campo_Grande\n- America/Cancun\n- America/Caracas\n- America/Catamarca\n- America/Cayenne\n- America/Cayman\n- America/Chicago\n- America/Chihuahua\n- America/Ciudad_Juarez\n- America/Coral_Harbour\n- America/Cordoba\n- America/Costa_Rica\n- America/Coyhaique\n- America/Creston\n- America/Cuiaba\n- America/Curacao\n- America/Danmarkshavn\n- America/Dawson\n- America/Dawson_Creek\n- America/Denver\n- America/Detroit\n- America/Dominica\n- America/Edmonton\n- America/Eirunepe\n- America/El_Salvador\n- America/Ensenada\n- America/Fort_Nelson\n- America/Fort_Wayne\n- America/Fortaleza\n- America/Glace_Bay\n- America/Godthab\n- America/Goose_Bay\n- America/Grand_Turk\n- America/Grenada\n- America/Guadeloupe\n- America/Guatemala\n- America/Guayaquil\n- America/Guyana\n- America/Halifax\n- America/Havana\n- America/Hermosillo\n- America/Indiana/Indianapolis\n- America/Indiana/Knox\n- America/Indiana/Marengo\n- America/Indiana/Petersburg\n- America/Indiana/Tell_City\n- America/Indiana/Vevay\n- America/Indiana/Vincennes\n- America/Indiana/Winamac\n- America/Indianapolis\n- America/Inuvik\n- America/Iqaluit\n- America/Jamaica\n- America/Jujuy\n- America/Juneau\n- America/Kentucky/Louisville\n- America/Kentucky/Monticello\n- America/Knox_IN\n- America/Kralendijk\n- America/La_Paz\n- America/Lima\n- America/Los_Angeles\n- America/Louisville\n- America/Lower_Princes\n- America/Maceio\n- America/Managua\n- America/Manaus\n- America/Marigot\n- America/Martinique\n- America/Matamoros\n- America/Mazatlan\n- America/Mendoza\n- America/Menominee\n- America/Merida\n- America/Metlakatla\n- America/Mexico_City\n- America/Miquelon\n- America/Moncton\n- America/Monterrey\n- America/Montevideo\n- America/Montreal\n- America/Montserrat\n- America/Nassau\n- America/New_York\n- America/Nipigon\n- America/Nome\n- America/Noronha\n- America/North_Dakota/Beulah\n- America/North_Dakota/Center\n- America/North_Dakota/New_Salem\n- America/Nuuk\n- America/Ojinaga\n- America/Panama\n- America/Pangnirtung\n- America/Paramaribo\n- America/Phoenix\n- America/Port-au-Prince\n- America/Port_of_Spain\n- America/Porto_Acre\n- America/Porto_Velho\n- America/Puerto_Rico\n- America/Punta_Arenas\n- America/Rainy_River\n- America/Rankin_Inlet\n- America/Recife\n- America/Regina\n- America/Resolute\n- America/Rio_Branco\n- America/Rosario\n- America/Santa_Isabel\n- America/Santarem\n- America/Santiago\n- America/Santo_Domingo\n- America/Sao_Paulo\n- America/Scoresbysund\n- America/Shiprock\n- America/Sitka\n- America/St_Barthelemy\n- America/St_Johns\n- America/St_Kitts\n- America/St_Lucia\n- America/St_Thomas\n- America/St_Vincent\n- America/Swift_Current\n- America/Tegucigalpa\n- America/Thule\n- America/Thunder_Bay\n- America/Tijuana\n- America/Toronto\n- America/Tortola\n- America/Vancouver\n- America/Virgin\n- America/Whitehorse\n- America/Winnipeg\n- America/Yakutat\n- America/Yellowknife\n- Antarctica/Casey\n- Antarctica/Davis\n- Antarctica/DumontDUrville\n- Antarctica/Macquarie\n- Antarctica/Mawson\n- Antarctica/McMurdo\n- Antarctica/Palmer\n- Antarctica/Rothera\n- Antarctica/South_Pole\n- Antarctica/Syowa\n- Antarctica/Troll\n- Antarctica/Vostok\n- Arctic/Longyearbyen\n- Asia/Aden\n- Asia/Almaty\n- Asia/Amman\n- Asia/Anadyr\n- Asia/Aqtau\n- Asia/Aqtobe\n- Asia/Ashgabat\n- Asia/Ashkhabad\n- Asia/Atyrau\n- Asia/Baghdad\n- Asia/Bahrain\n- Asia/Baku\n- Asia/Bangkok\n- Asia/Barnaul\n- Asia/Beirut\n- Asia/Bishkek\n- Asia/Brunei\n- Asia/Calcutta\n- Asia/Chita\n- Asia/Choibalsan\n- Asia/Chongqing\n- Asia/Chungking\n- Asia/Colombo\n- Asia/Dacca\n- Asia/Damascus\n- Asia/Dhaka\n- Asia/Dili\n- Asia/Dubai\n- Asia/Dushanbe\n- Asia/Famagusta\n- Asia/Gaza\n- Asia/Harbin\n- Asia/Hebron\n- Asia/Ho_Chi_Minh\n- Asia/Hong_Kong\n- Asia/Hovd\n- Asia/Irkutsk\n- Asia/Istanbul\n- Asia/Jakarta\n- Asia/Jayapura\n- Asia/Jerusalem\n- Asia/Kabul\n- Asia/Kamchatka\n- Asia/Karachi\n- Asia/Kashgar\n- Asia/Kathmandu\n- Asia/Katmandu\n- Asia/Khandyga\n- Asia/Kolkata\n- Asia/Krasnoyarsk\n- Asia/Kuala_Lumpur\n- Asia/Kuching\n- Asia/Kuwait\n- Asia/Macao\n- Asia/Macau\n- Asia/Magadan\n- Asia/Makassar\n- Asia/Manila\n- Asia/Muscat\n- Asia/Nicosia\n- Asia/Novokuznetsk\n- Asia/Novosibirsk\n- Asia/Omsk\n- Asia/Oral\n- Asia/Phnom_Penh\n- Asia/Pontianak\n- Asia/Pyongyang\n- Asia/Qatar\n- Asia/Qostanay\n- Asia/Qyzylorda\n- Asia/Rangoon\n- Asia/Riyadh\n- Asia/Saigon\n- Asia/Sakhalin\n- Asia/Samarkand\n- Asia/Seoul\n- Asia/Shanghai\n- Asia/Singapore\n- Asia/Srednekolymsk\n- Asia/Taipei\n- Asia/Tashkent\n- Asia/Tbilisi\n- Asia/Tehran\n- Asia/Tel_Aviv\n- Asia/Thimbu\n- Asia/Thimphu\n- Asia/Tokyo\n- Asia/Tomsk\n- Asia/Ujung_Pandang\n- Asia/Ulaanbaatar\n- Asia/Ulan_Bator\n- Asia/Urumqi\n- Asia/Ust-Nera\n- Asia/Vientiane\n- Asia/Vladivostok\n- Asia/Yakutsk\n- Asia/Yangon\n- Asia/Yekaterinburg\n- Asia/Yerevan\n- Atlantic/Azores\n- Atlantic/Bermuda\n- Atlantic/Canary\n- Atlantic/Cape_Verde\n- Atlantic/Faeroe\n- Atlantic/Faroe\n- Atlantic/Jan_Mayen\n- Atlantic/Madeira\n- Atlantic/Reykjavik\n- Atlantic/South_Georgia\n- Atlantic/St_Helena\n- Atlantic/Stanley\n- Australia/ACT\n- Australia/Adelaide\n- Australia/Brisbane\n- Australia/Broken_Hill\n- Australia/Canberra\n- Australia/Currie\n- Australia/Darwin\n- Australia/Eucla\n- Australia/Hobart\n- Australia/LHI\n- Australia/Lindeman\n- Australia/Lord_Howe\n- Australia/Melbourne\n- Australia/NSW\n- Australia/North\n- Australia/Perth\n- Australia/Queensland\n- Australia/South\n- Australia/Sydney\n- Australia/Tasmania\n- Australia/Victoria\n- Australia/West\n- Australia/Yancowinna\n- Brazil/Acre\n- Brazil/DeNoronha\n- Brazil/East\n- Brazil/West\n- CET\n- CST6CDT\n- Canada/Atlantic\n- Canada/Central\n- Canada/Eastern\n- Canada/Mountain\n- Canada/Newfoundland\n- Canada/Pacific\n- Canada/Saskatchewan\n- Canada/Yukon\n- Chile/Continental\n- Chile/EasterIsland\n- Cuba\n- EET\n- EST\n- EST5EDT\n- Egypt\n- Eire\n- Etc/GMT\n- Etc/GMT+0\n- Etc/GMT+1\n- Etc/GMT+10\n- Etc/GMT+11\n- Etc/GMT+12\n- Etc/GMT+2\n- Etc/GMT+3\n- Etc/GMT+4\n- Etc/GMT+5\n- Etc/GMT+6\n- Etc/GMT+7\n- Etc/GMT+8\n- Etc/GMT+9\n- Etc/GMT-0\n- Etc/GMT-1\n- Etc/GMT-10\n- Etc/GMT-11\n- Etc/GMT-12\n- Etc/GMT-13\n- Etc/GMT-14\n- Etc/GMT-2\n- Etc/GMT-3\n- Etc/GMT-4\n- Etc/GMT-5\n- Etc/GMT-6\n- Etc/GMT-7\n- Etc/GMT-8\n- Etc/GMT-9\n- Etc/GMT0\n- Etc/Greenwich\n- Etc/UCT\n- Etc/UTC\n- Etc/Universal\n- Etc/Zulu\n- Europe/Amsterdam\n- Europe/Andorra\n- Europe/Astrakhan\n- Europe/Athens\n- Europe/Belfast\n- Europe/Belgrade\n- Europe/Berlin\n- Europe/Bratislava\n- Europe/Brussels\n- Europe/Bucharest\n- Europe/Budapest\n- Europe/Busingen\n- Europe/Chisinau\n- Europe/Copenhagen\n- Europe/Dublin\n- Europe/Gibraltar\n- Europe/Guernsey\n- Europe/Helsinki\n- Europe/Isle_of_Man\n- Europe/Istanbul\n- Europe/Jersey\n- Europe/Kaliningrad\n- Europe/Kiev\n- Europe/Kirov\n- Europe/Kyiv\n- Europe/Lisbon\n- Europe/Ljubljana\n- Europe/London\n- Europe/Luxembourg\n- Europe/Madrid\n- Europe/Malta\n- Europe/Mariehamn\n- Europe/Minsk\n- Europe/Monaco\n- Europe/Moscow\n- Europe/Nicosia\n- Europe/Oslo\n- Europe/Paris\n- Europe/Podgorica\n- Europe/Prague\n- Europe/Riga\n- Europe/Rome\n- Europe/Samara\n- Europe/San_Marino\n- Europe/Sarajevo\n- Europe/Saratov\n- Europe/Simferopol\n- Europe/Skopje\n- Europe/Sofia\n- Europe/Stockholm\n- Europe/Tallinn\n- Europe/Tirane\n- Europe/Tiraspol\n- Europe/Ulyanovsk\n- Europe/Uzhgorod\n- Europe/Vaduz\n- Europe/Vatican\n- Europe/Vienna\n- Europe/Vilnius\n- Europe/Volgograd\n- Europe/Warsaw\n- Europe/Zagreb\n- Europe/Zaporozhye\n- Europe/Zurich\n- Factory\n- GB\n- GB-Eire\n- GMT\n- GMT+0\n- GMT-0\n- GMT0\n- Greenwich\n- HST\n- Hongkong\n- Iceland\n- Indian/Antananarivo\n- Indian/Chagos\n- Indian/Christmas\n- Indian/Cocos\n- Indian/Comoro\n- Indian/Kerguelen\n- Indian/Mahe\n- Indian/Maldives\n- Indian/Mauritius\n- Indian/Mayotte\n- Indian/Reunion\n- Iran\n- Israel\n- Jamaica\n- Japan\n- Kwajalein\n- Libya\n- MET\n- MST\n- MST7MDT\n- Mexico/BajaNorte\n- Mexico/BajaSur\n- Mexico/General\n- NZ\n- NZ-CHAT\n- Navajo\n- PRC\n- PST8PDT\n- Pacific/Apia\n- Pacific/Auckland\n- Pacific/Bougainville\n- Pacific/Chatham\n- Pacific/Chuuk\n- Pacific/Easter\n- Pacific/Efate\n- Pacific/Enderbury\n- Pacific/Fakaofo\n- Pacific/Fiji\n- Pacific/Funafuti\n- Pacific/Galapagos\n- Pacific/Gambier\n- Pacific/Guadalcanal\n- Pacific/Guam\n- Pacific/Honolulu\n- Pacific/Johnston\n- Pacific/Kanton\n- Pacific/Kiritimati\n- Pacific/Kosrae\n- Pacific/Kwajalein\n- Pacific/Majuro\n- Pacific/Marquesas\n- Pacific/Midway\n- Pacific/Nauru\n- Pacific/Niue\n- Pacific/Norfolk\n- Pacific/Noumea\n- Pacific/Pago_Pago\n- Pacific/Palau\n- Pacific/Pitcairn\n- Pacific/Pohnpei\n- Pacific/Ponape\n- Pacific/Port_Moresby\n- Pacific/Rarotonga\n- Pacific/Saipan\n- Pacific/Samoa\n- Pacific/Tahiti\n- Pacific/Tarawa\n- Pacific/Tongatapu\n- Pacific/Truk\n- Pacific/Wake\n- Pacific/Wallis\n- Pacific/Yap\n- Poland\n- Portugal\n- ROC\n- ROK\n- Singapore\n- Turkey\n- UCT\n- US/Alaska\n- US/Aleutian\n- US/Arizona\n- US/Central\n- US/East-Indiana\n- US/Eastern\n- US/Hawaii\n- US/Indiana-Starke\n- US/Michigan\n- US/Mountain\n- US/Pacific\n- US/Samoa\n- UTC\n- Universal\n- W-SU\n- WET\n- Zulu\n- posix/Africa/Abidjan\n- posix/Africa/Accra\n- posix/Africa/Addis_Ababa\n- posix/Africa/Algiers\n- posix/Africa/Asmara\n- posix/Africa/Asmera\n- posix/Africa/Bamako\n- posix/Africa/Bangui\n- posix/Africa/Banjul\n- posix/Africa/Bissau\n- posix/Africa/Blantyre\n- posix/Africa/Brazzaville\n- posix/Africa/Bujumbura\n- posix/Africa/Cairo\n- posix/Africa/Casablanca\n- posix/Africa/Ceuta\n- posix/Africa/Conakry\n- posix/Africa/Dakar\n- posix/Africa/Dar_es_Salaam\n- posix/Africa/Djibouti\n- posix/Africa/Douala\n- posix/Africa/El_Aaiun\n- posix/Africa/Freetown\n- posix/Africa/Gaborone\n- posix/Africa/Harare\n- posix/Africa/Johannesburg\n- posix/Africa/Juba\n- posix/Africa/Kampala\n- posix/Africa/Khartoum\n- posix/Africa/Kigali\n- posix/Africa/Kinshasa\n- posix/Africa/Lagos\n- posix/Africa/Libreville\n- posix/Africa/Lome\n- posix/Africa/Luanda\n- posix/Africa/Lubumbashi\n- posix/Africa/Lusaka\n- posix/Africa/Malabo\n- posix/Africa/Maputo\n- posix/Africa/Maseru\n- posix/Africa/Mbabane\n- posix/Africa/Mogadishu\n- posix/Africa/Monrovia\n- posix/Africa/Nairobi\n- posix/Africa/Ndjamena\n- posix/Africa/Niamey\n- posix/Africa/Nouakchott\n- posix/Africa/Ouagadougou\n- posix/Africa/Porto-Novo\n- posix/Africa/Sao_Tome\n- posix/Africa/Timbuktu\n- posix/Africa/Tripoli\n- posix/Africa/Tunis\n- posix/Africa/Windhoek\n- posix/America/Adak\n- posix/America/Anchorage\n- posix/America/Anguilla\n- posix/America/Antigua\n- posix/America/Araguaina\n- posix/America/Argentina/Buenos_Aires\n- posix/America/Argentina/Catamarca\n- posix/America/Argentina/ComodRivadavia\n- posix/America/Argentina/Cordoba\n- posix/America/Argentina/Jujuy\n- posix/America/Argentina/La_Rioja\n- posix/America/Argentina/Mendoza\n- posix/America/Argentina/Rio_Gallegos\n- posix/America/Argentina/Salta\n- posix/America/Argentina/San_Juan\n- posix/America/Argentina/San_Luis\n- posix/America/Argentina/Tucuman\n- posix/America/Argentina/Ushuaia\n- posix/America/Aruba\n- posix/America/Asuncion\n- posix/America/Atikokan\n- posix/America/Atka\n- posix/America/Bahia\n- posix/America/Bahia_Banderas\n- posix/America/Barbados\n- posix/America/Belem\n- posix/America/Belize\n- posix/America/Blanc-Sablon\n- posix/America/Boa_Vista\n- posix/America/Bogota\n- posix/America/Boise\n- posix/America/Buenos_Aires\n- posix/America/Cambridge_Bay\n- posix/America/Campo_Grande\n- posix/America/Cancun\n- posix/America/Caracas\n- posix/America/Catamarca\n- posix/America/Cayenne\n- posix/America/Cayman\n- posix/America/Chicago\n- posix/America/Chihuahua\n- posix/America/Ciudad_Juarez\n- posix/America/Coral_Harbour\n- posix/America/Cordoba\n- posix/America/Costa_Rica\n- posix/America/Coyhaique\n- posix/America/Creston\n- posix/America/Cuiaba\n- posix/America/Curacao\n- posix/America/Danmarkshavn\n- posix/America/Dawson\n- posix/America/Dawson_Creek\n- posix/America/Denver\n- posix/America/Detroit\n- posix/America/Dominica\n- posix/America/Edmonton\n- posix/America/Eirunepe\n- posix/America/El_Salvador\n- posix/America/Ensenada\n- posix/America/Fort_Nelson\n- posix/America/Fort_Wayne\n- posix/America/Fortaleza\n- posix/America/Glace_Bay\n- posix/America/Godthab\n- posix/America/Goose_Bay\n- posix/America/Grand_Turk\n- posix/America/Grenada\n- posix/America/Guadeloupe\n- posix/America/Guatemala\n- posix/America/Guayaquil\n- posix/America/Guyana\n- posix/America/Halifax\n- posix/America/Havana\n- posix/America/Hermosillo\n- posix/America/Indiana/Indianapolis\n- posix/America/Indiana/Knox\n- posix/America/Indiana/Marengo\n- posix/America/Indiana/Petersburg\n- posix/America/Indiana/Tell_City\n- posix/America/Indiana/Vevay\n- posix/America/Indiana/Vincennes\n- posix/America/Indiana/Winamac\n- posix/America/Indianapolis\n- posix/America/Inuvik\n- posix/America/Iqaluit\n- posix/America/Jamaica\n- posix/America/Jujuy\n- posix/America/Juneau\n- posix/America/Kentucky/Louisville\n- posix/America/Kentucky/Monticello\n- posix/America/Knox_IN\n- posix/America/Kralendijk\n- posix/America/La_Paz\n- posix/America/Lima\n- posix/America/Los_Angeles\n- posix/America/Louisville\n- posix/America/Lower_Princes\n- posix/America/Maceio\n- posix/America/Managua\n- posix/America/Manaus\n- posix/America/Marigot\n- posix/America/Martinique\n- posix/America/Matamoros\n- posix/America/Mazatlan\n- posix/America/Mendoza\n- posix/America/Menominee\n- posix/America/Merida\n- posix/America/Metlakatla\n- posix/America/Mexico_City\n- posix/America/Miquelon\n- posix/America/Moncton\n- posix/America/Monterrey\n- posix/America/Montevideo\n- posix/America/Montreal\n- posix/America/Montserrat\n- posix/America/Nassau\n- posix/America/New_York\n- posix/America/Nipigon\n- posix/America/Nome\n- posix/America/Noronha\n- posix/America/North_Dakota/Beulah\n- posix/America/North_Dakota/Center\n- posix/America/North_Dakota/New_Salem\n- posix/America/Nuuk\n- posix/America/Ojinaga\n- posix/America/Panama\n- posix/America/Pangnirtung\n- posix/America/Paramaribo\n- posix/America/Phoenix\n- posix/America/Port-au-Prince\n- posix/America/Port_of_Spain\n- posix/America/Porto_Acre\n- posix/America/Porto_Velho\n- posix/America/Puerto_Rico\n- posix/America/Punta_Arenas\n- posix/America/Rainy_River\n- posix/America/Rankin_Inlet\n- posix/America/Recife\n- posix/America/Regina\n- posix/America/Resolute\n- posix/America/Rio_Branco\n- posix/America/Rosario\n- posix/America/Santa_Isabel\n- posix/America/Santarem\n- posix/America/Santiago\n- posix/America/Santo_Domingo\n- posix/America/Sao_Paulo\n- posix/America/Scoresbysund\n- posix/America/Shiprock\n- posix/America/Sitka\n- posix/America/St_Barthelemy\n- posix/America/St_Johns\n- posix/America/St_Kitts\n- posix/America/St_Lucia\n- posix/America/St_Thomas\n- posix/America/St_Vincent\n- posix/America/Swift_Current\n- posix/America/Tegucigalpa\n- posix/America/Thule\n- posix/America/Thunder_Bay\n- posix/America/Tijuana\n- posix/America/Toronto\n- posix/America/Tortola\n- posix/America/Vancouver\n- posix/America/Virgin\n- posix/America/Whitehorse\n- posix/America/Winnipeg\n- posix/America/Yakutat\n- posix/America/Yellowknife\n- posix/Antarctica/Casey\n- posix/Antarctica/Davis\n- posix/Antarctica/DumontDUrville\n- posix/Antarctica/Macquarie\n- posix/Antarctica/Mawson\n- posix/Antarctica/McMurdo\n- posix/Antarctica/Palmer\n- posix/Antarctica/Rothera\n- posix/Antarctica/South_Pole\n- posix/Antarctica/Syowa\n- posix/Antarctica/Troll\n- posix/Antarctica/Vostok\n- posix/Arctic/Longyearbyen\n- posix/Asia/Aden\n- posix/Asia/Almaty\n- posix/Asia/Amman\n- posix/Asia/Anadyr\n- posix/Asia/Aqtau\n- posix/Asia/Aqtobe\n- posix/Asia/Ashgabat\n- posix/Asia/Ashkhabad\n- posix/Asia/Atyrau\n- posix/Asia/Baghdad\n- posix/Asia/Bahrain\n- posix/Asia/Baku\n- posix/Asia/Bangkok\n- posix/Asia/Barnaul\n- posix/Asia/Beirut\n- posix/Asia/Bishkek\n- posix/Asia/Brunei\n- posix/Asia/Calcutta\n- posix/Asia/Chita\n- posix/Asia/Choibalsan\n- posix/Asia/Chongqing\n- posix/Asia/Chungking\n- posix/Asia/Colombo\n- posix/Asia/Dacca\n- posix/Asia/Damascus\n- posix/Asia/Dhaka\n- posix/Asia/Dili\n- posix/Asia/Dubai\n- posix/Asia/Dushanbe\n- posix/Asia/Famagusta\n- posix/Asia/Gaza\n- posix/Asia/Harbin\n- posix/Asia/Hebron\n- posix/Asia/Ho_Chi_Minh\n- posix/Asia/Hong_Kong\n- posix/Asia/Hovd\n- posix/Asia/Irkutsk\n- posix/Asia/Istanbul\n- posix/Asia/Jakarta\n- posix/Asia/Jayapura\n- posix/Asia/Jerusalem\n- posix/Asia/Kabul\n- posix/Asia/Kamchatka\n- posix/Asia/Karachi\n- posix/Asia/Kashgar\n- posix/Asia/Kathmandu\n- posix/Asia/Katmandu\n- posix/Asia/Khandyga\n- posix/Asia/Kolkata\n- posix/Asia/Krasnoyarsk\n- posix/Asia/Kuala_Lumpur\n- posix/Asia/Kuching\n- posix/Asia/Kuwait\n- posix/Asia/Macao\n- posix/Asia/Macau\n- posix/Asia/Magadan\n- posix/Asia/Makassar\n- posix/Asia/Manila\n- posix/Asia/Muscat\n- posix/Asia/Nicosia\n- posix/Asia/Novokuznetsk\n- posix/Asia/Novosibirsk\n- posix/Asia/Omsk\n- posix/Asia/Oral\n- posix/Asia/Phnom_Penh\n- posix/Asia/Pontianak\n- posix/Asia/Pyongyang\n- posix/Asia/Qatar\n- posix/Asia/Qostanay\n- posix/Asia/Qyzylorda\n- posix/Asia/Rangoon\n- posix/Asia/Riyadh\n- posix/Asia/Saigon\n- posix/Asia/Sakhalin\n- posix/Asia/Samarkand\n- posix/Asia/Seoul\n- posix/Asia/Shanghai\n- posix/Asia/Singapore\n- posix/Asia/Srednekolymsk\n- posix/Asia/Taipei\n- posix/Asia/Tashkent\n- posix/Asia/Tbilisi\n- posix/Asia/Tehran\n- posix/Asia/Tel_Aviv\n- posix/Asia/Thimbu\n- posix/Asia/Thimphu\n- posix/Asia/Tokyo\n- posix/Asia/Tomsk\n- posix/Asia/Ujung_Pandang\n- posix/Asia/Ulaanbaatar\n- posix/Asia/Ulan_Bator\n- posix/Asia/Urumqi\n- posix/Asia/Ust-Nera\n- posix/Asia/Vientiane\n- posix/Asia/Vladivostok\n- posix/Asia/Yakutsk\n- posix/Asia/Yangon\n- posix/Asia/Yekaterinburg\n- posix/Asia/Yerevan\n- posix/Atlantic/Azores\n- posix/Atlantic/Bermuda\n- posix/Atlantic/Canary\n- posix/Atlantic/Cape_Verde\n- posix/Atlantic/Faeroe\n- posix/Atlantic/Faroe\n- posix/Atlantic/Jan_Mayen\n- posix/Atlantic/Madeira\n- posix/Atlantic/Reykjavik\n- posix/Atlantic/South_Georgia\n- posix/Atlantic/St_Helena\n- posix/Atlantic/Stanley\n- posix/Australia/ACT\n- posix/Australia/Adelaide\n- posix/Australia/Brisbane\n- posix/Australia/Broken_Hill\n- posix/Australia/Canberra\n- posix/Australia/Currie\n- posix/Australia/Darwin\n- posix/Australia/Eucla\n- posix/Australia/Hobart\n- posix/Australia/LHI\n- posix/Australia/Lindeman\n- posix/Australia/Lord_Howe\n- posix/Australia/Melbourne\n- posix/Australia/NSW\n- posix/Australia/North\n- posix/Australia/Perth\n- posix/Australia/Queensland\n- posix/Australia/South\n- posix/Australia/Sydney\n- posix/Australia/Tasmania\n- posix/Australia/Victoria\n- posix/Australia/West\n- posix/Australia/Yancowinna\n- posix/Brazil/Acre\n- posix/Brazil/DeNoronha\n- posix/Brazil/East\n- posix/Brazil/West\n- posix/CET\n- posix/CST6CDT\n- posix/Canada/Atlantic\n- posix/Canada/Central\n- posix/Canada/Eastern\n- posix/Canada/Mountain\n- posix/Canada/Newfoundland\n- posix/Canada/Pacific\n- posix/Canada/Saskatchewan\n- posix/Canada/Yukon\n- posix/Chile/Continental\n- posix/Chile/EasterIsland\n- posix/Cuba\n- posix/EET\n- posix/EST\n- posix/EST5EDT\n- posix/Egypt\n- posix/Eire\n- posix/Etc/GMT\n- posix/Etc/GMT+0\n- posix/Etc/GMT+1\n- posix/Etc/GMT+10\n- posix/Etc/GMT+11\n- posix/Etc/GMT+12\n- posix/Etc/GMT+2\n- posix/Etc/GMT+3\n- posix/Etc/GMT+4\n- posix/Etc/GMT+5\n- posix/Etc/GMT+6\n- posix/Etc/GMT+7\n- posix/Etc/GMT+8\n- posix/Etc/GMT+9\n- posix/Etc/GMT-0\n- posix/Etc/GMT-1\n- posix/Etc/GMT-10\n- posix/Etc/GMT-11\n- posix/Etc/GMT-12\n- posix/Etc/GMT-13\n- posix/Etc/GMT-14\n- posix/Etc/GMT-2\n- posix/Etc/GMT-3\n- posix/Etc/GMT-4\n- posix/Etc/GMT-5\n- posix/Etc/GMT-6\n- posix/Etc/GMT-7\n- posix/Etc/GMT-8\n- posix/Etc/GMT-9\n- posix/Etc/GMT0\n- posix/Etc/Greenwich\n- posix/Etc/UCT\n- posix/Etc/UTC\n- posix/Etc/Universal\n- posix/Etc/Zulu\n- posix/Europe/Amsterdam\n- posix/Europe/Andorra\n- posix/Europe/Astrakhan\n- posix/Europe/Athens\n- posix/Europe/Belfast\n- posix/Europe/Belgrade\n- posix/Europe/Berlin\n- posix/Europe/Bratislava\n- posix/Europe/Brussels\n- posix/Europe/Bucharest\n- posix/Europe/Budapest\n- posix/Europe/Busingen\n- posix/Europe/Chisinau\n- posix/Europe/Copenhagen\n- posix/Europe/Dublin\n- posix/Europe/Gibraltar\n- posix/Europe/Guernsey\n- posix/Europe/Helsinki\n- posix/Europe/Isle_of_Man\n- posix/Europe/Istanbul\n- posix/Europe/Jersey\n- posix/Europe/Kaliningrad\n- posix/Europe/Kiev\n- posix/Europe/Kirov\n- posix/Europe/Kyiv\n- posix/Europe/Lisbon\n- posix/Europe/Ljubljana\n- posix/Europe/London\n- posix/Europe/Luxembourg\n- posix/Europe/Madrid\n- posix/Europe/Malta\n- posix/Europe/Mariehamn\n- posix/Europe/Minsk\n- posix/Europe/Monaco\n- posix/Europe/Moscow\n- posix/Europe/Nicosia\n- posix/Europe/Oslo\n- posix/Europe/Paris\n- posix/Europe/Podgorica\n- posix/Europe/Prague\n- posix/Europe/Riga\n- posix/Europe/Rome\n- posix/Europe/Samara\n- posix/Europe/San_Marino\n- posix/Europe/Sarajevo\n- posix/Europe/Saratov\n- posix/Europe/Simferopol\n- posix/Europe/Skopje\n- posix/Europe/Sofia\n- posix/Europe/Stockholm\n- posix/Europe/Tallinn\n- posix/Europe/Tirane\n- posix/Europe/Tiraspol\n- posix/Europe/Ulyanovsk\n- posix/Europe/Uzhgorod\n- posix/Europe/Vaduz\n- posix/Europe/Vatican\n- posix/Europe/Vienna\n- posix/Europe/Vilnius\n- posix/Europe/Volgograd\n- posix/Europe/Warsaw\n- posix/Europe/Zagreb\n- posix/Europe/Zaporozhye\n- posix/Europe/Zurich\n- posix/Factory\n- posix/GB\n- posix/GB-Eire\n- posix/GMT\n- posix/GMT+0\n- posix/GMT-0\n- posix/GMT0\n- posix/Greenwich\n- posix/HST\n- posix/Hongkong\n- posix/Iceland\n- posix/Indian/Antananarivo\n- posix/Indian/Chagos\n- posix/Indian/Christmas\n- posix/Indian/Cocos\n- posix/Indian/Comoro\n- posix/Indian/Kerguelen\n- posix/Indian/Mahe\n- posix/Indian/Maldives\n- posix/Indian/Mauritius\n- posix/Indian/Mayotte\n- posix/Indian/Reunion\n- posix/Iran\n- posix/Israel\n- posix/Jamaica\n- posix/Japan\n- posix/Kwajalein\n- posix/Libya\n- posix/MET\n- posix/MST\n- posix/MST7MDT\n- posix/Mexico/BajaNorte\n- posix/Mexico/BajaSur\n- posix/Mexico/General\n- posix/NZ\n- posix/NZ-CHAT\n- posix/Navajo\n- posix/PRC\n- posix/PST8PDT\n- posix/Pacific/Apia\n- posix/Pacific/Auckland\n- posix/Pacific/Bougainville\n- posix/Pacific/Chatham\n- posix/Pacific/Chuuk\n- posix/Pacific/Easter\n- posix/Pacific/Efate\n- posix/Pacific/Enderbury\n- posix/Pacific/Fakaofo\n- posix/Pacific/Fiji\n- posix/Pacific/Funafuti\n- posix/Pacific/Galapagos\n- posix/Pacific/Gambier\n- posix/Pacific/Guadalcanal\n- posix/Pacific/Guam\n- posix/Pacific/Honolulu\n- posix/Pacific/Johnston\n- posix/Pacific/Kanton\n- posix/Pacific/Kiritimati\n- posix/Pacific/Kosrae\n- posix/Pacific/Kwajalein\n- posix/Pacific/Majuro\n- posix/Pacific/Marquesas\n- posix/Pacific/Midway\n- posix/Pacific/Nauru\n- posix/Pacific/Niue\n- posix/Pacific/Norfolk\n- posix/Pacific/Noumea\n- posix/Pacific/Pago_Pago\n- posix/Pacific/Palau\n- posix/Pacific/Pitcairn\n- posix/Pacific/Pohnpei\n- posix/Pacific/Ponape\n- posix/Pacific/Port_Moresby\n- posix/Pacific/Rarotonga\n- posix/Pacific/Saipan\n- posix/Pacific/Samoa\n- posix/Pacific/Tahiti\n- posix/Pacific/Tarawa\n- posix/Pacific/Tongatapu\n- posix/Pacific/Truk\n- posix/Pacific/Wake\n- posix/Pacific/Wallis\n- posix/Pacific/Yap\n- posix/Poland\n- posix/Portugal\n- posix/ROC\n- posix/ROK\n- posix/Singapore\n- posix/Turkey\n- posix/UCT\n- posix/US/Alaska\n- posix/US/Aleutian\n- posix/US/Arizona\n- posix/US/Central\n- posix/US/East-Indiana\n- posix/US/Eastern\n- posix/US/Hawaii\n- posix/US/Indiana-Starke\n- posix/US/Michigan\n- posix/US/Mountain\n- posix/US/Pacific\n- posix/US/Samoa\n- posix/UTC\n- posix/Universal\n- posix/W-SU\n- posix/WET\n- posix/Zulu\n"
  },
  {
    "path": "test/io/config.py",
    "content": "import os\nimport pathlib\nimport shutil\nimport uuid\nimport yaml\n\n\nBASEDIR = pathlib.Path(os.path.realpath(__file__)).parent\nCONFIGSDIR = BASEDIR / \"configs\"\nFIXTURES = yaml.load(\n    (BASEDIR / \"fixtures/fixtures.yaml\").read_text(), Loader=yaml.Loader\n)\nPOSTGREST_BIN = shutil.which(\"postgrest\")\nSECRET = \"reallyreallyreallyreallyverysafe\"\n\n\ndef hpctixfile():\n    \"\"\"\n    Returns a unique filename for each postgrest process that is\n    run, if the HPCTIXFILE environment variable is set.\n\n    Later, we combine these files using \"hpc sum\" to get the\n    complete coverage.\n    \"\"\"\n\n    if \"HPCTIXFILE\" not in os.environ:\n        return \"\"\n\n    tixfile = pathlib.Path(os.environ[\"HPCTIXFILE\"])\n    # 12 chars are unique enough and chances of collisions are\n    # astronomically low.\n    test = uuid.uuid4().hex[:12]\n    return tixfile.with_suffix(f\".{test}.tix\")\n\n\ndef get_admin_host_and_port_from_config(config):\n    admin_host = config.get(\"PGRST_ADMIN_SERVER_HOST\", config[\"PGRST_SERVER_HOST\"])\n    admin_port = config[\"PGRST_ADMIN_SERVER_PORT\"]\n    return (admin_host, admin_port)\n"
  },
  {
    "path": "test/io/configs/aliases.config",
    "content": "db-schema = \"provided_through_alias\"\ndb-pool-timeout = 5\nmax-rows = 1000\npre-request = \"check_alias\"\nrole-claim-key = \".aliased\"\nroot-spec = \"open_alias\"\nsecret-is-base64 = true\n"
  },
  {
    "path": "test/io/configs/boolean-numeric.config",
    "content": "db-channel-enabled = \"1\"\ndb-prepared-statements = \"0\"\njwt-secret-is-base64 = \"2\"\n"
  },
  {
    "path": "test/io/configs/boolean-string.config",
    "content": "db-channel-enabled = \"true\"\ndb-prepared-statements = \"FALSE\"\njwt-secret-is-base64 = \"\\\"true\\\"\"\n"
  },
  {
    "path": "test/io/configs/defaults.config",
    "content": "# Not the default, but only works with PG* variables, which are not set\ndb-config = false\n# not existing config options should not break tests\nnot-existing = \"should succeed\"\n"
  },
  {
    "path": "test/io/configs/expected/aliases.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = 1000\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 5\ndb-pool-automatic-recovery = true\ndb-pre-request = \"check_alias\"\ndb-prepared-statements = true\ndb-root-spec = \"open_alias\"\ndb-schemas = \"provided_through_alias\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"aliased\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = true\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/boolean-numeric.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = false\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"role\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = true\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/boolean-string.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = false\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"role\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = true\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/defaults.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = false\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"role\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/jwt-role-claim-key1.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"roles\\\"[?(@ == \\\"role1\\\")]\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/jwt-role-claim-key2.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"roles\\\"[?(@ != \\\"role1\\\")]\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/jwt-role-claim-key3.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"roles\\\"[?(@ ^== \\\"role1\\\")]\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/jwt-role-claim-key4.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"roles\\\"[?(@ ==^ \\\"role1\\\")]\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/jwt-role-claim-key5.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"roles\\\"[?(@ *== \\\"role1\\\")]\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/expected/no-defaults-with-db-other-authenticator.config",
    "content": "client-error-verbosity = \"minimal\"\ndb-aggregates-enabled = false\ndb-anon-role = \"pre_config_role\"\ndb-channel = \"postgrest\"\ndb-channel-enabled = false\ndb-extra-search-path = \"public,extensions,other\"\ndb-hoisted-tx-settings = \"maintenance_work_mem\"\ndb-max-rows = 100\ndb-plan-enabled = true\ndb-pool = 1\ndb-pool-acquisition-timeout = 30\ndb-pool-max-lifetime = 3600\ndb-pool-max-idletime = 60\ndb-pool-automatic-recovery = false\ndb-pre-request = \"test.other_custom_headers\"\ndb-prepared-statements = false\ndb-root-spec = \"other_root\"\ndb-schemas = \"test,other_tenant1,other_tenant2\"\ndb-config = true\ndb-pre-config = \"postgrest.other_preconf\"\ndb-tx-end = \"rollback-allow-override\"\ndb-uri = \"postgresql://\"\njwt-aud = \"https://otherexample.org\"\njwt-role-claim-key = \".\\\"other\\\".\\\"pre_config_role\\\"\"\njwt-secret = \"ODERREALLYREALLYREALLYREALLYVERYSAFE\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 86400\nlog-level = \"info\"\nlog-query = true\nopenapi-mode = \"disabled\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"https://otherexample.org/api\"\nserver-cors-allowed-origins = \"http://otherorigin.com\"\nserver-host = \"0.0.0.0\"\nserver-port = 80\nserver-trace-header = \"traceparent\"\nserver-timing-enabled = true\nserver-unix-socket = \"/tmp/pgrst_io_test.sock\"\nserver-unix-socket-mode = \"777\"\nadmin-server-host = \"127.0.0.1\"\nadmin-server-port = 3001\napp.settings.test = \"test\"\napp.settings.test2 = \"test\"\n"
  },
  {
    "path": "test/io/configs/expected/no-defaults-with-db.config",
    "content": "client-error-verbosity = \"minimal\"\ndb-aggregates-enabled = false\ndb-anon-role = \"anonymous\"\ndb-channel = \"postgrest\"\ndb-channel-enabled = false\ndb-extra-search-path = \"public,extensions,private\"\ndb-hoisted-tx-settings = \"autovacuum_work_mem\"\ndb-max-rows = 500\ndb-plan-enabled = false\ndb-pool = 1\ndb-pool-acquisition-timeout = 30\ndb-pool-max-lifetime = 3600\ndb-pool-max-idletime = 60\ndb-pool-automatic-recovery = false\ndb-pre-request = \"test.custom_headers\"\ndb-prepared-statements = false\ndb-root-spec = \"root\"\ndb-schemas = \"test,tenant1,tenant2\"\ndb-config = true\ndb-pre-config = \"postgrest.preconf\"\ndb-tx-end = \"commit-allow-override\"\ndb-uri = \"postgresql://\"\njwt-aud = \"https://example.org\"\njwt-role-claim-key = \".\\\"a\\\".\\\"role\\\"\"\njwt-secret = \"OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 86400\nlog-level = \"info\"\nlog-query = true\nopenapi-mode = \"ignore-privileges\"\nopenapi-security-active = true\nopenapi-server-proxy-uri = \"https://example.org/api\"\nserver-cors-allowed-origins = \"http://origin.com\"\nserver-host = \"0.0.0.0\"\nserver-port = 80\nserver-trace-header = \"CF-Ray\"\nserver-timing-enabled = false\nserver-unix-socket = \"/tmp/pgrst_io_test.sock\"\nserver-unix-socket-mode = \"777\"\nadmin-server-host = \"127.0.0.1\"\nadmin-server-port = 3001\napp.settings.test = \"test\"\napp.settings.test2 = \"test\"\n"
  },
  {
    "path": "test/io/configs/expected/no-defaults.config",
    "content": "client-error-verbosity = \"minimal\"\ndb-aggregates-enabled = true\ndb-anon-role = \"root\"\ndb-channel = \"postgrest\"\ndb-channel-enabled = false\ndb-extra-search-path = \"public,test\"\ndb-hoisted-tx-settings = \"work_mem\"\ndb-max-rows = 1000\ndb-plan-enabled = true\ndb-pool = 1\ndb-pool-acquisition-timeout = 30\ndb-pool-max-lifetime = 3600\ndb-pool-max-idletime = 60\ndb-pool-automatic-recovery = false\ndb-pre-request = \"please_run_fast\"\ndb-prepared-statements = false\ndb-root-spec = \"openapi_v3\"\ndb-schemas = \"multi,tenant,setup\"\ndb-config = false\ndb-pre-config = \"postgrest.pre_config\"\ndb-tx-end = \"rollback-allow-override\"\ndb-uri = \"tmp_db\"\njwt-aud = \"https://postgrest.org\"\njwt-role-claim-key = \".\\\"user\\\"[0].\\\"real-role\\\"\"\njwt-secret = \"c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ=\"\njwt-secret-is-base64 = true\njwt-cache-max-entries = 86400\nlog-level = \"info\"\nlog-query = true\nopenapi-mode = \"ignore-privileges\"\nopenapi-security-active = true\nopenapi-server-proxy-uri = \"https://postgrest.org\"\nserver-cors-allowed-origins = \"http://example.com\"\nserver-host = \"0.0.0.0\"\nserver-port = 80\nserver-trace-header = \"X-Request-Id\"\nserver-timing-enabled = true\nserver-unix-socket = \"/tmp/pgrst_io_test.sock\"\nserver-unix-socket-mode = \"777\"\nadmin-server-host = \"127.0.0.1\"\nadmin-server-port = 3001\napp.settings.test = \"test\"\napp.settings.test2 = \"test\"\n"
  },
  {
    "path": "test/io/configs/expected/types.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"role\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"error\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\napp.settings.test = \"Bool False\"\n"
  },
  {
    "path": "test/io/configs/expected/utf-8.config",
    "content": "client-error-verbosity = \"verbose\"\ndb-aggregates-enabled = false\ndb-anon-role = \"\"\ndb-channel = \"pgrst\"\ndb-channel-enabled = true\ndb-extra-search-path = \"public\"\ndb-hoisted-tx-settings = \"statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation\"\ndb-max-rows = \"\"\ndb-plan-enabled = false\ndb-pool = 10\ndb-pool-acquisition-timeout = 10\ndb-pool-max-lifetime = 1800\ndb-pool-max-idletime = 30\ndb-pool-automatic-recovery = true\ndb-pre-request = \"\"\ndb-prepared-statements = true\ndb-root-spec = \"\"\ndb-schemas = \"public\"\ndb-config = true\ndb-pre-config = \"\"\ndb-tx-end = \"commit\"\ndb-uri = \"postgresql://\"\njwt-aud = \"\"\njwt-role-claim-key = \".\\\"role\\\"\"\njwt-secret = \"\"\njwt-secret-is-base64 = false\njwt-cache-max-entries = 1000\nlog-level = \"crit\"\nlog-query = false\nopenapi-mode = \"follow-privileges\"\nopenapi-security-active = false\nopenapi-server-proxy-uri = \"\"\nserver-cors-allowed-origins = \"\"\nserver-host = \"!4\"\nserver-port = 3000\nserver-trace-header = \"\"\nserver-timing-enabled = false\nserver-unix-socket = \"\"\nserver-unix-socket-mode = \"660\"\nadmin-server-host = \"!4\"\nadmin-server-port = \"\"\n"
  },
  {
    "path": "test/io/configs/invalid.yaml",
    "content": "yaml:\n  is:\n    not:\n      supported:\n"
  },
  {
    "path": "test/io/configs/jwt-role-claim-key1.config",
    "content": "jwt-role-claim-key = \".roles[?(@ == \\\"role1\\\")]\"\n"
  },
  {
    "path": "test/io/configs/jwt-role-claim-key2.config",
    "content": "jwt-role-claim-key = \".roles[?(@ != \\\"role1\\\")]\"\n"
  },
  {
    "path": "test/io/configs/jwt-role-claim-key3.config",
    "content": "jwt-role-claim-key = \".roles[?(@ ^== \\\"role1\\\")]\"\n"
  },
  {
    "path": "test/io/configs/jwt-role-claim-key4.config",
    "content": "jwt-role-claim-key = \".roles[?(@ ==^ \\\"role1\\\")]\"\n"
  },
  {
    "path": "test/io/configs/jwt-role-claim-key5.config",
    "content": "jwt-role-claim-key = \".roles[?(@ *== \\\"role1\\\")]\"\n"
  },
  {
    "path": "test/io/configs/no-defaults-env.yaml",
    "content": "PGRST_APP_SETTINGS_test2: test\nPGRST_APP_SETTINGS_test: test\nPGRST_CLIENT_ERROR_VERBOSITY: minimal\nPGRST_DB_AGGREGATES_ENABLED: true\nPGRST_DB_ANON_ROLE: root\nPGRST_DB_CHANNEL: postgrest\nPGRST_DB_CHANNEL_ENABLED: false\nPGRST_DB_EXTRA_SEARCH_PATH: public, test\nPGRST_DB_HOISTED_TX_SETTINGS: work_mem\nPGRST_DB_MAX_ROWS: 1000\nPGRST_DB_PLAN_ENABLED: true\nPGRST_DB_POOL: 1\nPGRST_DB_POOL_ACQUISITION_TIMEOUT: 30\nPGRST_DB_POOL_MAX_LIFETIME: 3600\nPGRST_DB_POOL_MAX_IDLETIME: 60\nPGRST_DB_POOL_AUTOMATIC_RECOVERY: false\nPGRST_DB_PREPARED_STATEMENTS: false\nPGRST_DB_PRE_REQUEST: please_run_fast\nPGRST_DB_ROOT_SPEC: openapi_v3\nPGRST_DB_SCHEMAS: multi,   tenant,setup\nPGRST_DB_CONFIG: false\nPGRST_DB_PRE_CONFIG: \"postgrest.pre_config\"\nPGRST_DB_TX_END: rollback-allow-override\nPGRST_DB_URI: tmp_db\nPGRST_DB_USE_LEGACY_GUCS: false\nPGRST_JWT_AUD: 'https://postgrest.org'\nPGRST_JWT_ROLE_CLAIM_KEY: '.user[0].\"real-role\"'\nPGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ=\nPGRST_JWT_SECRET_IS_BASE64: true\nPGRST_JWT_CACHE_MAX_ENTRIES: 86400\nPGRST_LOG_LEVEL: info\nPGRST_LOG_QUERY: true\nPGRST_OPENAPI_MODE: 'ignore-privileges'\nPGRST_OPENAPI_SECURITY_ACTIVE: true\nPGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'\nPGRST_SERVER_CORS_ALLOWED_ORIGINS: \"http://example.com\"\nPGRST_SERVER_HOST: 0.0.0.0\nPGRST_SERVER_PORT: 80\nPGRST_SERVER_TRACE_HEADER: X-Request-Id\nPGRST_SERVER_TIMING_ENABLED: true\nPGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock\nPGRST_SERVER_UNIX_SOCKET_MODE: 777\nPGRST_ADMIN_SERVER_HOST: 127.0.0.1\nPGRST_ADMIN_SERVER_PORT: 3001\n"
  },
  {
    "path": "test/io/configs/no-defaults.config",
    "content": "client-error-verbosity = \"minimal\"\ndb-aggregates-enabled = true\ndb-anon-role = \"root\"\ndb-channel = \"postgrest\"\ndb-channel-enabled = false\ndb-extra-search-path = \"public, test\"\ndb-hoisted-tx-settings = \"work_mem\"\ndb-max-rows = 1000\ndb-plan-enabled = true\ndb-pool = 1\ndb-pool-acquisition-timeout = 30\ndb-pool-max-lifetime = 3600\ndb-pool-max-idletime = 60\ndb-pool-automatic-recovery = false\ndb-pre-request = \"please_run_fast\"\ndb-prepared-statements = false\ndb-root-spec = \"openapi_v3\"\ndb-schemas = \"multi,   tenant,setup\"\ndb-config = false\ndb-pre-config = \"postgrest.pre_config\"\ndb-tx-end = \"rollback-allow-override\"\ndb-uri = \"tmp_db\"\njwt-aud = \"https://postgrest.org\"\njwt-role-claim-key = \".user[0].\\\"real-role\\\"\"\njwt-secret = \"c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ=\"\njwt-secret-is-base64 = true\njwt-cache-max-entries = 86400\nlog-level = \"info\"\nlog-query = true\nopenapi-mode = \"ignore-privileges\"\nopenapi-security-active = true\nopenapi-server-proxy-uri = \"https://postgrest.org\"\nserver-cors-allowed-origins = \"http://example.com\"\nserver-host = \"0.0.0.0\"\nserver-port = 80\nserver-trace-header = \"X-Request-Id\"\nserver-timing-enabled = true\nserver-unix-socket = \"/tmp/pgrst_io_test.sock\"\nserver-unix-socket-mode = \"777\"\nadmin-server-port = 3001\nadmin-server-host = \"127.0.0.1\"\napp.settings.test = \"test\"\napp.settings.test2 = \"test\"\n"
  },
  {
    "path": "test/io/configs/sigusr2-settings.config",
    "content": "# will be replaced in test\ndb-schemas = \"public\"\n\napp.settings.name_var = \"John\"\njwt-secret = \"invalidinvalidinvalidinvalidinvalid\"\n"
  },
  {
    "path": "test/io/configs/types.config",
    "content": "# tests how config options fall back with invalid types\n\n# expects string\napp.settings.test = false\n\n# expects boolean or string\ndb-channel-enabled = 13\n\n# expects integer or string\ndb-max-rows = true\n"
  },
  {
    "path": "test/io/configs/utf-8.config",
    "content": "# Commènt utf-8 chàrs\nlog-level = \"crit\"\n"
  },
  {
    "path": "test/io/conftest.py",
    "content": "import os\nimport pytest\nfrom syrupy.extensions.json import SingleFileSnapshotExtension\nfrom postgrest import run\n\n\n@pytest.fixture\ndef dburi():\n    \"Postgres database connection URI.\"\n    dbname = os.environ[\"PGDATABASE\"]\n    host = os.environ[\"PGHOST\"]\n    user = os.environ[\"PGUSER\"]\n    return f\"postgresql://?dbname={dbname}&host={host}&user={user}\".encode()\n\n\n@pytest.fixture\ndef baseenv():\n    \"Base environment to connect to PostgreSQL\"\n    return {\n        \"PGDATABASE\": os.environ[\"PGDATABASE\"],\n        \"PGHOST\": os.environ[\"PGHOST\"],\n        \"PGUSER\": os.environ[\"PGUSER\"],\n    }\n\n\n@pytest.fixture\ndef defaultenv(baseenv):\n    \"Default environment for PostgREST.\"\n    return {\n        **baseenv,\n        \"PGRST_DB_CONFIG\": \"true\",\n        \"PGRST_LOG_LEVEL\": \"info\",\n        \"PGRST_DB_POOL\": \"1\",\n        \"PGRST_NOT_EXISTING\": \"should not break any tests\",\n    }\n\n\n@pytest.fixture\ndef replicaenv(defaultenv):\n    \"Default environment for a PostgREST replica.\"\n    conf = {\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",\n        \"PGRST_DB_SCHEMAS\": \"replica\",\n    }\n    return {\n        \"primary\": {\n            **defaultenv,\n            **conf,\n        },\n        \"replica\": {\n            **defaultenv,\n            **conf,\n            \"PGHOST\": os.environ[\"PGREPLICAHOST\"] + \",\" + os.environ[\"PGHOST\"],\n            \"PGREPLICASLOT\": os.environ[\"PGREPLICASLOT\"],\n        },\n    }\n\n\n@pytest.fixture\ndef slow_schema_cache_env(defaultenv):\n    \"Slow schema cache load environment PostgREST.\"\n    return {\n        **defaultenv,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"1000\",  # this does a pg_sleep internally, it will cause the schema cache query to be slow\n        # the slow schema cache query will keep using one pool connection until it finishes\n        # to prevent requests waiting for PGRST_DB_POOL_ACQUISITION_TIMEOUT we'll increase the pool size (must be >= 2)\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n    }\n\n\n@pytest.fixture\ndef metapostgrest():\n    \"A shared postgrest instance to use for interacting with the database independently of the instance under test\"\n    role = \"meta_authenticator\"\n    env = {\n        \"PGDATABASE\": os.environ[\"PGDATABASE\"],\n        \"PGHOST\": os.environ[\"PGHOST\"],\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_DB_CONFIG\": \"true\",\n        \"PGRST_LOG_LEVEL\": \"info\",\n        \"PGRST_DB_POOL\": \"1\",\n    }\n    with run(env=env) as postgrest:\n        yield postgrest\n\n\nclass YamlSnapshotExtension(SingleFileSnapshotExtension):\n    _file_extension = \"yaml\"\n\n\n@pytest.fixture\ndef snapshot_yaml(snapshot):\n    return snapshot.use_extension(YamlSnapshotExtension)\n"
  },
  {
    "path": "test/io/fixtures/big_schema.sql",
    "content": "/*\nThis is a 2018 version of the apflora schema https://github.com/barbalex/apf2/tree/master/sql/apflora - latest version likely has differing contents\n\nWe use it to test our metadata generation because it contains a good amount of db objects.\n\nCustom roles and privileges were removed.\n\npostgrest-with-pg-14 -f test/io/big_schema.sql psql\n\nHas 12 functions:\n\nselect count(*)  from information_schema.routines where specific_schema = 'apflora';\n count\n-------\n    12\n\nHas 45 tables:\n\nselect count(*)  from information_schema.tables where table_schema = 'apflora' and table_type = 'BASE TABLE';\n count\n-------\n    45\n(1 row)\n\nHas 281 views:\n\nselect count(*)  from information_schema.views where table_schema = 'apflora';\n count\n-------\n   281\n(1 row)\n\nHas 45 pkcols where none of them is a composite primary key:\n\nwith pkcols as (\n  select kcu.table_schema,\n         kcu.table_name,\n         tco.constraint_name,\n         array_agg(kcu.column_name order by kcu.ordinal_position) as key_columns\n  from information_schema.table_constraints tco\n  join information_schema.key_column_usage kcu\n       on kcu.constraint_name = tco.constraint_name\n       and kcu.constraint_schema = tco.constraint_schema\n       and kcu.constraint_name = tco.constraint_name\n  where tco.constraint_type = 'PRIMARY KEY' and kcu.table_schema = 'apflora'\n  group by kcu.table_schema, kcu.table_name, tco.constraint_name\n)\nselect count(*) from pkcols\nwhere array_length(key_columns, 1) > 1;\n count\n-------\n     0\n(1 row)\n\nHas 50 foreign key relationships:\n\nwith fk_rel as (\n  select ns1.nspname as table_schema,\n             tab.relname as table_name,\n             ns2.nspname as foreign_table_schema,\n             other.relname as foreign_table_name,\n             conname     as constraint_name,\n             column_info.cols as columns\n  from pg_constraint,\n  lateral (\n    select array_agg(row(cols.attname, refs.attname) order by cols.attnum) as cols\n    from ( select unnest(conkey) as col, unnest(confkey) as ref) k,\n    lateral (select * from pg_attribute where attrelid = conrelid and attnum = col) as cols,\n    lateral (select * from pg_attribute where attrelid = confrelid and attnum = ref) as refs) as column_info,\n  lateral (select * from pg_namespace where pg_namespace.oid = connamespace) as ns1,\n  lateral (select * from pg_class where pg_class.oid = conrelid) as tab,\n  lateral (select * from pg_class where pg_class.oid = confrelid) as other,\n  lateral (select * from pg_namespace where pg_namespace.oid = other.relnamespace) as ns2\n  where contype = 'f' and conparentid = 0\n)\nselect count(*) from fk_rel;\n\n count\n-------\n    50\n(1 row)\n*/\n\nSET statement_timeout = 0;\nSET lock_timeout = 0;\nSET idle_in_transaction_session_timeout = 0;\nSET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSET check_function_bodies = false;\nSET client_min_messages = warning;\nSET row_security = off;\n\nCREATE SCHEMA apflora;\n\nCREATE SCHEMA auth;\n\nCREATE SCHEMA request;\n\n\nCREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;\n\n\n\nCOMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';\n\n\n\nCREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;\n\n\n\nCOMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';\n\n\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\" WITH SCHEMA public;\n\n\n\nCOMMENT ON EXTENSION \"uuid-ossp\" IS 'generate universally unique identifiers (UUIDs)';\n\n\n\nCREATE TYPE apflora.qk_pop_ohne_popber AS (\n\tproj_id uuid,\n\tap_id uuid,\n\thw text,\n\turl text[],\n\ttext text[]\n);\n\n\n\n\nCREATE TYPE apflora.qk_pop_ohne_popmassnber AS (\n\tproj_id uuid,\n\tap_id uuid,\n\thw text,\n\turl text[],\n\ttext text[]\n);\n\n\n\n\nCREATE TYPE apflora.qk_tpop_ohne_massnber AS (\n\tproj_id uuid,\n\tap_id uuid,\n\thw text,\n\turl text[],\n\ttext text[]\n);\n\n\n\n\nCREATE TYPE apflora.qk_tpop_ohne_tpopber AS (\n\tproj_id uuid,\n\tap_id uuid,\n\thw text,\n\turl text[],\n\ttext text[]\n);\n\n\n\n\nCREATE TYPE auth.jwt_token AS (\n\ttoken text\n);\n\n\n\n\nCREATE FUNCTION apflora.ap_insert_add_apart() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n  INSERT INTO\n    apflora.apart (ap_id, art_id)\n  VALUES (NEW.id, NEW.art_id);\n  RETURN NEW;\nEND;\n$$;\n\n\n\n\nCREATE FUNCTION apflora.ap_insert_add_idealbiotop() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n  INSERT INTO\n    apflora.idealbiotop (ap_id)\n  VALUES (NEW.id);\n  RETURN NEW;\nEND;\n$$;\n\n\n\n\nCREATE FUNCTION apflora.correct_vornach_beginnap_stati(apid uuid) RETURNS void\n    LANGUAGE plpgsql SECURITY DEFINER\n    AS $_$\n BEGIN\n\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr IS NULL\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr IS NULL\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr <= apflora.tpop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr <= apflora.pop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr > apflora.tpop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr > apflora.pop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr IS NULL\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr IS NULL\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr <= apflora.tpop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr <= apflora.pop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.tpop\n   WHERE id IN (\n     SELECT\n       tpop.id\n     FROM\n       apflora.tpop\n       INNER JOIN apflora.pop\n       ON apflora.tpop.pop_id = apflora.pop.id\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr > apflora.tpop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n   UPDATE apflora.pop\n   WHERE id IN (\n     SELECT\n       pop.id\n     FROM\n       apflora.pop\n         INNER JOIN apflora.ap\n         ON apflora.pop.ap_id = apflora.ap.id\n     WHERE\n       AND apflora.ap.start_jahr > apflora.pop.bekannt_seit\n       AND apflora.ap.id = $1\n   );\n\n END;\n $_$;\n\n\n\n\nCREATE FUNCTION apflora.login(username text, pass text) RETURNS auth.jwt_token\n    LANGUAGE plpgsql\n    AS $_$\ndeclare\n  _role name;\n  result auth.jwt_token;\nbegin\n  select auth.user_role($1, $2) into _role;\n  if _role is null then\n    raise invalid_password using message = 'invalid user or password';\n  end if;\n\n  select auth.sign(\n      row_to_json(r), current_setting('app.jwt_secret')\n    ) as token\n    from (\n      select _role as role,\n      $1 as username,\n      extract(epoch from now())::integer + 60*60*24*30 as exp\n    ) r\n    into result;\n  return result;\nend;\n$_$;\n\n\n\n\nCREATE FUNCTION apflora.pop_max_one_massnber_per_year() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    IF\n      (\n        NEW.jahr > 0\n        AND NEW.jahr IN\n          (\n            SELECT\n              jahr\n            FROM\n              apflora.popmassnber\n            WHERE\n              pop_id = NEW.pop_id\n              AND id <> NEW.id\n          )\n      )\n    THEN\n      RAISE EXCEPTION 'Pro Population und Jahr darf maximal ein Massnahmenbericht erfasst werden';\n    END IF;\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION apflora.pop_max_one_popber_per_year() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    IF\n      (\n        NEW.jahr > 0\n        AND NEW.jahr IN\n          (\n            SELECT\n              jahr\n            FROM\n              apflora.popber\n            WHERE\n              pop_id = NEW.pop_id\n              AND id <> NEW.id\n          )\n      )\n    THEN\n      RAISE EXCEPTION 'Pro Population und Jahr darf maximal ein Populationsbericht erfasst werden';\n    END IF;\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION apflora.qk_pop_ohne_popber(apid uuid, berichtjahr integer) RETURNS SETOF apflora.qk_pop_ohne_popber\n    LANGUAGE sql STABLE\n    AS $_$\n  SELECT DISTINCT\n    apflora.ap.proj_id,\n    apflora.pop.ap_id,\n    'Population mit angesiedelten Teilpopulationen (vor dem Berichtjahr), die (im Berichtjahr) kontrolliert wurden, aber ohne Populations-Bericht (im Berichtjahr):' AS hw,\n    ARRAY['Projekte', '4635372c-431c-11e8-bb30-e77f6cdd35a6' , 'Aktionspläne', apflora.ap.id, 'Populationen', apflora.pop.id]::text[] AS \"url\",\n    ARRAY[concat('Population (Nr.): ', apflora.pop.nr)]::text[] AS text\n  FROM\n    apflora.ap\n    INNER JOIN\n      apflora.pop\n      ON apflora.pop.ap_id = apflora.ap.id\n  WHERE\n    apflora.pop.id IN (\n      SELECT\n        apflora.tpop.pop_id\n      FROM\n        apflora.tpop\n      WHERE\n        apflora.tpop.apber_relevant = 1\n      GROUP BY\n        apflora.tpop.pop_id\n    )\n    AND apflora.pop.id IN (\n      SELECT DISTINCT\n        apflora.tpop.pop_id\n      FROM\n        apflora.tpop\n      WHERE\n        apflora.tpop.id IN (\n          SELECT DISTINCT\n          apflora.tpopmassn.tpop_id\n          FROM\n            apflora.tpopmassn\n          WHERE\n            apflora.tpopmassn.typ in (1, 2, 3)\n            AND apflora.tpopmassn.jahr < $2\n        )\n        AND apflora.tpop.id IN (\n          SELECT DISTINCT\n            apflora.tpopkontr.tpop_id\n          FROM\n            apflora.tpopkontr\n          WHERE\n            apflora.tpopkontr.typ NOT IN ('Zwischenziel', 'Ziel')\n            AND apflora.tpopkontr.jahr = $2\n        )\n    )\n    AND apflora.pop.id NOT IN (\n      SELECT DISTINCT\n        apflora.popber.pop_id\n      FROM\n        apflora.popber\n      WHERE\n        apflora.popber.jahr = $2\n    )\n    AND apflora.pop.ap_id = $1\n  $_$;\n\n\n\n\nCREATE FUNCTION apflora.qk_pop_ohne_popmassnber(apid uuid, berichtjahr integer) RETURNS SETOF apflora.qk_pop_ohne_popmassnber\n    LANGUAGE sql STABLE\n    AS $_$\n  SELECT DISTINCT\n    apflora.ap.proj_id,\n    apflora.pop.ap_id,\n    'Population mit angesiedelten Teilpopulationen (vor dem Berichtjahr), die (im Berichtjahr) kontrolliert wurden, aber ohne Massnahmen-Bericht (im Berichtjahr):' AS hw,\n    ARRAY['Projekte', '4635372c-431c-11e8-bb30-e77f6cdd35a6' , 'Aktionspläne', apflora.ap.id, 'Populationen', apflora.pop.id]::text[] AS \"url\",\n    ARRAY[concat('Population (Nr.): ', apflora.pop.nr)]::text[] AS text\n  FROM\n    apflora.ap\n    INNER JOIN\n      apflora.pop\n      ON apflora.pop.ap_id = apflora.ap.id\n  WHERE\n    apflora.pop.id IN (\n      SELECT\n        apflora.tpop.pop_id\n      FROM\n        apflora.tpop\n      WHERE\n        apflora.tpop.apber_relevant = 1\n      GROUP BY\n        apflora.tpop.pop_id\n    )\n    AND apflora.pop.id IN (\n      SELECT DISTINCT\n        apflora.tpop.pop_id\n      FROM\n        apflora.tpop\n      WHERE\n        apflora.tpop.id IN (\n          SELECT DISTINCT\n            apflora.tpopmassn.tpop_id\n          FROM\n            apflora.tpopmassn\n          WHERE\n            apflora.tpopmassn.typ IN (1, 2, 3)\n            AND apflora.tpopmassn.jahr < $2\n        )\n        AND apflora.tpop.id IN (\n          SELECT DISTINCT\n            apflora.tpopkontr.tpop_id\n          FROM\n            apflora.tpopkontr\n          WHERE\n            apflora.tpopkontr.typ NOT IN ('Zwischenziel', 'Ziel')\n            AND apflora.tpopkontr.jahr = $2\n        )\n    )\n    AND apflora.pop.id NOT IN (\n      SELECT DISTINCT\n        apflora.popmassnber.pop_id\n      FROM\n        apflora.popmassnber\n      WHERE\n        apflora.popmassnber.jahr = $2\n    )\n    AND apflora.pop.ap_id = $1\n  $_$;\n\n\n\n\nCREATE FUNCTION apflora.qk_tpop_ohne_massnber(apid uuid, berichtjahr integer) RETURNS SETOF apflora.qk_tpop_ohne_massnber\n    LANGUAGE sql STABLE\n    AS $_$\n  SELECT DISTINCT\n    '4635372c-431c-11e8-bb30-e77f6cdd35a6'::uuid AS proj_id,\n    apflora.pop.ap_id,\n    'Teilpopulation mit Ansiedlung (vor dem Berichtjahr) und Kontrolle (im Berichtjahr) aber ohne Massnahmen-Bericht (im Berichtjahr):' AS hw,\n    ARRAY['Projekte', '4635372c-431c-11e8-bb30-e77f6cdd35a6' , 'Aktionspläne', apflora.pop.ap_id, 'Populationen', apflora.pop.id, 'Teil-Populationen', apflora.tpop.id]::text[] AS \"url\",\n    ARRAY[concat('Population (Nr.): ', apflora.pop.nr), concat('Teil-Population (Nr.): ', apflora.tpop.nr)]::text[] AS text\n  FROM\n    apflora.pop\n    INNER JOIN\n      apflora.tpop\n      ON apflora.pop.id = apflora.tpop.pop_id\n  WHERE\n    apflora.tpop.apber_relevant = 1\n    AND apflora.tpop.id IN (\n      SELECT DISTINCT\n        apflora.tpopmassn.tpop_id\n      FROM\n        apflora.tpopmassn\n      WHERE\n        apflora.tpopmassn.typ IN (1, 2, 3)\n        AND apflora.tpopmassn.jahr < $2\n    )\n    AND apflora.tpop.id IN (\n      SELECT DISTINCT\n        apflora.tpopkontr.tpop_id\n      FROM\n        apflora.tpopkontr\n      WHERE\n        apflora.tpopkontr.typ NOT IN ('Zwischenziel', 'Ziel')\n        AND apflora.tpopkontr.jahr = $2\n    )\n    AND apflora.tpop.id NOT IN (\n      SELECT DISTINCT\n        apflora.tpopmassnber.tpop_id\n      FROM\n        apflora.tpopmassnber\n      WHERE\n        apflora.tpopmassnber.jahr = $2\n    )\n    AND apflora.pop.ap_id = $1\n  $_$;\n\n\n\n\nCREATE FUNCTION apflora.qk_tpop_ohne_tpopber(apid uuid, berichtjahr integer) RETURNS SETOF apflora.qk_tpop_ohne_tpopber\n    LANGUAGE sql STABLE\n    AS $_$\n  SELECT DISTINCT\n    apflora.ap.proj_id,\n    apflora.pop.ap_id,\n    'Teilpopulation mit Kontrolle (im Berichtjahr) aber ohne Teilpopulations-Bericht (im Berichtjahr):' AS hw,\n    ARRAY['Projekte', '4635372c-431c-11e8-bb30-e77f6cdd35a6' , 'Aktionspläne', apflora.ap.id, 'Populationen', apflora.pop.id, 'Teil-Populationen', apflora.tpop.id]::text[] AS \"url\",\n    ARRAY[concat('Population (Nr.): ', apflora.pop.nr), concat('Teil-Population (Nr.): ', apflora.tpop.nr)]::text[] AS text\n  FROM\n    apflora.ap\n    INNER JOIN\n      apflora.pop\n      INNER JOIN\n        apflora.tpop\n        ON apflora.pop.id = apflora.tpop.pop_id\n    ON apflora.pop.ap_id = apflora.ap.id\n  WHERE\n    apflora.tpop.apber_relevant = 1\n    AND apflora.tpop.id IN (\n      SELECT DISTINCT\n        apflora.tpopkontr.tpop_id\n      FROM\n        apflora.tpopkontr\n      WHERE\n        apflora.tpopkontr.typ NOT IN ('Zwischenziel', 'Ziel')\n        AND apflora.tpopkontr.jahr = $2\n    )\n    AND apflora.tpop.id NOT IN (\n      SELECT DISTINCT\n        apflora.tpopber.tpop_id\n      FROM\n        apflora.tpopber\n      WHERE\n        apflora.tpopber.jahr = $2\n    )\n    AND apflora.pop.ap_id = $1\n  $_$;\n\n\n\n\nCREATE FUNCTION apflora.tpop_max_one_massnber_per_year() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    IF\n      (\n        NEW.jahr > 0\n        AND NEW.jahr IN\n          (\n            SELECT\n              jahr\n            FROM\n              apflora.tpopmassnber\n            WHERE\n              tpop_id = NEW.tpop_id\n              AND id <> NEW.id\n          )\n      )\n    THEN\n      RAISE EXCEPTION  'Pro Teilpopulation und Jahr darf maximal ein Massnahmenbericht erfasst werden';\n    END IF;\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION apflora.tpop_max_one_tpopber_per_year() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    IF\n      (\n        NEW.jahr > 0\n        AND NEW.jahr IN\n        (\n          SELECT\n            jahr\n          FROM\n            apflora.tpopber\n          WHERE\n            tpop_id = NEW.tpop_id\n            AND id <> NEW.id\n        )\n      )\n    THEN\n      RAISE EXCEPTION 'Pro Teilpopulation und Jahr darf maximal ein Teilpopulationsbericht erfasst werden';\n    END IF;\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION auth.algorithm_sign(signables text, secret text, algorithm text) RETURNS text\n    LANGUAGE sql\n    AS $$\nWITH\n  alg AS (\n    SELECT CASE\n      WHEN algorithm = 'HS256' THEN 'sha256'\n      WHEN algorithm = 'HS384' THEN 'sha384'\n      WHEN algorithm = 'HS512' THEN 'sha512'\nSELECT auth.url_encode(hmac(signables, secret, alg.id)) FROM alg;\n$$;\n\n\n\n\nCREATE FUNCTION auth.check_role_exists() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nbegin\n  if not exists (select 1 from pg_roles as r where r.rolname = new.role) then\n    raise foreign_key_violation using message =\n      'unknown database role: ' || new.role;\n    return null;\n  end if;\n  return new;\nend\n$$;\n\n\n\n\nCREATE FUNCTION auth.encrypt_pass() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nbegin\n  if tg_op = 'INSERT' or new.pass <> old.pass then\n    new.pass = crypt(new.pass, gen_salt('bf'));\n  end if;\n  return new;\nend\n$$;\n\n\n\n\nCREATE FUNCTION auth.sign(payload json, secret text, algorithm text DEFAULT 'HS256'::text) RETURNS text\n    LANGUAGE sql\n    AS $$\nWITH\n  header AS (\n    SELECT auth.url_encode(convert_to('{\"alg\":\"' || algorithm || '\",\"typ\":\"JWT\"}', 'utf8')) AS data\n    ),\n  payload AS (\n    SELECT auth.url_encode(convert_to(payload::text, 'utf8')) AS data\n    ),\n  signables AS (\n    SELECT header.data || '.' || payload.data AS data FROM header, payload\n    )\nSELECT\n    signables.data || '.' ||\n    auth.algorithm_sign(signables.data, secret, algorithm) FROM signables;\n$$;\n\n\n\n\nCREATE FUNCTION auth.url_decode(data text) RETURNS bytea\n    LANGUAGE sql\n    AS $$\nWITH t AS (SELECT translate(data, '-_', '+/') AS trans),\n    SELECT decode(\n        t.trans ||\n        CASE WHEN rem.remainder > 0\n           THEN repeat('=', (4 - rem.remainder))\n           ELSE '' END,\n    'base64') FROM t, rem;\n$$;\n\n\n\n\nCREATE FUNCTION auth.url_encode(data bytea) RETURNS text\n    LANGUAGE sql\n    AS $$\n    SELECT translate(encode(data, 'base64'), E'+/=\\n', '-_');\n$$;\n\n\n\n\nCREATE FUNCTION auth.user_role(username text, pass text) RETURNS name\n    LANGUAGE plpgsql\n    AS $_$\nbegin\n  return (\n  select role from apflora.user\n   where apflora.user.name = $1\n     and apflora.user.pass = crypt($2, apflora.user.pass)\n  );\nend;\n$_$;\n\n\n\n\nCREATE FUNCTION auth.verify(token text, secret text, algorithm text DEFAULT 'HS256'::text) RETURNS TABLE(header json, payload json, valid boolean)\n    LANGUAGE sql\n    AS $$\n  SELECT\n    convert_from(auth.url_decode(r[1]), 'utf8')::json AS header,\n    convert_from(auth.url_decode(r[2]), 'utf8')::json AS payload,\n    r[3] = auth.algorithm_sign(r[1] || '.' || r[2], secret, algorithm) AS valid\n  FROM regexp_split_to_array(token, '\\.') r;\n$$;\n\n\n\nCREATE FUNCTION public.adresse_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ap_bearbstand_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ap_erfbeurtkrit_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ap_erfkrit_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ap_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ap_umsetzung_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.apber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.apberuebersicht_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.assozart_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.beob_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.beob_zuordnung_set_quelleid_on_insert() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    IF\n      length(NEW.\"NO_NOTE\") > 10\n    THEN\n      NEW.\"QuelleId\" = '1';\n    ELSE\n      NEW.\"QuelleId\" = '2';\n    END IF;\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.beobzuordnung_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.current_user_name() RETURNS text\n    LANGUAGE sql STABLE SECURITY DEFINER\n    AS $$\n  select nullif(current_setting('jwt.claims.username', true), '')::text;\n$$;\n\n\n\n\nCREATE FUNCTION public.dsql2(i_text text) RETURNS integer\n    LANGUAGE plpgsql\n    AS $$\nDeclare\n  v_val int;\nBEGIN\n  execute i_text into v_val;\n  return v_val;\nEND;\n$$;\n\n\n\n\nCOMMENT ON FUNCTION public.dsql2(i_text text) IS 'get number of rows per table';\n\n\n\nCREATE FUNCTION public.erfkrit_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.idealbiotop_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.pop_entwicklung_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.\"MutWer\" = current_setting('request.jwt.claim.username', true);\n    NEW.\"MutWann\" = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.pop_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.pop_status_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.popber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.popmassnber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.projekt_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tabelle_delete_notify() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n  PERFORM pg_notify('tabelle_update', json_build_object('table', TG_TABLE_NAME, 'type', TG_OP, 'row', row_to_json(OLD))::text);\n  RETURN OLD;\nEND;\n$$;\n\n\n\n\nCREATE FUNCTION public.tabelle_insert_notify() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n  PERFORM pg_notify('tabelle_update', json_build_object('table', TG_TABLE_NAME, 'type', TG_OP, 'row', row_to_json(NEW))::text);\n  RETURN NEW;\nEND;\n$$;\n\n\n\n\nCREATE FUNCTION public.tabelle_update_notify() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n  PERFORM pg_notify('tabelle_update', json_build_object('table', TG_TABLE_NAME, 'type', TG_OP, 'row', row_to_json(NEW))::text);\n  RETURN NEW;\nEND;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpop_apberrelevant_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpop_entwicklung_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpop_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontr_idbiotuebereinst_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontr_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontr_typ_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontrzaehl_einheit_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontrzaehl_methode_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopkontrzaehl_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopmassn_erfbeurt_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopmassn_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopmassn_typ_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.tpopmassnber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ziel_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.ziel_typ_werte_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION public.zielber_on_update_set_mut() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\n  BEGIN\n    NEW.changed_by = current_setting('request.jwt.claim.username', true);\n    NEW.changed = NOW();\n    RETURN NEW;\n  END;\n$$;\n\n\n\n\nCREATE FUNCTION request.cookie(c text) RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select request.env_var('request.cookie.' || c);\n$$;\n\n\n\n\nCREATE FUNCTION request.env_var(v text) RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select current_setting(v, true);\n$$;\n\n\n\n\nCREATE FUNCTION request.header(h text) RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select request.env_var('request.header.' || h);\n$$;\n\n\n\n\nCREATE FUNCTION request.jwt_claim(c text) RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select request.env_var('request.jwt.claim.' || c);\n$$;\n\n\n\n\nCREATE FUNCTION request.user_name() RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select case request.jwt_claim('username')\n    when '' then ''\n    else request.jwt_claim('username')::text\n end\n$$;\n\n\n\n\nCREATE FUNCTION request.user_role() RETURNS text\n    LANGUAGE sql STABLE\n    AS $$\n    select request.jwt_claim('role')::text;\n$$;\n\n\n\nSET default_tablespace = '';\n\nSET default_with_oids = false;\n\n\nCREATE TABLE apflora._variable (\n    \"KonstId\" integer NOT NULL,\n    apber_jahr smallint,\n    \"ApArtId\" integer\n);\n\n\n\n\nCOMMENT ON COLUMN apflora._variable.apber_jahr IS 'Von Access aus ein Berichtsjahr wählen, um die Erstellung des Jahresberichts zu beschleunigen';\n\n\n\nCOMMENT ON COLUMN apflora._variable.\"ApArtId\" IS 'Von Access aus eine Art wählen, um views zu beschleunigen';\n\n\n\nCREATE SEQUENCE apflora.\"_variable_KonstId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"_variable_KonstId_seq\" OWNED BY apflora._variable.\"KonstId\";\n\n\n\nCREATE TABLE apflora.adresse (\n    id_old integer,\n    name text,\n    adresse text,\n    telefon text,\n    email text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    evab_id_person uuid,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    freiw_erfko boolean DEFAULT false,\n    evab_nachname character varying(50) DEFAULT NULL::character varying,\n    evab_vorname character varying(50) DEFAULT NULL::character varying,\n    evab_ort character varying(50) DEFAULT NULL::character varying\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.adresse.id_old IS 'Frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.name IS 'Vor- und Nachname';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.adresse IS 'Strasse, PLZ und Ort';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.telefon IS 'Telefonnummer';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.email IS 'Email';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.evab_id_person IS 'Personen werden in EvAB separat und mit eigener ID erfasst. Daher muss die passende Person hier gewählt werden';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.adresse.freiw_erfko IS 'Ist die Person freiwillige(r) Kontrolleur(in)';\n\n\n\nCREATE SEQUENCE apflora.\"adresse_AdrId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"adresse_AdrId_seq\" OWNED BY apflora.adresse.id_old;\n\n\n\nCREATE TABLE apflora.ae_eigenschaften (\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    taxid integer,\n    familie character varying(100) DEFAULT NULL::character varying,\n    artname character varying(100) DEFAULT NULL::character varying,\n    namedeutsch character varying(100) DEFAULT NULL::character varying,\n    status character varying(47) DEFAULT NULL::character varying,\n    artwert smallint,\n    kefkontrolljahr smallint,\n    fnsjahresartjahr smallint,\n    kefart boolean DEFAULT false\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ae_eigenschaften.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.ae_lrdelarze (\n    sort integer NOT NULL,\n    label character varying(50) DEFAULT NULL::character varying,\n    einheit character varying(255) DEFAULT NULL::character varying,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ae_lrdelarze.sort IS 'Primärschlüssel der Tabelle ArtenDb_LR';\n\n\n\nCOMMENT ON COLUMN apflora.ae_lrdelarze.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.ap (\n    id_old integer,\n    bearbeitung integer,\n    start_jahr smallint,\n    umsetzung integer,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    art_id uuid,\n    bearbeiter uuid,\n    proj_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ap.id_old IS 'Frühere id. = SISF2-Nr';\n\n\n\nCOMMENT ON COLUMN apflora.ap.bearbeitung IS 'In welchem Bearbeitungsstand befindet sich der AP?';\n\n\n\nCOMMENT ON COLUMN apflora.ap.start_jahr IS 'Wann wurde mit der Umsetzung des Aktionsplans begonnen?';\n\n\n\nCOMMENT ON COLUMN apflora.ap.umsetzung IS 'In welchem Umsetzungsstand befindet sich der AP?';\n\n\n\nCOMMENT ON COLUMN apflora.ap.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.ap.art_id IS 'Namensgebende Art. Unter ihrem Namen bzw. Nummer werden Kontrollen an InfoFlora geliefert';\n\n\n\nCOMMENT ON COLUMN apflora.ap.bearbeiter IS 'Zugehöriger Bearbeiter. Fremdschlüssel aus der Tabelle \"adresse\"';\n\n\n\nCOMMENT ON COLUMN apflora.ap.proj_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"projekt\"';\n\n\n\nCREATE SEQUENCE apflora.\"ap_ApArtId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"ap_ApArtId_seq\" OWNED BY apflora.ap.id_old;\n\n\n\nCREATE TABLE apflora.ap_bearbstand_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ap_bearbstand_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_bearbstand_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_bearbstand_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.ap_erfbeurtkrit_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfbeurtkrit_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfbeurtkrit_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfbeurtkrit_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.ap_erfkrit_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfkrit_werte.text IS 'Wie werden die durchgefuehrten Massnahmen beurteilt?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfkrit_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfkrit_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_erfkrit_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.ap_umsetzung_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ap_umsetzung_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_umsetzung_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ap_umsetzung_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.apart (\n    id_old integer,\n    changed date,\n    changed_by character varying(20) DEFAULT NULL::character varying,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid,\n    art_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.apart.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.apart.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apart.changed_by IS 'Wer hat den Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apart.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.apart.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE TABLE apflora.apber (\n    id_old integer,\n    jahr smallint,\n    situation text,\n    vergleich_vorjahr_gesamtziel text,\n    beurteilung integer,\n    veraenderung_zum_vorjahr character varying(2) DEFAULT NULL::character varying,\n    apber_analyse text,\n    konsequenzen_umsetzung text,\n    konsequenzen_erfolgskontrolle text,\n    biotope_neue text,\n    biotope_optimieren text,\n    massnahmen_optimieren text,\n    wirkung_auf_art text,\n    datum date,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    massnahmen_ap_bearb text,\n    massnahmen_planung_vs_ausfuehrung text,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid,\n    bearbeiter uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.apber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.apber.jahr IS 'Für welches Jahr gilt der Bericht?';\n\n\n\nCOMMENT ON COLUMN apflora.apber.situation IS 'Beschreibung der Situation im Berichtjahr. Seit 2017 nicht mehr verwendet: Früher wurden hier die Massnahmen aufgelistet';\n\n\n\nCOMMENT ON COLUMN apflora.apber.vergleich_vorjahr_gesamtziel IS 'Vergleich zu Vorjahr und Ausblick auf das Gesamtziel';\n\n\n\nCOMMENT ON COLUMN apflora.apber.beurteilung IS 'Beurteilung des Erfolgs des Aktionsplans bisher';\n\n\n\nCOMMENT ON COLUMN apflora.apber.veraenderung_zum_vorjahr IS 'Veränderung gegenüber dem Vorjahr: plus heisst aufgestiegen, minus heisst abgestiegen';\n\n\n\nCOMMENT ON COLUMN apflora.apber.apber_analyse IS 'Was sind die Ursachen fuer die beobachtete Entwicklung?';\n\n\n\nCOMMENT ON COLUMN apflora.apber.konsequenzen_umsetzung IS 'Konsequenzen für die Umsetzung';\n\n\n\nCOMMENT ON COLUMN apflora.apber.konsequenzen_erfolgskontrolle IS 'Konsequenzen für die Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.apber.biotope_neue IS 'Bemerkungen zum Aussagebereich A: Grundmengen und getroffene Massnahmen';\n\n\n\nCOMMENT ON COLUMN apflora.apber.biotope_optimieren IS 'Bemerkungen zum Aussagebereich B: Bestandeskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.apber.massnahmen_optimieren IS 'Bemerkungen zum Aussagebereich C: Zwischenbilanz zur Wirkung von Massnahmen';\n\n\n\nCOMMENT ON COLUMN apflora.apber.wirkung_auf_art IS 'Bemerkungen zum Aussagebereich D: Einschätzung der Wirkung des AP insgesamt pro Art';\n\n\n\nCOMMENT ON COLUMN apflora.apber.datum IS 'Datum der Nachführung';\n\n\n\nCOMMENT ON COLUMN apflora.apber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apber.massnahmen_ap_bearb IS 'Bemerkungen zum Aussagebereich C: Weitere Aktivitäten der Aktionsplan-Verantwortlichen';\n\n\n\nCOMMENT ON COLUMN apflora.apber.massnahmen_planung_vs_ausfuehrung IS 'Bemerkungen zum Aussagebereich C: Vergleich Ausführung/Planung';\n\n\n\nCOMMENT ON COLUMN apflora.apber.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.apber.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCOMMENT ON COLUMN apflora.apber.bearbeiter IS 'Zugehöriger Bearbeiter. Fremdschlüssel aus der Tabelle \"adresse\"';\n\n\n\nCREATE SEQUENCE apflora.\"apber_JBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"apber_JBerId_seq\" OWNED BY apflora.apber.id_old;\n\n\n\nCREATE TABLE apflora.apberuebersicht (\n    jahr smallint,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id_old integer,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    proj_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.jahr IS 'Berichtsjahr. Zusammen mit proj_id eindeutig';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.bemerkungen IS 'Bemerkungen zur Artübersicht';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.apberuebersicht.proj_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"projekt\"';\n\n\n\nCREATE SEQUENCE apflora.apberuebersicht_id_seq\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.apberuebersicht_id_seq OWNED BY apflora.apberuebersicht.id_old;\n\n\n\nCREATE TABLE apflora.assozart (\n    id_old integer,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ae_id uuid,\n    ap_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.assozart.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.assozart.bemerkungen IS 'Bemerkungen zur Assoziation';\n\n\n\nCOMMENT ON COLUMN apflora.assozart.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.assozart.changed_by IS 'Wer hat den Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.assozart.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.assozart.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE SEQUENCE apflora.\"assozart_AaId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"assozart_AaId_seq\" OWNED BY apflora.assozart.id_old;\n\n\n\nCREATE TABLE apflora.beob (\n    id_old integer,\n    id_field character varying(38) DEFAULT NULL::character varying,\n    art_id_old integer,\n    datum date,\n    autor character varying(100) DEFAULT NULL::character varying,\n    x integer,\n    y integer,\n    data jsonb,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    art_id uuid,\n    tpop_id uuid,\n    nicht_zuordnen boolean DEFAULT false,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    quelle_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.beob.id_old IS 'Frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.beob.art_id_old IS 'Frühere Art id (=SISF2-Nr)';\n\n\n\nCOMMENT ON COLUMN apflora.beob.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.beob.tpop_id IS 'Dieser Teilpopulation wurde die Beobachtung zugeordnet. Fremdschlüssel aus der Tabelle \"tpop\"';\n\n\n\nCOMMENT ON COLUMN apflora.beob.nicht_zuordnen IS 'Wird ja gesetzt, wenn eine Beobachtung keiner Teilpopulation zugeordnet werden kann. Sollte im Bemerkungsfeld begründet werden. In der Regel ist die Artbestimmung zweifelhaft. Oder die Beobachtung ist nicht (genau genug) lokalisierbar';\n\n\n\nCOMMENT ON COLUMN apflora.beob.bemerkungen IS 'Bemerkungen zur Zuordnung';\n\n\n\nCOMMENT ON COLUMN apflora.beob.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.beob.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.beob.quelle_id IS 'Zugehörige Beobachtungs-Quelle. Fremdschlüssel aus der Tabelle \"beob_quelle_werte\"';\n\n\n\nCREATE SEQUENCE apflora.beob_id_seq\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.beob_id_seq OWNED BY apflora.beob.id_old;\n\n\n\nCREATE TABLE apflora.beob_quelle_werte (\n    id_old integer,\n    name character varying(255) DEFAULT NULL::character varying,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCREATE SEQUENCE apflora.\"beobart_BeobArtId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"beobart_BeobArtId_seq\" OWNED BY apflora.apart.id_old;\n\n\n\nCREATE TABLE apflora.ber (\n    id_old integer,\n    autor character varying(150) DEFAULT NULL::character varying,\n    jahr smallint,\n    titel text,\n    url text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ber.id_old IS 'Primärschlüssel der Tabelle \"ber\"';\n\n\n\nCOMMENT ON COLUMN apflora.ber.autor IS 'Autor des Berichts';\n\n\n\nCOMMENT ON COLUMN apflora.ber.jahr IS 'Jahr der Publikation';\n\n\n\nCOMMENT ON COLUMN apflora.ber.titel IS 'Titel des Berichts';\n\n\n\nCOMMENT ON COLUMN apflora.ber.url IS 'Link zum Bericht';\n\n\n\nCOMMENT ON COLUMN apflora.ber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ber.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.ber.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE SEQUENCE apflora.\"ber_BerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"ber_BerId_seq\" OWNED BY apflora.ber.id_old;\n\n\n\nCREATE TABLE apflora.erfkrit (\n    id_old integer,\n    erfolg integer,\n    kriterien text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.erfolg IS 'Wie gut werden die Ziele erreicht? Auswahl aus der Tabelle \"ap_erfkrit_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.kriterien IS 'Beschreibung der Kriterien für den Erfolg';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.erfkrit.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE SEQUENCE apflora.\"erfkrit_ErfkritId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"erfkrit_ErfkritId_seq\" OWNED BY apflora.erfkrit.id_old;\n\n\n\nCREATE TABLE apflora.evab_typologie (\n    \"TYPO\" character varying(9) NOT NULL,\n    \"LEBENSRAUM\" character varying(100),\n    \"Alliance\" character varying(100)\n);\n\n\n\n\nCREATE TABLE apflora.gemeinde (\n    name character varying(50) DEFAULT NULL::character varying,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCREATE TABLE apflora.idealbiotop (\n    erstelldatum date,\n    hoehenlage text,\n    region text,\n    exposition text,\n    besonnung text,\n    hangneigung text,\n    boden_typ text,\n    boden_kalkgehalt text,\n    boden_durchlaessigkeit text,\n    boden_humus text,\n    boden_naehrstoffgehalt text,\n    wasserhaushalt text,\n    konkurrenz text,\n    moosschicht text,\n    krautschicht text,\n    strauchschicht text,\n    baumschicht text,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.erstelldatum IS 'Erstelldatum';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.hoehenlage IS 'Höhenlage';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.region IS 'Region';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.exposition IS 'Exposition';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.besonnung IS 'Besonnung';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.hangneigung IS 'Hangneigung';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.boden_typ IS 'Bodentyp';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.boden_kalkgehalt IS 'Kalkgehalt im Boden';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.boden_durchlaessigkeit IS 'Bodendurchlässigkeit';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.boden_humus IS 'Bodenhumusgehalt';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.boden_naehrstoffgehalt IS 'Bodennährstoffgehalt';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.wasserhaushalt IS 'Wasserhaushalt';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.konkurrenz IS 'Konkurrenz';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.moosschicht IS 'Moosschicht';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.krautschicht IS 'Krautschicht';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.strauchschicht IS 'Strauchschicht';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.baumschicht IS 'Baumschicht';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.bemerkungen IS 'Bemerkungen';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.changed IS 'Wann wurde der Datensatz zuletzt verändert?';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.changed_by IS 'Wer hat den Datensatz zuletzt verändert?';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.idealbiotop.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE TABLE apflora.message (\n    id_old integer,\n    message text NOT NULL,\n    \"time\" timestamp without time zone DEFAULT now() NOT NULL,\n    active boolean DEFAULT true NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.message.message IS 'Nachricht an die Benutzer';\n\n\n\nCREATE SEQUENCE apflora.message_id_seq\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.message_id_seq OWNED BY apflora.message.id_old;\n\n\n\nCREATE TABLE apflora.pop (\n    id_old integer,\n    nr integer,\n    name character varying(150) DEFAULT NULL::character varying,\n    status integer,\n    status_unklar_begruendung text,\n    bekannt_seit smallint,\n    x integer,\n    y integer,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid,\n    status_unklar boolean DEFAULT false,\n    CONSTRAINT zulaessige_x_koordinate CHECK (((x IS NULL) OR ((x > 2485071) AND (x < 2828516)))),\n    CONSTRAINT zulaessige_y_koordinate CHECK (((y IS NULL) OR ((y > 1075346) AND (y < 1299942))))\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.pop.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.pop.nr IS 'Nummer der Population';\n\n\n\nCOMMENT ON COLUMN apflora.pop.name IS 'Bezeichnung der Population';\n\n\n\nCOMMENT ON COLUMN apflora.pop.status IS 'Herkunft der Population: autochthon oder angesiedelt? Auswahl aus der Tabelle \"pop_status_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.pop.status_unklar_begruendung IS 'Begründung, wieso die Herkunft unklar ist';\n\n\n\nCOMMENT ON COLUMN apflora.pop.bekannt_seit IS 'Seit wann ist die Population bekannt?';\n\n\n\nCOMMENT ON COLUMN apflora.pop.x IS 'Wird in der Regel von einer Teilpopulation übernommen';\n\n\n\nCOMMENT ON COLUMN apflora.pop.y IS 'Wird in der Regel von einer Teilpopulation übernommen';\n\n\n\nCOMMENT ON COLUMN apflora.pop.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.pop.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.pop.id IS 'Primärschlüssel der Tabelle \"pop\"';\n\n\n\nCOMMENT ON COLUMN apflora.pop.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCOMMENT ON COLUMN apflora.pop.status_unklar IS 'true = die Herkunft der Population ist unklar';\n\n\n\nCREATE SEQUENCE apflora.\"pop_PopId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"pop_PopId_seq\" OWNED BY apflora.pop.id_old;\n\n\n\nCREATE TABLE apflora.pop_status_werte (\n    code integer,\n    text character varying(60) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.pop_status_werte.text IS 'Beschreibung der Herkunft';\n\n\n\nCOMMENT ON COLUMN apflora.pop_status_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.pop_status_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.pop_status_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.popber (\n    id_old integer,\n    jahr smallint,\n    entwicklung integer,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    pop_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.popber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.popber.jahr IS 'Für welches Jahr gilt der Bericht?';\n\n\n\nCOMMENT ON COLUMN apflora.popber.entwicklung IS 'Beurteilung der Populationsentwicklung: Auswahl aus Tabelle \"tpop_entwicklung_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.popber.bemerkungen IS 'Bemerkungen zur Beurteilung';\n\n\n\nCOMMENT ON COLUMN apflora.popber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.popber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.popber.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.popber.pop_id IS 'Zugehörige Population. Fremdschlüssel aus der Tabelle \"pop\"';\n\n\n\nCREATE SEQUENCE apflora.\"popber_PopBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"popber_PopBerId_seq\" OWNED BY apflora.popber.id_old;\n\n\n\nCREATE TABLE apflora.popmassnber (\n    id_old integer,\n    jahr smallint,\n    beurteilung integer,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    pop_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.jahr IS 'Für welches Jahr gilt der Bericht?';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.beurteilung IS 'Wie wird die Wirkung aller im Rahmen des AP durchgeführten Massnahmen beurteilt?';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.bemerkungen IS 'Bemerkungen zur Beurteilung';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.popmassnber.pop_id IS 'Zugehörige Population. Fremdschlüssel aus der Tabelle \"pop\"';\n\n\n\nCREATE SEQUENCE apflora.\"popmassnber_PopMassnBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"popmassnber_PopMassnBerId_seq\" OWNED BY apflora.popmassnber.id_old;\n\n\n\nCREATE TABLE apflora.projekt (\n    id_old integer,\n    name character varying(150) DEFAULT NULL::character varying,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.projekt.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.projekt.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCREATE SEQUENCE apflora.\"projekt_ProjId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"projekt_ProjId_seq\" OWNED BY apflora.projekt.id_old;\n\n\n\nCREATE TABLE apflora.tpop (\n    id_old integer,\n    nr integer,\n    gemeinde text,\n    flurname text,\n    x integer,\n    y integer,\n    radius smallint,\n    hoehe smallint,\n    exposition character varying(50) DEFAULT NULL::character varying,\n    klima character varying(50) DEFAULT NULL::character varying,\n    neigung character varying(50) DEFAULT NULL::character varying,\n    beschreibung text,\n    kataster_nr text,\n    status integer,\n    status_unklar_grund text,\n    apber_relevant integer,\n    bekannt_seit smallint,\n    eigentuemer text,\n    kontakt text,\n    nutzungszone text,\n    bewirtschafter text,\n    bewirtschaftung text,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    pop_id uuid,\n    status_unklar boolean DEFAULT false,\n    CONSTRAINT zulaessige_x_koordinate CHECK (((x IS NULL) OR ((x > 2485071) AND (x < 2828516)))),\n    CONSTRAINT zulaessige_y_koordinate CHECK (((y IS NULL) OR ((y > 1075346) AND (y < 1299942))))\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpop.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.nr IS 'Nummer der Teilpopulation';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.gemeinde IS 'Gemeinde';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.flurname IS 'Flurname';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.x IS 'X-Koordinate';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.y IS 'Y-Koordinate';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.radius IS 'Radius der Teilpopulation (m)';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.hoehe IS 'Höhe über Meer (m)';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.exposition IS 'Exposition / Besonnung des Standorts';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.klima IS 'Klima des Standorts';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.neigung IS 'Hangneigung des Standorts';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.beschreibung IS 'Beschreibung der Fläche';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.kataster_nr IS 'Kataster-Nummer';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.status IS 'Herkunft der Teilpopulation. Auswahl aus Tabelle \"pop_status_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.status_unklar_grund IS 'Wieso ist der Status unklar?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.apber_relevant IS 'Ist die Teilpopulation für den AP-Bericht relevant? Auswahl aus der Tabelle \"tpop_apberrelevant_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.bekannt_seit IS 'Seit wann ist die Teilpopulation bekannt?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.eigentuemer IS 'EigentümerIn';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.kontakt IS 'Kontaktperson vor Ort';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.nutzungszone IS 'Nutzungszone';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.bewirtschafter IS 'Wer bewirtschaftet die Fläche?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.bewirtschaftung IS 'Wie wird die Fläche bewirtschaftet?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.bemerkungen IS 'Bemerkungen zur Teilpopulation';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.changed IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.pop_id IS 'Zugehörige Population. Fremdschlüssel aus der Tabelle \"pop\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpop.status_unklar IS 'Ist der Status der Teilpopulation unklar? (es bestehen keine glaubwuerdigen Beboachtungen)';\n\n\n\nCREATE SEQUENCE apflora.\"tpop_TPopId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpop_TPopId_seq\" OWNED BY apflora.tpop.id_old;\n\n\n\nCREATE TABLE apflora.tpop_apberrelevant_werte (\n    code integer,\n    text text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpop_apberrelevant_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop_apberrelevant_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop_apberrelevant_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpop_entwicklung_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpop_entwicklung_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop_entwicklung_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpop_entwicklung_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopber (\n    id_old integer,\n    jahr smallint,\n    entwicklung integer,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    tpop_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.jahr IS 'Für welches Jahr gilt der Bericht?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.entwicklung IS 'Beurteilung der Populationsentwicklung: Auswahl aus Tabelle \"tpop_entwicklung_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.bemerkungen IS 'Bemerkungen zur Beurteilung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopber.tpop_id IS 'Zugehörige Teilpopulation. Fremdschlüssel der Tabelle \"tpop\"';\n\n\n\nCREATE SEQUENCE apflora.\"tpopber_TPopBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpopber_TPopBerId_seq\" OWNED BY apflora.tpopber.id_old;\n\n\n\nCREATE TABLE apflora.tpopkontr (\n    id_old integer,\n    typ character varying(50) DEFAULT NULL::character varying,\n    datum date,\n    jahr smallint,\n    jungpflanzen_anzahl integer,\n    vitalitaet text,\n    ueberlebensrate smallint,\n    entwicklung integer,\n    ursachen text,\n    erfolgsbeurteilung text,\n    umsetzung_aendern text,\n    kontrolle_aendern text,\n    bemerkungen text,\n    lr_delarze text,\n    flaeche integer,\n    lr_umgebung_delarze text,\n    vegetationstyp character varying(100) DEFAULT NULL::character varying,\n    konkurrenz character varying(100) DEFAULT NULL::character varying,\n    moosschicht character varying(100) DEFAULT NULL::character varying,\n    krautschicht character varying(100) DEFAULT NULL::character varying,\n    strauchschicht text,\n    baumschicht character varying(100) DEFAULT NULL::character varying,\n    boden_typ text,\n    boden_kalkgehalt character varying(100) DEFAULT NULL::character varying,\n    boden_durchlaessigkeit character varying(100) DEFAULT NULL::character varying,\n    boden_humus character varying(100) DEFAULT NULL::character varying,\n    boden_naehrstoffgehalt character varying(100) DEFAULT NULL::character varying,\n    boden_abtrag text,\n    wasserhaushalt text,\n    idealbiotop_uebereinstimmung integer,\n    handlungsbedarf text,\n    flaeche_ueberprueft integer,\n    deckung_vegetation smallint,\n    deckung_nackter_boden smallint,\n    deckung_ap_art smallint,\n    vegetationshoehe_maximum smallint,\n    vegetationshoehe_mittel smallint,\n    gefaehrdung text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    zeit_id uuid DEFAULT public.uuid_generate_v1mc(),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    tpop_id uuid,\n    bearbeiter uuid,\n    plan_vorhanden boolean DEFAULT false,\n    jungpflanzen_vorhanden boolean\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.typ IS 'Typ der Kontrolle. Auswahl aus Tabelle \"tpopkontr_typ_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.datum IS 'Wann wurde kontrolliert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.jahr IS 'In welchem Jahr wurde kontrolliert? Für welches Jahr gilt die Beschreibung?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.jungpflanzen_anzahl IS 'Anzahl Jungpflanzen';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.vitalitaet IS 'Vitalität der Pflanzen';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.ueberlebensrate IS 'Überlebensrate in Prozent';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.entwicklung IS 'Entwicklung des Bestandes. Auswahl aus Tabelle \"tpop_entwicklung_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.ursachen IS 'Ursachen der Entwicklung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.erfolgsbeurteilung IS 'Erfolgsbeurteilung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.umsetzung_aendern IS 'Vorschlag für Änderung der Umsetzung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.kontrolle_aendern IS 'Vorschlag für Änderung der Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.bemerkungen IS 'Bemerkungen zur Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.lr_delarze IS 'Lebensraumtyp nach Delarze';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.flaeche IS 'Fläche der Teilpopulation';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.lr_umgebung_delarze IS 'Lebensraumtyp der direkt angrenzenden Umgebung (nach Delarze)';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.vegetationstyp IS 'Vegetationstyp';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.konkurrenz IS 'Konkurrenz';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.moosschicht IS 'Moosschicht';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.krautschicht IS 'Krautschicht';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.strauchschicht IS 'Strauchschicht, ehemals Verbuschung (%)';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.baumschicht IS 'Baumschicht';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_typ IS 'Bodentyp';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_kalkgehalt IS 'Kalkgehalt des Bodens';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_durchlaessigkeit IS 'Durchlässigkeit des Bodens';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_humus IS 'Humusgehalt des Bodens';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_naehrstoffgehalt IS 'Nährstoffgehalt des Bodens';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.boden_abtrag IS 'Oberbodenabtrag';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.wasserhaushalt IS 'Wasserhaushalt';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.idealbiotop_uebereinstimmung IS 'Übereinstimmung mit dem Idealbiotop';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.handlungsbedarf IS 'Handlungsbedarf bezüglich Biotop';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.flaeche_ueberprueft IS 'Überprüfte Fläche in m2. Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.deckung_vegetation IS 'Von Pflanzen, Streu oder Moos bedeckter Boden (%). Nur für Freiwilligen-Erfolgskontrolle. Nur bis 2012 erfasst.';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.deckung_nackter_boden IS 'Flächenanteil nackter Boden (%). Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.deckung_ap_art IS 'Flächenanteil der überprüften Pflanzenart (%). Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.vegetationshoehe_maximum IS 'Maximale Vegetationshöhe in cm. Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.vegetationshoehe_mittel IS 'Mittlere Vegetationshöhe in cm. Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.gefaehrdung IS 'Gefährdung. Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.zeit_id IS 'GUID für den Export von Zeiten in EvAB';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.id IS 'Primärschlüssel. Wird u.a. verwendet für die Identifikation der Beobachtung im nationalen Beobachtungs-Daten-Kreislauf';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.tpop_id IS 'Zugehörige Teilpopulation. Fremdschlüssel der Tabelle \"tpop\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.bearbeiter IS 'Zugehöriger Bearbeiter. Fremdschlüssel aus der Tabelle \"adresse\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.plan_vorhanden IS 'Fläche / Wuchsort auf Plan eingezeichnet? Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr.jungpflanzen_vorhanden IS 'Gibt es neben alten Pflanzen auch junge? Nur für Freiwilligen-Erfolgskontrolle';\n\n\n\nCREATE SEQUENCE apflora.\"tpopkontr_TPopKontrId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpopkontr_TPopKontrId_seq\" OWNED BY apflora.tpopkontr.id_old;\n\n\n\nCREATE TABLE apflora.tpopkontr_idbiotuebereinst_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_idbiotuebereinst_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_idbiotuebereinst_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_idbiotuebereinst_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopkontr_typ_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_typ_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_typ_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontr_typ_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopkontrzaehl (\n    id_old integer,\n    anzahl integer,\n    einheit integer,\n    methode integer,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    tpopkontr_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.anzahl IS 'Anzahl Zaehleinheiten';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.einheit IS 'Verwendete Zaehleinheit. Auswahl aus Tabelle \"tpopkontrzaehl_einheit_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.methode IS 'Verwendete Methodik. Auswahl aus Tabelle \"tpopkontrzaehl_methode_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCREATE SEQUENCE apflora.\"tpopkontrzaehl_TPopKontrZaehlId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpopkontrzaehl_TPopKontrZaehlId_seq\" OWNED BY apflora.tpopkontrzaehl.id_old;\n\n\n\nCREATE TABLE apflora.tpopkontrzaehl_einheit_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_einheit_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_einheit_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_einheit_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopkontrzaehl_methode_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_methode_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_methode_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopkontrzaehl_methode_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopmassn (\n    id_old integer,\n    typ integer,\n    beschreibung text,\n    jahr smallint,\n    datum date,\n    bemerkungen text,\n    plan_bezeichnung text,\n    flaeche integer,\n    markierung text,\n    anz_triebe integer,\n    anz_pflanzen integer,\n    anz_pflanzstellen integer,\n    wirtspflanze text,\n    herkunft_pop text,\n    sammeldatum character varying(50) DEFAULT NULL::character varying,\n    form text,\n    pflanzanordnung text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    tpop_id uuid,\n    bearbeiter uuid,\n    plan_vorhanden boolean DEFAULT false\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.typ IS 'Typ der Massnahme. Auswahl aus Tabelle \"tpopmassn_typ_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.beschreibung IS 'Was wurde gemacht? V.a. für Typ \"Spezial\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.jahr IS 'Jahr, in dem die Massnahme durchgeführt wurde';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.datum IS 'Datum, an dem die Massnahme durchgeführt wurde';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.bemerkungen IS 'Bemerkungen zur Massnahme';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.plan_bezeichnung IS 'Bezeichnung auf dem Plan';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.flaeche IS 'Fläche der Massnahme bzw. Teilpopulation (m2)';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.markierung IS 'Markierung der Massnahme bzw. Teilpopulation';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.anz_triebe IS 'Anzahl angesiedelte Triebe';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.anz_pflanzen IS 'Anzahl angesiedelte Pflanzen';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.anz_pflanzstellen IS 'Anzahl Töpfe/Pflanzstellen';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.wirtspflanze IS 'Wirtspflanze';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.herkunft_pop IS 'Aus welcher Population stammt das Pflanzenmaterial?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.sammeldatum IS 'Datum, an dem die angesiedelten Pflanzen gesammelt wurden';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.form IS 'Form, Grösse der Ansiedlung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.pflanzanordnung IS 'Anordnung der Pflanzung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.tpop_id IS 'Zugehörige Teilpopulation. Fremdschlüssel der Tabelle \"tpop\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.bearbeiter IS 'Zugehöriger Bearbeiter. Fremdschlüssel aus der Tabelle \"adresse\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn.plan_vorhanden IS 'Existiert ein Plan?';\n\n\n\nCREATE SEQUENCE apflora.\"tpopmassn_TPopMassnId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpopmassn_TPopMassnId_seq\" OWNED BY apflora.tpopmassn.id_old;\n\n\n\nCREATE TABLE apflora.tpopmassn_erfbeurt_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_erfbeurt_werte.text IS 'Wie werden die durchgefuehrten Massnahmen beurteilt?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_erfbeurt_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_erfbeurt_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_erfbeurt_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopmassn_typ_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    ansiedlung smallint NOT NULL,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_typ_werte.ansiedlung IS 'Handelt es sich um eine Ansiedlung?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_typ_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_typ_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassn_typ_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE TABLE apflora.tpopmassnber (\n    id_old integer,\n    jahr smallint,\n    beurteilung integer,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    tpop_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.jahr IS 'Jahr, für den der Bericht gilt';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.beurteilung IS 'Beurteilung des Erfolgs. Auswahl aus Tabelle \"tpopmassn_erfbeurt_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.bemerkungen IS 'Bemerkungen zur Beurteilung';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.tpopmassnber.tpop_id IS 'Zugehörige Teilpopulation. Fremdschlüssel der Tabelle \"tpop\"';\n\n\n\nCREATE SEQUENCE apflora.\"tpopmassnber_TPopMassnBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"tpopmassnber_TPopMassnBerId_seq\" OWNED BY apflora.tpopmassnber.id_old;\n\n\n\nCREATE TABLE apflora.\"user\" (\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    name text,\n    email text,\n    role name,\n    pass text,\n    CONSTRAINT proper_email CHECK ((email ~* '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'::text)),\n    CONSTRAINT user_pass_check CHECK ((length(pass) > 5)),\n    CONSTRAINT user_role_check CHECK ((length((role)::text) < 512))\n);\n\n\n\n\nCREATE TABLE apflora.usermessage (\n    user_name character varying(30) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    message_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.usermessage.message_id IS 'Zugehörige Nachricht. Fremdschlüssel aus der Tabelle \"message\"';\n\n\n\nCREATE TABLE apflora.ziel (\n    id_old integer,\n    typ integer,\n    jahr smallint DEFAULT 1 NOT NULL,\n    bezeichnung text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ap_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ziel.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.typ IS 'Typ des Ziels. Z.B. Zwischenziel, Gesamtziel. Auswahl aus Tabelle \"ziel_typ_werte\"';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.jahr IS 'In welchem Jahr soll das Ziel erreicht werden?';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.bezeichnung IS 'Textliche Beschreibung des Ziels';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.ziel.ap_id IS 'Zugehöriger Aktionsplan. Fremdschlüssel aus der Tabelle \"ap\"';\n\n\n\nCREATE TABLE apflora.ziel_typ_werte (\n    code integer,\n    text character varying(50) DEFAULT NULL::character varying,\n    sort smallint,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true) NOT NULL,\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.ziel_typ_werte.text IS 'Beschreibung des Ziels';\n\n\n\nCOMMENT ON COLUMN apflora.ziel_typ_werte.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ziel_typ_werte.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.ziel_typ_werte.id IS 'Primärschlüssel';\n\n\n\nCREATE VIEW apflora.v_abper_ziel AS\n SELECT ziel.id_old,\n    ziel.typ,\n    ziel.jahr,\n    ziel.bezeichnung,\n    ziel.changed,\n    ziel.changed_by,\n    ziel.id,\n    ziel.ap_id,\n    ziel_typ_werte.text AS typ_decodiert\n   FROM (apflora._variable\n     JOIN (apflora.ziel\n     JOIN apflora.ziel_typ_werte ON ((ziel.typ = ziel_typ_werte.code))) ON ((_variable.apber_jahr = ziel.jahr)))\n  WHERE (ziel.typ = ANY (ARRAY[1, 2, 1170775556]))\n  ORDER BY ziel_typ_werte.sort, ziel.bezeichnung;\n\n\n\n\nCREATE VIEW apflora.v_ap AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS bearbeitung,\n    ap.start_jahr,\n    ap_umsetzung_werte.text AS umsetzung,\n    adresse.name AS bearbeiter,\n    ap.changed,\n    ap.changed_by\n   FROM ((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzkontr AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS bearbeitung,\n    ap.start_jahr,\n    ap_umsetzung_werte.text AS umsetzung,\n    count(tpopkontr.id) AS anzahl_kontrollen\n   FROM ((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN ((apflora.pop\n     LEFT JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     LEFT JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n  GROUP BY ap.id, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzkontrinjahr AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    tpopkontr.id AS tpopkontr_id,\n    tpopkontr.jahr AS tpopkontr_jahr\n   FROM ((apflora.ap\n     JOIN apflora.ae_eigenschaften ON ((ap.art_id = ae_eigenschaften.id)))\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  GROUP BY ap.id, ae_eigenschaften.artname, tpopkontr.id, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzmassn AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS bearbeitung,\n    ap.start_jahr,\n    ap_umsetzung_werte.text AS umsetzung,\n    count(tpopmassn.id) AS anzahl_massnahmen\n   FROM ((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN ((apflora.pop\n     LEFT JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     LEFT JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n  GROUP BY ap.id, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzmassnprojahr0 AS\n SELECT ap.id,\n    tpopmassn.jahr,\n    count(tpopmassn.id) AS \"AnzahlvonTPopMassnId\"\n   FROM (apflora.ap\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY ap.id, tpopmassn.jahr\n HAVING (tpopmassn.jahr IS NOT NULL)\n  ORDER BY ap.id, tpopmassn.jahr;\n\n\n\n\nCREATE VIEW apflora.v_massn_jahre AS\n SELECT tpopmassn.jahr\n   FROM apflora.tpopmassn\n  GROUP BY tpopmassn.jahr\n HAVING ((tpopmassn.jahr >= 1900) AND (tpopmassn.jahr <= 2100))\n  ORDER BY tpopmassn.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_massnjahre AS\n SELECT ap.id,\n    v_massn_jahre.jahr\n   FROM apflora.ap,\n    apflora.v_massn_jahre\n  WHERE (ap.bearbeitung < 4)\n  ORDER BY ap.id, v_massn_jahre.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzmassnprojahr AS\n SELECT v_ap_massnjahre.id,\n    v_ap_massnjahre.jahr,\n    COALESCE(v_ap_anzmassnprojahr0.\"AnzahlvonTPopMassnId\", (0)::bigint) AS anzahl_massnahmen\n   FROM (apflora.v_ap_massnjahre\n     LEFT JOIN apflora.v_ap_anzmassnprojahr0 ON (((v_ap_massnjahre.jahr = v_ap_anzmassnprojahr0.jahr) AND (v_ap_massnjahre.id = v_ap_anzmassnprojahr0.id))))\n  ORDER BY v_ap_massnjahre.id, v_ap_massnjahre.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_anzmassnbisjahr AS\n SELECT v_ap_massnjahre.id,\n    v_ap_massnjahre.jahr,\n    sum(v_ap_anzmassnprojahr.anzahl_massnahmen) AS anzahl_massnahmen\n   FROM (apflora.v_ap_massnjahre\n     JOIN apflora.v_ap_anzmassnprojahr ON ((v_ap_massnjahre.id = v_ap_anzmassnprojahr.id)))\n  WHERE (v_ap_anzmassnprojahr.jahr <= v_ap_massnjahre.jahr)\n  GROUP BY v_ap_massnjahre.id, v_ap_massnjahre.jahr\n  ORDER BY v_ap_massnjahre.id, v_ap_massnjahre.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_apberrelevant AS\n SELECT ap.id\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY ap.id;\n\n\n\n\nCREATE VIEW apflora.v_ap_apberundmassn AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS bearbeitung,\n    ap.start_jahr,\n    ap_umsetzung_werte.text AS umsetzung,\n    adresse.name AS bearbeiter,\n    ae_eigenschaften.artwert,\n    v_ap_anzmassnprojahr.jahr AS massn_jahr,\n    v_ap_anzmassnprojahr.anzahl_massnahmen AS massn_anzahl,\n    v_ap_anzmassnbisjahr.anzahl_massnahmen AS massn_anzahl_bisher,\n        CASE\n            WHEN (apber.jahr > 0) THEN 'ja'::text\n            ELSE 'nein'::text\n        END AS bericht_erstellt\n   FROM (apflora.ae_eigenschaften\n     JOIN ((((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     JOIN (apflora.v_ap_anzmassnprojahr\n     JOIN (apflora.v_ap_anzmassnbisjahr\n     LEFT JOIN apflora.apber ON (((v_ap_anzmassnbisjahr.jahr = apber.jahr) AND (v_ap_anzmassnbisjahr.id = apber.ap_id)))) ON (((v_ap_anzmassnprojahr.jahr = v_ap_anzmassnbisjahr.jahr) AND (v_ap_anzmassnprojahr.id = v_ap_anzmassnbisjahr.id)))) ON ((ap.id = v_ap_anzmassnprojahr.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  ORDER BY ae_eigenschaften.artname, v_ap_anzmassnprojahr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ap_mitmassninjahr0 AS\n SELECT ae_eigenschaften.artname,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpopmassn.jahr,\n    tpopmassn_typ_werte.text AS typ,\n    tpopmassn.beschreibung,\n    tpopmassn.datum,\n    adresse.name AS bearbeiter,\n    tpopmassn.bemerkungen,\n    tpopmassn.plan_vorhanden,\n    tpopmassn.plan_bezeichnung,\n    tpopmassn.flaeche,\n    tpopmassn.markierung,\n    tpopmassn.anz_triebe,\n    tpopmassn.anz_pflanzen,\n    tpopmassn.anz_pflanzstellen,\n    tpopmassn.wirtspflanze,\n    tpopmassn.herkunft_pop,\n    tpopmassn.sammeldatum,\n    tpopmassn.form,\n    tpopmassn.pflanzanordnung\n   FROM ((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN ((apflora.tpopmassn\n     JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_ap_ohnepop AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS bearbeitung,\n    ap.start_jahr,\n    ap_umsetzung_werte.text AS umsetzung,\n    adresse.name AS bearbeiter,\n    pop.id AS pop_id\n   FROM (((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE (pop.id IS NULL)\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_ap_tpopmassnjahr0 AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    tpopmassn.id AS tpopmassn_id,\n    tpopmassn.jahr AS tpopmassn_jahr\n   FROM ((apflora.ap\n     JOIN apflora.ae_eigenschaften ON ((ap.art_id = ae_eigenschaften.id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  GROUP BY ap.id, ae_eigenschaften.artname, tpopmassn.id, tpopmassn.jahr;\n\n\n\n\nCREATE VIEW apflora.v_apbeob AS\n SELECT beob.id_old,\n    beob.id_field,\n    beob.art_id_old,\n    beob.datum,\n    beob.autor,\n    beob.x,\n    beob.y,\n    beob.data,\n    beob.id,\n    beob.art_id,\n    beob.tpop_id,\n    beob.nicht_zuordnen,\n    beob.bemerkungen,\n    beob.changed,\n    beob.changed_by,\n    beob.quelle_id,\n    apart.ap_id,\n    beob_quelle_werte.name AS quelle\n   FROM ((apflora.beob\n     JOIN apflora.apart ON ((apart.art_id = beob.art_id)))\n     JOIN apflora.beob_quelle_werte ON ((beob_quelle_werte.id = beob.quelle_id)))\n  ORDER BY beob.datum DESC;\n\n\n\n\nCREATE VIEW apflora.v_apber AS\n SELECT ae_eigenschaften.artname,\n    apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ap_erfkrit_werte.text AS beurteilung_decodiert,\n    adresse.name AS bearbeiter_decodiert\n   FROM ((apflora.ap\n     JOIN apflora.ae_eigenschaften ON ((ap.art_id = ae_eigenschaften.id)))\n     JOIN ((apflora.apber\n     LEFT JOIN apflora.ap_erfkrit_werte ON ((apber.beurteilung = ap_erfkrit_werte.code)))\n     LEFT JOIN apflora.adresse ON ((apber.bearbeiter = adresse.id))) ON ((ap.id = apber.ap_id)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_a10lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE (pop.status = 300)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a10ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE (tpop.status = 300)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a2lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((pop.status = 100) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a2ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((pop.status <> 300) AND (tpop.status = 100) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a3lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[200, 210])) AND (tpop.apber_relevant = 1) AND ((pop.bekannt_seit < ap.start_jahr) OR (pop.bekannt_seit IS NULL) OR (ap.start_jahr IS NULL)))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a3ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status <> 300) AND (tpop.status = ANY (ARRAY[200, 210])) AND (tpop.apber_relevant = 1) AND ((tpop.bekannt_seit < ap.start_jahr) OR (tpop.bekannt_seit IS NULL) OR (ap.start_jahr IS NULL)))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a4lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[200, 210])) AND (tpop.apber_relevant = 1) AND (pop.bekannt_seit >= ap.start_jahr))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a4ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status <> 300) AND (tpop.status = ANY (ARRAY[200, 210])) AND (tpop.apber_relevant = 1) AND (tpop.bekannt_seit >= ap.start_jahr))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a5lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((pop.status = 201) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a5ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.status = 201) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a8lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE (((pop.status = 101) OR ((pop.status = 211) AND ((pop.bekannt_seit < ap.start_jahr) OR (pop.bekannt_seit IS NULL) OR (ap.start_jahr IS NULL)))) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a8ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status <> 300) AND ((tpop.status = 101) OR ((tpop.status = 211) AND ((tpop.bekannt_seit < ap.start_jahr) OR (tpop.bekannt_seit IS NULL) OR (ap.start_jahr IS NULL)))) AND (tpop.apber_relevant = 1))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a9lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[202, 211])) AND (tpop.apber_relevant = 1) AND (pop.bekannt_seit >= ap.start_jahr))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_a9ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.ap ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status <> 300) AND (tpop.status = ANY (ARRAY[202, 211])) AND (tpop.apber_relevant = 1) AND (tpop.bekannt_seit >= ap.start_jahr))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b1lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b1ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b1rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM apflora._variable,\n    ((apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300) AND (popber.jahr <= _variable.apber_jahr) AND (popber.entwicklung = ANY (ARRAY[1, 2, 3, 4, 8])))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b1rtpop AS\n SELECT pop.ap_id,\n    tpopber.tpop_id\n   FROM apflora._variable,\n    (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300) AND (tpopber.jahr <= _variable.apber_jahr) AND (tpopber.entwicklung = ANY (ARRAY[1, 2, 3, 4, 8])))\n  GROUP BY pop.ap_id, tpopber.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b2lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 3) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b2ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpopber.entwicklung = 3) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_pop_letzterpopber0 AS\n SELECT pop.ap_id,\n    pop.id,\n    popber.jahr\n   FROM apflora._variable,\n    ((apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (pop.status <> 300));\n\n\n\n\nCREATE VIEW apflora.v_pop_letzterpopber AS\n SELECT v_pop_letzterpopber0.ap_id,\n    v_pop_letzterpopber0.id,\n    max(v_pop_letzterpopber0.jahr) AS jahr\n   FROM apflora.v_pop_letzterpopber0\n  GROUP BY v_pop_letzterpopber0.ap_id, v_pop_letzterpopber0.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b2rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (((apflora.v_pop_letzterpopber\n     JOIN apflora.pop ON ((v_pop_letzterpopber.ap_id = pop.ap_id)))\n     JOIN apflora.popber ON (((pop.id = popber.pop_id) AND (v_pop_letzterpopber.id = popber.pop_id) AND (v_pop_letzterpopber.jahr = popber.jahr))))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 3) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztertpopber0 AS\n SELECT pop.ap_id,\n    tpop.id,\n    tpopber.jahr AS tpopber_jahr\n   FROM apflora._variable,\n    (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopber.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (pop.status <> 300));\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztertpopber AS\n SELECT v_tpop_letztertpopber0.ap_id,\n    v_tpop_letztertpopber0.id,\n    max(v_tpop_letztertpopber0.tpopber_jahr) AS jahr\n   FROM apflora.v_tpop_letztertpopber0\n  GROUP BY v_tpop_letztertpopber0.ap_id, v_tpop_letztertpopber0.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b2rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN (apflora.pop\n     JOIN apflora.v_tpop_letztertpopber ON ((pop.ap_id = v_tpop_letztertpopber.ap_id))) ON (((tpopber.tpop_id = v_tpop_letztertpopber.id) AND (tpopber.jahr = v_tpop_letztertpopber.jahr)))) ON (((tpop.pop_id = pop.id) AND (tpop.id = tpopber.tpop_id))))\n  WHERE ((tpopber.entwicklung = 3) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b3lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 2) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b3ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpopber.entwicklung = 2) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b3rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (((apflora.v_pop_letzterpopber\n     JOIN apflora.pop ON ((v_pop_letzterpopber.ap_id = pop.ap_id)))\n     JOIN apflora.popber ON (((pop.id = popber.pop_id) AND (v_pop_letzterpopber.id = popber.pop_id) AND (v_pop_letzterpopber.jahr = popber.jahr))))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 2) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b3rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN (apflora.pop\n     JOIN apflora.v_tpop_letztertpopber ON ((pop.ap_id = v_tpop_letztertpopber.ap_id))) ON (((tpopber.tpop_id = v_tpop_letztertpopber.id) AND (tpopber.jahr = v_tpop_letztertpopber.jahr)))) ON (((tpop.pop_id = pop.id) AND (tpop.id = tpopber.tpop_id))))\n  WHERE ((tpopber.entwicklung = 2) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b4lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 1) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b4ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpopber.entwicklung = 1) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b4rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (((apflora.v_pop_letzterpopber\n     JOIN apflora.pop ON ((v_pop_letzterpopber.ap_id = pop.ap_id)))\n     JOIN apflora.popber ON (((pop.id = popber.pop_id) AND (v_pop_letzterpopber.id = popber.pop_id) AND (v_pop_letzterpopber.jahr = popber.jahr))))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 1) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b4rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN (apflora.pop\n     JOIN apflora.v_tpop_letztertpopber ON ((pop.ap_id = v_tpop_letztertpopber.ap_id))) ON (((tpopber.tpop_id = v_tpop_letztertpopber.id) AND (tpopber.jahr = v_tpop_letztertpopber.jahr)))) ON (((tpop.pop_id = pop.id) AND (tpop.id = tpopber.tpop_id))))\n  WHERE ((tpopber.entwicklung = 1) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b5lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 4) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b5ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpopber.entwicklung = 4) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b5rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (((apflora.v_pop_letzterpopber\n     JOIN apflora.pop ON ((v_pop_letzterpopber.ap_id = pop.ap_id)))\n     JOIN apflora.popber ON (((pop.id = popber.pop_id) AND (v_pop_letzterpopber.id = popber.pop_id) AND (v_pop_letzterpopber.jahr = popber.jahr))))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE (((popber.entwicklung = 4) OR (popber.entwicklung = 9)) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b5rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN (apflora.pop\n     JOIN apflora.v_tpop_letztertpopber ON ((pop.ap_id = v_tpop_letztertpopber.ap_id))) ON (((tpopber.tpop_id = v_tpop_letztertpopber.id) AND (tpopber.jahr = v_tpop_letztertpopber.jahr)))) ON (((tpop.pop_id = pop.id) AND (tpop.id = tpopber.tpop_id))))\n  WHERE ((tpopber.entwicklung = 4) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b6lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora._variable ON ((popber.jahr = _variable.apber_jahr))) ON ((pop.id = popber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 8) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b6ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora._variable ON ((tpopber.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpopber.entwicklung = 8) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b6rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (((apflora.v_pop_letzterpopber\n     JOIN apflora.pop ON ((v_pop_letzterpopber.ap_id = pop.ap_id)))\n     JOIN apflora.popber ON (((pop.id = popber.pop_id) AND (v_pop_letzterpopber.id = popber.pop_id) AND (v_pop_letzterpopber.jahr = popber.jahr))))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((popber.entwicklung = 8) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b6rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN (apflora.pop\n     JOIN apflora.v_tpop_letztertpopber ON ((pop.ap_id = v_tpop_letztertpopber.ap_id))) ON (((tpopber.tpop_id = v_tpop_letztertpopber.id) AND (tpopber.jahr = v_tpop_letztertpopber.jahr)))) ON (((tpop.pop_id = pop.id) AND (tpop.id = tpopber.tpop_id))))\n  WHERE ((tpopber.entwicklung = 8) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b7lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_b7ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c1lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN (apflora.tpopmassn\n     JOIN apflora._variable ON ((tpopmassn.jahr = _variable.apber_jahr))) ON ((tpop.id = tpopmassn.tpop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c1ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n     JOIN apflora._variable ON ((tpopmassn.jahr = _variable.apber_jahr)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c1rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM apflora._variable,\n    ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n  WHERE ((tpopmassn.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c1rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM apflora._variable,\n    ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n  WHERE ((tpopmassn.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (pop.status <> 300) AND (tpop.status <> 300))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_pop_letztermassnber0 AS\n SELECT pop.ap_id,\n    pop.id,\n    popmassnber.jahr\n   FROM apflora._variable,\n    (((apflora.pop\n     JOIN apflora.popmassnber ON ((pop.id = popmassnber.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n  WHERE ((popmassnber.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (tpopmassn.jahr <= _variable.apber_jahr) AND (pop.status <> 300));\n\n\n\n\nCREATE VIEW apflora.v_pop_letztermassnber AS\n SELECT v_pop_letztermassnber0.ap_id,\n    v_pop_letztermassnber0.id,\n    max(v_pop_letztermassnber0.jahr) AS jahr\n   FROM apflora.v_pop_letztermassnber0\n  GROUP BY v_pop_letztermassnber0.ap_id, v_pop_letztermassnber0.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c3rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.v_pop_letztermassnber\n     JOIN apflora.pop ON ((v_pop_letztermassnber.ap_id = pop.ap_id)))\n     JOIN apflora.popmassnber ON (((pop.id = popmassnber.pop_id) AND (v_pop_letztermassnber.jahr = popmassnber.jahr) AND (v_pop_letztermassnber.id = popmassnber.pop_id))))\n  WHERE (popmassnber.beurteilung = 1)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztermassnber0 AS\n SELECT pop.ap_id,\n    tpop.id,\n    tpopmassnber.jahr\n   FROM apflora._variable,\n    (((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassnber ON ((tpop.id = tpopmassnber.tpop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n  WHERE ((tpopmassnber.jahr <= _variable.apber_jahr) AND (tpop.apber_relevant = 1) AND (tpopmassn.jahr <= _variable.apber_jahr) AND (pop.status <> 300) AND ((tpopmassnber.beurteilung >= 1) AND (tpopmassnber.beurteilung <= 5)));\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztermassnber AS\n SELECT v_tpop_letztermassnber0.ap_id,\n    v_tpop_letztermassnber0.id,\n    max(v_tpop_letztermassnber0.jahr) AS jahr\n   FROM apflora.v_tpop_letztermassnber0\n  GROUP BY v_tpop_letztermassnber0.ap_id, v_tpop_letztermassnber0.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c3rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN ((apflora.v_tpop_letztermassnber\n     JOIN apflora.tpopmassnber ON (((v_tpop_letztermassnber.id = tpopmassnber.tpop_id) AND (v_tpop_letztermassnber.jahr = tpopmassnber.jahr))))\n     JOIN apflora.tpop ON ((tpopmassnber.tpop_id = tpop.id))) ON ((pop.id = tpop.pop_id)))\n  WHERE (tpopmassnber.beurteilung = 1)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c4rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.v_pop_letztermassnber\n     JOIN apflora.pop ON ((v_pop_letztermassnber.ap_id = pop.ap_id)))\n     JOIN apflora.popmassnber ON (((pop.id = popmassnber.pop_id) AND (v_pop_letztermassnber.jahr = popmassnber.jahr) AND (v_pop_letztermassnber.id = popmassnber.pop_id))))\n  WHERE (popmassnber.beurteilung = 2)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c4rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN ((apflora.v_tpop_letztermassnber\n     JOIN apflora.tpopmassnber ON (((v_tpop_letztermassnber.id = tpopmassnber.tpop_id) AND (v_tpop_letztermassnber.jahr = tpopmassnber.jahr))))\n     JOIN apflora.tpop ON ((tpopmassnber.tpop_id = tpop.id))) ON ((pop.id = tpop.pop_id)))\n  WHERE (tpopmassnber.beurteilung = 2)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c5rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.v_pop_letztermassnber\n     JOIN apflora.pop ON ((v_pop_letztermassnber.ap_id = pop.ap_id)))\n     JOIN apflora.popmassnber ON (((pop.id = popmassnber.pop_id) AND (v_pop_letztermassnber.jahr = popmassnber.jahr) AND (v_pop_letztermassnber.id = popmassnber.pop_id))))\n  WHERE (popmassnber.beurteilung = 3)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c5rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN ((apflora.v_tpop_letztermassnber\n     JOIN apflora.tpopmassnber ON (((v_tpop_letztermassnber.id = tpopmassnber.tpop_id) AND (v_tpop_letztermassnber.jahr = tpopmassnber.jahr))))\n     JOIN apflora.tpop ON ((tpopmassnber.tpop_id = tpop.id))) ON ((pop.id = tpop.pop_id)))\n  WHERE (tpopmassnber.beurteilung = 3)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c6rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.v_pop_letztermassnber\n     JOIN apflora.pop ON ((v_pop_letztermassnber.ap_id = pop.ap_id)))\n     JOIN apflora.popmassnber ON (((pop.id = popmassnber.pop_id) AND (v_pop_letztermassnber.id = popmassnber.pop_id) AND (v_pop_letztermassnber.jahr = popmassnber.jahr))))\n  WHERE (popmassnber.beurteilung = 4)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c6rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN ((apflora.v_tpop_letztermassnber\n     JOIN apflora.tpopmassnber ON (((v_tpop_letztermassnber.id = tpopmassnber.tpop_id) AND (v_tpop_letztermassnber.jahr = tpopmassnber.jahr))))\n     JOIN apflora.tpop ON ((tpopmassnber.tpop_id = tpop.id))) ON ((pop.id = tpop.pop_id)))\n  WHERE (tpopmassnber.beurteilung = 4)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c7rpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM ((apflora.v_pop_letztermassnber\n     JOIN apflora.pop ON ((v_pop_letztermassnber.ap_id = pop.ap_id)))\n     JOIN apflora.popmassnber ON (((pop.id = popmassnber.pop_id) AND (v_pop_letztermassnber.id = popmassnber.pop_id) AND (v_pop_letztermassnber.jahr = popmassnber.jahr))))\n  WHERE (popmassnber.beurteilung = 5)\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_c7rtpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN ((apflora.v_tpop_letztermassnber\n     JOIN apflora.tpopmassnber ON (((v_tpop_letztermassnber.id = tpopmassnber.tpop_id) AND (v_tpop_letztermassnber.jahr = tpopmassnber.jahr))))\n     JOIN apflora.tpop ON ((tpopmassnber.tpop_id = tpop.id))) ON ((pop.id = tpop.pop_id)))\n  WHERE (tpopmassnber.beurteilung = 5)\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_erstemassnproap AS\n SELECT ap.id AS ap_id,\n    min(tpopmassn.jahr) AS jahr\n   FROM (((apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n  GROUP BY ap.id;\n\n\n\n\nCREATE VIEW apflora.v_apber_injahr AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    apber.id,\n    concat(adresse.name, ', ', adresse.adresse) AS bearbeiter,\n    apberuebersicht.jahr AS apberuebersicht_jahr,\n    apberuebersicht.bemerkungen,\n    v_erstemassnproap.jahr AS jahr_erste_massnahme\n   FROM ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     LEFT JOIN apflora.v_erstemassnproap ON ((ap.id = v_erstemassnproap.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (((apflora.apber\n     LEFT JOIN apflora.adresse ON ((apber.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.apberuebersicht ON ((apber.jahr = apberuebersicht.jahr)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id)))\n  WHERE (ap.bearbeitung < 4)\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_pop_uebersicht AS\n SELECT ap.id,\n    ae_eigenschaften.artname AS \"Art\",\n    ( SELECT count(*) AS count\n           FROM apflora.pop pop_1\n          WHERE ((pop_1.ap_id = ae_eigenschaften.id) AND (pop_1.status = 100) AND (pop_1.id IN ( SELECT DISTINCT tpop.pop_id\n                   FROM apflora.tpop\n                  WHERE (tpop.apber_relevant = 1))))) AS \"aktuellUrspruenglich\",\n    ( SELECT count(*) AS count\n           FROM apflora.pop pop_1\n          WHERE ((pop_1.ap_id = ae_eigenschaften.id) AND (pop_1.status = ANY (ARRAY[200, 210])) AND (pop_1.id IN ( SELECT DISTINCT tpop.pop_id\n                   FROM apflora.tpop\n                  WHERE (tpop.apber_relevant = 1))))) AS \"aktuellAngesiedelt\",\n    ( SELECT count(*) AS count\n           FROM apflora.pop pop_1\n          WHERE ((pop_1.ap_id = ae_eigenschaften.id) AND (pop_1.status = ANY (ARRAY[100, 200, 210])) AND (pop_1.id IN ( SELECT DISTINCT tpop.pop_id\n                   FROM apflora.tpop\n                  WHERE (tpop.apber_relevant = 1))))) AS aktuell\n   FROM (apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  GROUP BY ap.id, ae_eigenschaften.id, ae_eigenschaften.artname\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebe AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname,\n    v_ap_anzmassnbisjahr.anzahl_massnahmen\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora._variable\n     JOIN (apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id))) ON ((_variable.apber_jahr = apber.jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND (apber.beurteilung = 1) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebe_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora._variable\n     JOIN (apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id))) ON ((_variable.apber_jahr = apber.jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND (apber.beurteilung = 1) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebkm AS\n SELECT ae_eigenschaften.artname,\n        CASE\n            WHEN (ae_eigenschaften.kefart = true) THEN 'Ja'::text\n            ELSE ''::text\n        END AS \"FnsKefArt2\",\n        CASE\n            WHEN (round((((_variable.apber_jahr - ae_eigenschaften.kefkontrolljahr) / 4))::numeric, 0) = (((_variable.apber_jahr - ae_eigenschaften.kefkontrolljahr) / 4))::numeric) THEN 'Ja'::text\n            ELSE ''::text\n        END AS \"FnsKefKontrJahr2\"\n   FROM ((apflora.ae_eigenschaften\n     JOIN ((apflora.v_ap_anzmassnbisjahr \"vApAnzMassnBisJahr_1\"\n     JOIN apflora.ap ON ((\"vApAnzMassnBisJahr_1\".id = ap.id)))\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.apber\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON (((_variable.apber_jahr = \"vApAnzMassnBisJahr_1\".jahr) AND (ap.id = apber.ap_id))))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3) AND (\"vApAnzMassnBisJahr_1\".anzahl_massnahmen = '0'::numeric))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebma AS\n SELECT ae_eigenschaften.artname,\n    v_ap_anzmassnbisjahr.anzahl_massnahmen\n   FROM (apflora._variable\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((ap.id = v_ap_anzmassnbisjahr.id))) ON ((_variable.apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebma_apid AS\n SELECT ap.id\n   FROM (apflora._variable\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((ap.id = v_ap_anzmassnbisjahr.id))) ON ((_variable.apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebme AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 5) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebme_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 5) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebne_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 3) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebse_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 4) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebun_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 1168274204) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebwe_apid AS\n SELECT ap.id\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 6) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebnb AS\n SELECT ap.id,\n    ae_eigenschaften.artname\n   FROM (apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3) AND (NOT (ap.id IN ( SELECT v_apber_uebse_apid.id\n           FROM apflora.v_apber_uebse_apid))) AND (NOT (ap.id IN ( SELECT v_apber_uebe_apid.id\n           FROM apflora.v_apber_uebe_apid))) AND (NOT (ap.id IN ( SELECT v_apber_uebme_apid.id\n           FROM apflora.v_apber_uebme_apid))) AND (NOT (ap.id IN ( SELECT v_apber_uebwe_apid.id\n           FROM apflora.v_apber_uebwe_apid))) AND (NOT (ap.id IN ( SELECT v_apber_uebne_apid.id\n           FROM apflora.v_apber_uebne_apid))) AND (NOT (ap.id IN ( SELECT v_apber_uebun_apid.id\n           FROM apflora.v_apber_uebun_apid))) AND (ap.id IN ( SELECT v_apber_uebma_apid.id\n           FROM apflora.v_apber_uebma_apid)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebnb00 AS\n SELECT ap.id,\n    apber.jahr\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN (((apflora.ap\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((ap.id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id)))\n     JOIN (apflora.apber\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3) AND (apber.beurteilung IS NULL));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebnb000 AS\n SELECT ap.id,\n    apber.jahr\n   FROM ((((apflora.ap\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((ap.id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id)))\n     LEFT JOIN apflora.apber ON ((ap.id = apber.ap_id)))\n     JOIN apflora._variable ON ((v_ap_anzmassnbisjahr.jahr = _variable.apber_jahr)))\n  WHERE ((apber.ap_id IS NULL) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)));\n\n\n\n\nCREATE VIEW apflora.v_apber_uebnb0 AS\n SELECT v_apber_uebnb000.id,\n    v_apber_uebnb000.jahr\n   FROM apflora.v_apber_uebnb000\nUNION\n SELECT v_apber_uebnb00.id,\n    v_apber_uebnb00.jahr\n   FROM apflora.v_apber_uebnb00;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebne AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 3) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebse AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 4) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebun AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 1168274204) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uebwe AS\n SELECT apber.id_old,\n    apber.jahr,\n    apber.situation,\n    apber.vergleich_vorjahr_gesamtziel,\n    apber.beurteilung,\n    apber.veraenderung_zum_vorjahr,\n    apber.apber_analyse,\n    apber.konsequenzen_umsetzung,\n    apber.konsequenzen_erfolgskontrolle,\n    apber.biotope_neue,\n    apber.biotope_optimieren,\n    apber.massnahmen_optimieren,\n    apber.wirkung_auf_art,\n    apber.datum,\n    apber.changed,\n    apber.changed_by,\n    apber.massnahmen_ap_bearb,\n    apber.massnahmen_planung_vs_ausfuehrung,\n    apber.id,\n    apber.ap_id,\n    apber.bearbeiter,\n    ae_eigenschaften.artname\n   FROM (apflora._variable \"tblKonstanten_1\"\n     JOIN ((apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.apber\n     JOIN apflora.v_ap_anzmassnbisjahr ON ((apber.ap_id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora._variable ON ((apber.jahr = _variable.apber_jahr))) ON ((ap.id = apber.ap_id))) ON ((\"tblKonstanten_1\".apber_jahr = v_ap_anzmassnbisjahr.jahr)))\n  WHERE ((apber.beurteilung = 6) AND (v_ap_anzmassnbisjahr.anzahl_massnahmen > (0)::numeric) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uet01 AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n        CASE\n            WHEN (NOT (ap.id IN ( SELECT v_apber_uebma_apid.id\n               FROM apflora.v_apber_uebma_apid))) THEN 1\n            ELSE 0\n        END AS \"keineMassnahmen\",\n        CASE\n            WHEN (ae_eigenschaften.kefart = true) THEN 1\n            ELSE 0\n        END AS \"FnsKefArt\",\n        CASE\n            WHEN (round((((_variable.apber_jahr - ae_eigenschaften.kefkontrolljahr) / 4))::numeric, 0) = (((_variable.apber_jahr - ae_eigenschaften.kefkontrolljahr) / 4))::numeric) THEN 1\n            ELSE 0\n        END AS \"FnsKefKontrJahr\"\n   FROM (apflora.ae_eigenschaften\n     JOIN ((apflora.ap\n     JOIN (apflora.v_ap_anzmassnbisjahr\n     JOIN apflora._variable ON ((v_ap_anzmassnbisjahr.jahr = _variable.apber_jahr))) ON ((ap.id = v_ap_anzmassnbisjahr.id)))\n     JOIN apflora.v_ap_apberrelevant ON ((ap.id = v_ap_apberrelevant.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_apber_uet_veraengegenvorjahr AS\n SELECT ap.id,\n    apber.veraenderung_zum_vorjahr,\n    apber.jahr\n   FROM (apflora.ap\n     LEFT JOIN apflora.apber ON ((ap.id = apber.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3) AND ((apber.jahr IN ( SELECT _variable.apber_jahr\n           FROM apflora._variable)) OR (apber.jahr IS NULL)));\n\n\n\n\nCREATE TABLE apflora.zielber (\n    id_old integer,\n    jahr smallint,\n    erreichung text,\n    bemerkungen text,\n    changed date DEFAULT now(),\n    changed_by character varying(20) DEFAULT current_setting('request.jwt.claim.username'::text, true),\n    id uuid DEFAULT public.uuid_generate_v1mc() NOT NULL,\n    ziel_id uuid\n);\n\n\n\n\nCOMMENT ON COLUMN apflora.zielber.id_old IS 'frühere id';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.jahr IS 'Für welches Jahr gilt der Bericht?';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.erreichung IS 'Beurteilung der Zielerreichung';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.bemerkungen IS 'Bemerkungen zur Zielerreichung';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.changed IS 'Wann wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.changed_by IS 'Von wem wurde der Datensatz zuletzt geändert?';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.id IS 'Primärschlüssel';\n\n\n\nCOMMENT ON COLUMN apflora.zielber.ziel_id IS 'Zugehöriges Ziel. Fremdschlüssel aus der Tabelle \"ziel\"';\n\n\n\nCREATE VIEW apflora.v_apber_zielber AS\n SELECT zielber.id_old,\n    zielber.jahr,\n    zielber.erreichung,\n    zielber.bemerkungen,\n    zielber.changed,\n    zielber.changed_by,\n    zielber.id,\n    zielber.ziel_id\n   FROM (apflora.zielber\n     JOIN apflora._variable ON ((zielber.jahr = _variable.apber_jahr)));\n\n\n\n\nCREATE VIEW apflora.v_apbera1lpop AS\n SELECT pop.ap_id,\n    pop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id;\n\n\n\n\nCREATE VIEW apflora.v_apbera1ltpop AS\n SELECT pop.ap_id,\n    tpop.id\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status IS NOT NULL) AND (pop.status <> 300) AND (tpop.status <> 300) AND (tpop.status IS NOT NULL))\n  GROUP BY pop.ap_id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_assozart AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    assozart.id,\n    \"ArtenDb_Arteigenschaften_1\".artname AS artname_assoziiert,\n    assozart.bemerkungen,\n    assozart.changed,\n    assozart.changed_by\n   FROM (apflora.ae_eigenschaften \"ArtenDb_Arteigenschaften_1\"\n     RIGHT JOIN (((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     RIGHT JOIN apflora.assozart ON ((ap.id = assozart.ap_id))) ON ((\"ArtenDb_Arteigenschaften_1\".id = assozart.ae_id)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_auswapbearbmassninjahr0 AS\n SELECT adresse.name AS bearbeiter,\n    ae_eigenschaften.artname,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpopmassn.jahr,\n    tpopmassn_typ_werte.text AS typ,\n    tpopmassn.beschreibung,\n    tpopmassn.datum,\n    tpopmassn.bemerkungen,\n    tpopmassn.plan_vorhanden,\n    tpopmassn.plan_bezeichnung,\n    tpopmassn.flaeche,\n    tpopmassn.markierung,\n    tpopmassn.anz_triebe,\n    tpopmassn.anz_pflanzen,\n    tpopmassn.anz_pflanzstellen,\n    tpopmassn.wirtspflanze,\n    tpopmassn.herkunft_pop,\n    tpopmassn.sammeldatum,\n    tpopmassn.form,\n    tpopmassn.pflanzanordnung\n   FROM ((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN ((apflora.tpopmassn\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id)))\n     JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))\n  ORDER BY adresse.name, ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_beob AS\n SELECT beob.id,\n    beob_quelle_werte.name AS quelle,\n    beob.id_field,\n    (beob.data ->> (( SELECT beob_1.id_field\n           FROM apflora.beob beob_1\n          WHERE (beob_1.id = beob2.id)))::text) AS \"OriginalId\",\n    beob.art_id,\n    ae_eigenschaften.artname AS \"Artname\",\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    beob.x,\n    beob.y,\n        CASE\n            WHEN ((beob.x > 0) AND (tpop.x > 0) AND (beob.y > 0) AND (tpop.y > 0)) THEN round(sqrt((power(((beob.x - tpop.x))::double precision, (2)::double precision) + power(((beob.y - tpop.y))::double precision, (2)::double precision))))\n            ELSE NULL::double precision\n        END AS distanz_zur_teilpopulation,\n    beob.datum,\n    beob.autor,\n    beob.nicht_zuordnen,\n    beob.bemerkungen,\n    beob.changed,\n    beob.changed_by\n   FROM (((((apflora.beob\n     JOIN apflora.beob beob2 ON ((beob2.id = beob.id)))\n     JOIN (apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ap.art_id = ae_eigenschaften.id))) ON ((beob.art_id = ae_eigenschaften.id)))\n     JOIN apflora.beob_quelle_werte ON ((beob.quelle_id = beob_quelle_werte.id)))\n     LEFT JOIN apflora.tpop ON ((tpop.id = beob.tpop_id)))\n     LEFT JOIN apflora.pop ON ((pop.id = tpop.pop_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, beob.datum DESC;\n\n\n\n\nCREATE VIEW apflora.v_beob__mit_data AS\n SELECT beob.id,\n    beob_quelle_werte.name AS quelle,\n    beob.id_field,\n    (beob.data ->> (( SELECT beob_1.id_field\n           FROM apflora.beob beob_1\n          WHERE (beob_1.id = beob2.id)))::text) AS \"OriginalId\",\n    beob.art_id,\n    ae_eigenschaften.artname AS \"Artname\",\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    beob.x,\n    beob.y,\n        CASE\n            WHEN ((beob.x > 0) AND (tpop.x > 0) AND (beob.y > 0) AND (tpop.y > 0)) THEN round(sqrt((power(((beob.x - tpop.x))::double precision, (2)::double precision) + power(((beob.y - tpop.y))::double precision, (2)::double precision))))\n            ELSE NULL::double precision\n        END AS \"Distanz zur Teilpopulation (m)\",\n    beob.datum,\n    beob.autor,\n    beob.nicht_zuordnen,\n    beob.bemerkungen,\n    beob.changed,\n    beob.changed_by,\n    beob.data AS \"Originaldaten\"\n   FROM (((((apflora.beob\n     JOIN apflora.beob beob2 ON ((beob2.id = beob.id)))\n     JOIN (apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ap.art_id = ae_eigenschaften.id))) ON ((beob.art_id = ae_eigenschaften.id)))\n     JOIN apflora.beob_quelle_werte ON ((beob.quelle_id = beob_quelle_werte.id)))\n     LEFT JOIN apflora.tpop ON ((tpop.id = beob.tpop_id)))\n     LEFT JOIN apflora.pop ON ((pop.id = tpop.pop_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, beob.datum DESC;\n\n\n\n\nCREATE VIEW apflora.v_ber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    ber.id,\n    ber.autor,\n    ber.jahr,\n    ber.titel,\n    ber.url,\n    ber.changed,\n    ber.changed_by\n   FROM (((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     RIGHT JOIN apflora.ber ON ((ap.id = ber.ap_id)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_bertpopfuerangezeigteap0 AS\n SELECT ap.id AS ap_id,\n    pop.id AS pop_id,\n    tpop.id AS tpop_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    pop_status_werte_2.text AS tpop_status,\n    tpop.apber_relevant\n   FROM (((((apflora.ae_eigenschaften\n     JOIN ((apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)));\n\n\n\n\nCREATE VIEW apflora.v_datenstruktur AS\n SELECT tables.table_schema AS \"Tabelle: Schema\",\n    tables.table_name AS \"Tabelle: Name\",\n    public.dsql2((((('select count(*) from \"'::text || (tables.table_schema)::text) || '\".\"'::text) || (tables.table_name)::text) || '\"'::text)) AS \"Tabelle: Anzahl Datensaetze\",\n    columns.column_name AS \"Feld: Name\",\n    columns.column_default AS \"Feld: Standardwert\",\n    columns.data_type AS \"Feld: Datentyp\",\n    columns.is_nullable AS \"Feld: Nullwerte\"\n   FROM (information_schema.tables\n     JOIN information_schema.columns ON ((((tables.table_name)::text = (columns.table_name)::text) AND ((tables.table_schema)::text = (columns.table_schema)::text))))\n  WHERE ((tables.table_schema)::text = 'apflora'::text)\n  ORDER BY tables.table_schema, tables.table_name, columns.column_name;\n\n\n\n\nCREATE VIEW apflora.v_erfkrit AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    erfkrit.id,\n    ap_erfkrit_werte.text AS beurteilung,\n    erfkrit.kriterien,\n    erfkrit.changed,\n    erfkrit.changed_by\n   FROM ((((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     RIGHT JOIN apflora.erfkrit ON ((ap.id = erfkrit.ap_id)))\n     LEFT JOIN apflora.ap_erfkrit_werte ON ((erfkrit.erfolg = ap_erfkrit_werte.code)))\n  ORDER BY ae_eigenschaften.artname;\n\n\n\n\nCREATE VIEW apflora.v_exportevab_beob AS\nSELECT\n    NULL::uuid AS \"fkZeitpunkt\",\n    NULL::uuid AS \"idBeobachtung\",\n    NULL::uuid AS fkautor,\n    NULL::uuid AS fkart,\n    NULL::integer AS fkartgruppe,\n    NULL::integer AS fkaa1,\n    NULL::integer AS \"fkAAINTRODUIT\",\n    NULL::integer AS \"fkAAPRESENCE\",\n    NULL::text AS \"MENACES\",\n    NULL::text AS \"VITALITE_PLANTE\",\n    NULL::text AS \"STATION\",\n    NULL::text AS \"ABONDANCE\",\n    NULL::text AS \"EXPERTISE_INTRODUIT\",\n    NULL::text AS \"EXPERTISE_INTRODUITE_NOM\";\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_maxanzahl AS\n SELECT tpopkontr.id,\n    max(tpopkontrzaehl.anzahl) AS anzahl\n   FROM (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n  GROUP BY tpopkontr.id\n  ORDER BY tpopkontr.id;\n\n\n\n\nCREATE VIEW apflora.v_exportevab_projekt AS\n SELECT ap.id AS \"idProjekt\",\n    concat('AP Flora ZH: ', ae_eigenschaften.artname) AS \"Name\",\n        CASE\n            WHEN (ap.start_jahr IS NOT NULL) THEN concat('01.01.', ap.start_jahr)\n            ELSE to_char((('now'::text)::date)::timestamp with time zone, 'DD.MM.YYYY'::text)\n        END AS \"Eroeffnung\",\n    '7c71b8af-df3e-4844-a83b-55735f80b993'::uuid AS \"fkAutor\",\n    concat('Aktionsplan: ', ap_bearbstand_werte.text,\n        CASE\n            WHEN (ap.start_jahr IS NOT NULL) THEN concat('; Start im Jahr: ', ap.start_jahr)\n            ELSE ''::text\n        END,\n        CASE\n            WHEN (ap.umsetzung IS NOT NULL) THEN concat('; Stand Umsetzung: ', ap_umsetzung_werte.text)\n            ELSE ''::text\n        END, '') AS \"Bemerkungen\"\n   FROM ((((apflora.ap\n     JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN apflora.ae_eigenschaften ON ((ap.art_id = ae_eigenschaften.id)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN (apflora.tpop\n     JOIN ((apflora.tpopkontr\n     JOIN apflora.v_tpopkontr_maxanzahl ON ((v_tpopkontr_maxanzahl.id = tpopkontr.id)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((ae_eigenschaften.taxid > 150) AND (ae_eigenschaften.taxid < 1000000) AND (tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL) AND ((tpopkontr.typ)::text = ANY (ARRAY[('Ausgangszustand'::character varying)::text, ('Zwischenbeurteilung'::character varying)::text, ('Freiwilligen-Erfolgskontrolle'::character varying)::text])) AND (tpop.status <> 201) AND (tpopkontr.bearbeiter IS NOT NULL) AND (tpopkontr.bearbeiter <> 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.jahr)::double precision <> date_part('year'::text, ('now'::text)::date)) AND (tpop.bekannt_seit IS NOT NULL) AND ((tpop.status = ANY (ARRAY[100, 101])) OR ((tpopkontr.jahr - tpop.bekannt_seit) > 5)) AND (tpop.flurname IS NOT NULL))\n  GROUP BY ae_eigenschaften.artname, ap.id, ap.start_jahr, ap.umsetzung, ap_bearbstand_werte.text, ap_umsetzung_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_exportevab_raum AS\n SELECT ap.id AS \"fkProjekt\",\n    pop.id AS \"idRaum\",\n    concat(pop.name,\n        CASE\n            WHEN (pop.nr IS NOT NULL) THEN concat(' (Nr. ', pop.nr, ')')\n            ELSE ''::text\n        END) AS \"Name\",\n    to_char((('now'::text)::date)::timestamp with time zone, 'DD.MM.YYYY'::text) AS \"Erfassungsdatum\",\n    '7c71b8af-df3e-4844-a83b-55735f80b993'::uuid AS \"fkAutor\",\n        CASE\n            WHEN (pop.status IS NOT NULL) THEN concat('Status: ', \"popHerkunft\".text,\n            CASE\n                WHEN (pop.bekannt_seit IS NOT NULL) THEN concat('; Bekannt seit: ', pop.bekannt_seit)\n                ELSE ''::text\n            END)\n            ELSE ''::text\n        END AS \"Bemerkungen\"\n   FROM ((apflora.ap\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte \"popHerkunft\" ON ((pop.status = \"popHerkunft\".code)))\n     JOIN (apflora.tpop\n     JOIN ((apflora.tpopkontr\n     JOIN apflora.v_tpopkontr_maxanzahl ON ((v_tpopkontr_maxanzahl.id = tpopkontr.id)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     JOIN apflora.ae_eigenschaften ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ae_eigenschaften.taxid > 150) AND (ae_eigenschaften.taxid < 1000000) AND (tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL) AND ((tpopkontr.typ)::text = ANY (ARRAY[('Ausgangszustand'::character varying)::text, ('Zwischenbeurteilung'::character varying)::text, ('Freiwilligen-Erfolgskontrolle'::character varying)::text])) AND (tpop.status <> 201) AND (tpopkontr.bearbeiter IS NOT NULL) AND (tpopkontr.bearbeiter <> 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.jahr)::double precision <> date_part('year'::text, ('now'::text)::date)) AND (tpop.bekannt_seit IS NOT NULL) AND ((tpop.status = ANY (ARRAY[100, 101])) OR ((tpopkontr.jahr - tpop.bekannt_seit) > 5)) AND (tpop.flurname IS NOT NULL) AND (ap.id IN ( SELECT v_exportevab_projekt.\"idProjekt\"\n           FROM apflora.v_exportevab_projekt)))\n  GROUP BY ap.id, pop.id, pop.name, pop.nr, pop.status, \"popHerkunft\".text, pop.bekannt_seit;\n\n\n\n\nCREATE VIEW apflora.v_exportevab_ort AS\n SELECT tpop.id AS \"TPopGuid\",\n    pop.id AS \"fkRaum\",\n    tpop.id AS \"idOrt\",\n    \"substring\"(concat(tpop.flurname,\n        CASE\n            WHEN (tpop.nr IS NOT NULL) THEN concat(' (Nr. ', tpop.nr, ')')\n            ELSE ''::text\n        END), 1, 40) AS \"Name\",\n    to_char((('now'::text)::date)::timestamp with time zone, 'DD.MM.YYYY'::text) AS \"Erfassungsdatum\",\n    '7c71b8af-df3e-4844-a83b-55735f80b993'::uuid AS \"fkAutor\",\n    (\"substring\"(max((evab_typologie.\"TYPO\")::text), 1, 9))::character varying(10) AS \"fkLebensraumtyp\",\n    1 AS \"fkGenauigkeitLage\",\n    1 AS \"fkGeometryType\",\n        CASE\n            WHEN (tpop.hoehe IS NOT NULL) THEN (tpop.hoehe)::integer\n            ELSE 0\n        END AS \"obergrenzeHoehe\",\n    4 AS \"fkGenauigkeitHoehe\",\n    tpop.x AS \"X\",\n    tpop.y AS \"Y\",\n    \"substring\"(tpop.gemeinde, 1, 25) AS \"NOM_COMMUNE\",\n    \"substring\"(tpop.flurname, 1, 255) AS \"DESC_LOCALITE\",\n    max(tpopkontr.lr_umgebung_delarze) AS \"ENV\",\n        CASE\n            WHEN (tpop.status IS NOT NULL) THEN concat('Status: ', pop_status_werte.text,\n            CASE\n                WHEN (tpop.bekannt_seit IS NOT NULL) THEN concat('; Bekannt seit: ', tpop.bekannt_seit)\n                ELSE ''::text\n            END)\n            ELSE ''::text\n        END AS \"Bemerkungen\"\n   FROM ((apflora.ap\n     JOIN (apflora.pop\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte ON ((tpop.status = pop_status_werte.code)))\n     JOIN (((apflora.tpopkontr\n     JOIN apflora.v_tpopkontr_maxanzahl ON ((v_tpopkontr_maxanzahl.id = tpopkontr.id)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.evab_typologie ON ((tpopkontr.lr_delarze = (evab_typologie.\"TYPO\")::text))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     JOIN apflora.ae_eigenschaften ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ae_eigenschaften.taxid > 150) AND (ae_eigenschaften.taxid < 1000000) AND (tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL) AND ((tpopkontr.typ)::text = ANY (ARRAY[('Ausgangszustand'::character varying)::text, ('Zwischenbeurteilung'::character varying)::text, ('Freiwilligen-Erfolgskontrolle'::character varying)::text])) AND (tpop.status <> 201) AND (tpopkontr.bearbeiter IS NOT NULL) AND (tpopkontr.bearbeiter <> 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.jahr)::double precision <> date_part('year'::text, ('now'::text)::date)) AND (tpop.bekannt_seit IS NOT NULL) AND ((tpop.status = ANY (ARRAY[100, 101])) OR ((tpopkontr.jahr - tpop.bekannt_seit) > 5)) AND (tpop.flurname IS NOT NULL) AND (ap.id IN ( SELECT v_exportevab_projekt.\"idProjekt\"\n           FROM apflora.v_exportevab_projekt)) AND (pop.id IN ( SELECT v_exportevab_raum.\"idRaum\"\n           FROM apflora.v_exportevab_raum)))\n  GROUP BY pop.id, tpop.id, tpop.nr, tpop.bekannt_seit, tpop.flurname, tpop.status, pop_status_werte.text, tpop.hoehe, tpop.x, tpop.y, tpop.gemeinde;\n\n\n\n\nCREATE VIEW apflora.v_exportevab_zeit AS\n SELECT tpop.id AS \"fkOrt\",\n    tpopkontr.zeit_id AS \"idZeitpunkt\",\n        CASE\n            WHEN (tpopkontr.datum IS NOT NULL) THEN to_char((tpopkontr.datum)::timestamp with time zone, 'DD.MM.YYYY'::text)\n            ELSE concat('01.01.', tpopkontr.jahr)\n        END AS \"Datum\",\n        CASE\n            WHEN (tpopkontr.datum IS NOT NULL) THEN 'T'::character varying(10)\n            ELSE 'J'::character varying(10)\n        END AS \"fkGenauigkeitDatum\",\n        CASE\n            WHEN (tpopkontr.datum IS NOT NULL) THEN 'P'::character varying(10)\n            ELSE 'X'::character varying(10)\n        END AS \"fkGenauigkeitDatumZDSF\",\n    \"substring\"((tpopkontr.moosschicht)::text, 1, 10) AS \"COUV_MOUSSES\",\n    \"substring\"((tpopkontr.krautschicht)::text, 1, 10) AS \"COUV_HERBACEES\",\n    \"substring\"(tpopkontr.strauchschicht, 1, 10) AS \"COUV_BUISSONS\",\n    \"substring\"((tpopkontr.baumschicht)::text, 1, 10) AS \"COUV_ARBRES\"\n   FROM ((apflora.ap\n     JOIN (apflora.pop\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte tpop_status_werte ON ((tpop.status = tpop_status_werte.code)))\n     JOIN ((apflora.tpopkontr\n     JOIN apflora.v_tpopkontr_maxanzahl ON ((v_tpopkontr_maxanzahl.id = tpopkontr.id)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     JOIN apflora.ae_eigenschaften ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ae_eigenschaften.taxid > 150) AND (ae_eigenschaften.taxid < 1000000) AND (tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL) AND ((tpopkontr.typ)::text = ANY (ARRAY[('Ausgangszustand'::character varying)::text, ('Zwischenbeurteilung'::character varying)::text, ('Freiwilligen-Erfolgskontrolle'::character varying)::text])) AND (tpop.status <> 201) AND (tpopkontr.bearbeiter IS NOT NULL) AND (tpopkontr.bearbeiter <> 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.jahr)::double precision <> date_part('year'::text, ('now'::text)::date)) AND (tpop.bekannt_seit IS NOT NULL) AND ((tpop.status = ANY (ARRAY[100, 101])) OR ((tpopkontr.jahr - tpop.bekannt_seit) > 5)) AND (tpop.flurname IS NOT NULL) AND (ap.id IN ( SELECT v_exportevab_projekt.\"idProjekt\"\n           FROM apflora.v_exportevab_projekt)) AND (pop.id IN ( SELECT v_exportevab_raum.\"idRaum\"\n           FROM apflora.v_exportevab_raum)) AND (tpop.id IN ( SELECT v_exportevab_ort.\"idOrt\"\n           FROM apflora.v_exportevab_ort)));\n\n\n\n\nCREATE VIEW apflora.v_idealbiotop AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    ap.changed AS ap_changed,\n    ap.changed_by AS ap_changed_by,\n    idealbiotop.erstelldatum,\n    idealbiotop.hoehenlage,\n    idealbiotop.region,\n    idealbiotop.exposition,\n    idealbiotop.besonnung,\n    idealbiotop.hangneigung,\n    idealbiotop.boden_typ,\n    idealbiotop.boden_kalkgehalt,\n    idealbiotop.boden_durchlaessigkeit,\n    idealbiotop.boden_humus,\n    idealbiotop.boden_naehrstoffgehalt,\n    idealbiotop.wasserhaushalt,\n    idealbiotop.konkurrenz,\n    idealbiotop.moosschicht,\n    idealbiotop.krautschicht,\n    idealbiotop.strauchschicht,\n    idealbiotop.baumschicht,\n    idealbiotop.bemerkungen,\n    idealbiotop.changed,\n    idealbiotop.changed_by\n   FROM (apflora.idealbiotop\n     LEFT JOIN ((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id))) ON ((idealbiotop.ap_id = ap.id)))\n  ORDER BY ae_eigenschaften.artname, idealbiotop.erstelldatum;\n\n\n\n\nCREATE VIEW apflora.v_kontrzaehl_anzproeinheit AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    apflora_adresse_1.name AS ap_bearbeiter,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpop_status_werte.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius_m,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopkontr.id AS kontr_id,\n    tpopkontr.jahr AS kontr_jahr,\n    tpopkontr.datum AS kontr_datum,\n    tpopkontr_typ_werte.text AS kontr_typ,\n    adresse.name AS kontr_bearbeiter,\n    tpopkontr.ueberlebensrate AS kontr_ueberlebensrate,\n    tpopkontr.vitalitaet AS kontr_vitalitaet,\n    tpop_entwicklung_werte.text AS kontr_entwicklung,\n    tpopkontr.ursachen AS kontr_ursachen,\n    tpopkontr.erfolgsbeurteilung AS kontr_erfolgsbeurteilung,\n    tpopkontr.umsetzung_aendern AS kontr_umsetzung_aendern,\n    tpopkontr.kontrolle_aendern AS kontr_kontrolle_aendern,\n    tpopkontr.bemerkungen AS kontr_bemerkungen,\n    tpopkontr.lr_delarze AS kontr_lr_delarze,\n    tpopkontr.lr_umgebung_delarze AS kontr_lr_umgebung_delarze,\n    tpopkontr.vegetationstyp AS kontr_vegetationstyp,\n    tpopkontr.konkurrenz AS kontr_konkurrenz,\n    tpopkontr.moosschicht AS kontr_moosschicht,\n    tpopkontr.krautschicht AS kontr_krautschicht,\n    tpopkontr.strauchschicht AS kontr_strauchschicht,\n    tpopkontr.baumschicht AS kontr_baumschicht,\n    tpopkontr.boden_typ AS kontr_boden_typ,\n    tpopkontr.boden_kalkgehalt AS kontr_boden_kalkgehalt,\n    tpopkontr.boden_durchlaessigkeit AS kontr_boden_durchlaessigkeit,\n    tpopkontr.boden_humus AS kontr_boden_humus,\n    tpopkontr.boden_naehrstoffgehalt AS kontr_boden_naehrstoffgehalt,\n    tpopkontr.boden_abtrag AS kontr_boden_abtrag,\n    tpopkontr.wasserhaushalt AS kontr_wasserhaushalt,\n    tpopkontr_idbiotuebereinst_werte.text AS kontr_idealbiotop_uebereinstimmung,\n    tpopkontr.handlungsbedarf AS kontr_handlungsbedarf,\n    tpopkontr.flaeche_ueberprueft AS kontr_flaeche_ueberprueft,\n    tpopkontr.flaeche AS kontr_flaeche,\n    tpopkontr.plan_vorhanden AS kontr_plan_vorhanden,\n    tpopkontr.deckung_vegetation AS kontr_deckung_vegetation,\n    tpopkontr.deckung_nackter_boden AS kontr_deckung_nackter_boden,\n    tpopkontr.deckung_ap_art AS kontr_deckung_ap_art,\n    tpopkontr.jungpflanzen_vorhanden AS kontr_jungpflanzen_vorhanden,\n    tpopkontr.vegetationshoehe_maximum AS kontr_vegetationshoehe_maximum,\n    tpopkontr.vegetationshoehe_mittel AS kontr_vegetationshoehe_mittel,\n    tpopkontr.gefaehrdung AS kontr_gefaehrdung,\n    tpopkontr.changed AS kontr_changed,\n    tpopkontr.changed_by AS kontr_changed_by,\n    tpopkontrzaehl.id,\n    tpopkontrzaehl_einheit_werte.text AS einheit,\n    tpopkontrzaehl_methode_werte.text AS methode,\n    tpopkontrzaehl.anzahl\n   FROM (apflora.ae_eigenschaften\n     JOIN ((((apflora.ap\n     LEFT JOIN apflora.adresse apflora_adresse_1 ON ((ap.bearbeiter = apflora_adresse_1.id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte tpop_status_werte ON ((tpop_status_werte.code = tpop.status)))\n     JOIN (((((apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontr_idbiotuebereinst_werte ON ((tpopkontr.idealbiotop_uebereinstimmung = tpopkontr_idbiotuebereinst_werte.code)))\n     LEFT JOIN apflora.tpopkontr_typ_werte ON (((tpopkontr.typ)::text = (tpopkontr_typ_werte.text)::text)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopkontr.entwicklung = tpop_entwicklung_werte.code)))\n     LEFT JOIN ((apflora.tpopkontrzaehl\n     LEFT JOIN apflora.tpopkontrzaehl_einheit_werte ON ((tpopkontrzaehl.einheit = tpopkontrzaehl_einheit_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl_methode_werte ON ((tpopkontrzaehl.methode = tpopkontrzaehl_methode_werte.code))) ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopkontr.jahr, tpopkontr.datum;\n\n\n\n\nCREATE VIEW apflora.v_massn AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.familie,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    pop_status_werte_2.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopmassn.id,\n    tpopmassn.jahr,\n    tpopmassn.datum,\n    tpopmassn_typ_werte.text AS typ,\n    tpopmassn.beschreibung,\n    adresse.name AS bearbeiter,\n    tpopmassn.bemerkungen,\n    tpopmassn.plan_vorhanden,\n    tpopmassn.plan_bezeichnung,\n    tpopmassn.flaeche,\n    tpopmassn.form,\n    tpopmassn.pflanzanordnung,\n    tpopmassn.markierung,\n    tpopmassn.anz_triebe,\n    tpopmassn.anz_pflanzen,\n    tpopmassn.anz_pflanzstellen,\n    tpopmassn.wirtspflanze,\n    tpopmassn.herkunft_pop,\n    tpopmassn.sammeldatum,\n    tpopmassn.changed,\n    tpopmassn.changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN (apflora.tpopmassn\n     LEFT JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassn.jahr, tpopmassn.datum, tpopmassn_typ_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_massn_fuergis_read AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    (pop.id)::character varying(50) AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    (tpop.id)::character varying(50) AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    pop_status_werte_2.text AS tpop_status,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    adresse.name AS tpop_bearbeiter,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    (tpopmassn.id)::character varying(50) AS massn_id,\n    tpopmassn.jahr AS massn_jahr,\n    (tpopmassn.datum)::timestamp without time zone AS massn_datum,\n    tpopmassn_typ_werte.text AS massn_typ,\n    tpopmassn.beschreibung AS massn_beschreibung,\n    adresse.name AS massn_bearbeiter,\n    tpopmassn.plan_vorhanden AS massn_plan_vorhanden,\n    tpopmassn.plan_bezeichnung AS massn_plan_bezeichnung,\n    tpopmassn.flaeche AS massn_flaeche,\n    tpopmassn.form AS massn_form,\n    tpopmassn.pflanzanordnung AS massn_pflanzanordnung,\n    tpopmassn.markierung AS massn_markierung,\n    tpopmassn.anz_triebe AS massn_anz_triebe,\n    tpopmassn.anz_pflanzen AS massn_anz_pflanzen,\n    tpopmassn.anz_pflanzstellen AS massn_anz_pflanzstellen,\n    tpopmassn.wirtspflanze AS massn_wirtspflanze,\n    tpopmassn.herkunft_pop AS massn_herkunft_pop,\n    tpopmassn.sammeldatum AS massn_sammeldatum,\n    (tpopmassn.changed)::timestamp without time zone AS massn_changed,\n    tpopmassn.changed_by AS massn_changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN (apflora.tpopmassn\n     LEFT JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassn.jahr, tpopmassn.datum, tpopmassn_typ_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_massn_fuergis_write AS\n SELECT tpopmassn.tpop_id,\n    (tpopmassn.id)::character varying(50) AS massn_id,\n    tpopmassn.typ AS massn_typ,\n    tpopmassn.jahr AS massn_jahr,\n    (tpopmassn.datum)::timestamp without time zone AS massn_datum,\n    tpopmassn.bearbeiter AS massn_bearbeiter,\n    tpopmassn.beschreibung AS massn_beschreibung,\n    tpopmassn.plan_vorhanden AS massn_plan_vorhanden,\n    tpopmassn.plan_bezeichnung AS massn_plan_bezeichnung,\n    tpopmassn.flaeche AS massn_flaeche,\n    tpopmassn.form AS massn_form,\n    tpopmassn.pflanzanordnung AS massn_pflanzanordnung,\n    tpopmassn.markierung AS massn_markierung,\n    tpopmassn.anz_triebe AS massn_anz_triebe,\n    tpopmassn.anz_pflanzen AS massn_anz_pflanzen,\n    tpopmassn.anz_pflanzstellen AS massn_anz_pflanzstellen,\n    tpopmassn.wirtspflanze AS massn_wirtspflanze,\n    tpopmassn.herkunft_pop AS massn_herkunft_pop,\n    tpopmassn.sammeldatum AS massn_sammeldatum,\n    tpopmassn.bemerkungen AS massn_bemerkungen,\n    (tpopmassn.changed)::timestamp without time zone AS massn_changed,\n    tpopmassn.changed_by AS massn_changed_by\n   FROM apflora.tpopmassn;\n\n\n\n\nCREATE VIEW apflora.v_massn_webgisbun AS\n SELECT ap.id AS \"APARTID\",\n    ae_eigenschaften.artname AS \"APART\",\n    pop.id AS \"POPGUID\",\n    pop.nr AS \"POPNR\",\n    tpop.id AS \"TPOPGUID\",\n    tpop.nr AS \"TPOPNR\",\n    tpop.x AS \"TPOP_X\",\n    tpop.y AS \"TPOP_Y\",\n    pop_status_werte_2.text AS \"TPOPSTATUS\",\n    tpopmassn.id AS \"MASSNGUID\",\n    tpopmassn.jahr AS \"MASSNJAHR\",\n    to_char((tpopmassn.datum)::timestamp with time zone, 'DD.MM.YY'::text) AS \"MASSNDAT\",\n    tpopmassn_typ_werte.text AS \"MASSTYP\",\n    tpopmassn.beschreibung AS \"MASSNMASSNAHME\",\n    adresse.name AS \"MASSNBEARBEITER\",\n    (tpopmassn.bemerkungen)::character(1) AS \"MASSNBEMERKUNG\",\n    tpopmassn.plan_vorhanden AS \"MASSNPLAN\",\n    tpopmassn.plan_bezeichnung AS \"MASSPLANBEZ\",\n    tpopmassn.flaeche AS \"MASSNFLAECHE\",\n    tpopmassn.form AS \"MASSNFORMANSIEDL\",\n    tpopmassn.pflanzanordnung AS \"MASSNPFLANZANORDNUNG\",\n    tpopmassn.markierung AS \"MASSNMARKIERUNG\",\n    tpopmassn.anz_triebe AS \"MASSNANZTRIEBE\",\n    tpopmassn.anz_pflanzen AS \"MASSNANZPFLANZEN\",\n    tpopmassn.anz_pflanzstellen AS \"MASSNANZPFLANZSTELLEN\",\n    tpopmassn.wirtspflanze AS \"MASSNWIRTSPFLANZEN\",\n    tpopmassn.herkunft_pop AS \"MASSNHERKUNFTSPOP\",\n    tpopmassn.sammeldatum AS \"MASSNSAMMELDAT\",\n    to_char((tpopmassn.changed)::timestamp with time zone, 'DD.MM.YY'::text) AS \"MASSNCHANGEDAT\",\n    tpopmassn.changed_by AS \"MASSNCHANGEBY\"\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN (apflora.tpopmassn\n     LEFT JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassn.jahr, tpopmassn.datum, tpopmassn_typ_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_pop AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id,\n    pop.nr,\n    pop.name,\n    pop_status_werte.text AS status,\n    pop.bekannt_seit,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.x,\n    pop.y,\n    pop.changed,\n    pop.changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN (apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_pop_anzkontr AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id,\n    pop.nr,\n    pop.name,\n    pop_status_werte.text AS status,\n    pop.bekannt_seit,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.x,\n    pop.y,\n    count(tpopkontr.id) AS anzahl_kontrollen\n   FROM (((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     LEFT JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n  GROUP BY ap.id, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text, pop.id, pop.nr, pop.name, pop_status_werte.text, pop.status_unklar, pop.status_unklar_begruendung, pop.bekannt_seit, pop.x, pop.y\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_pop_anzmassn AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id,\n    pop.nr,\n    pop.name,\n    pop_status_werte.text AS status,\n    pop.bekannt_seit,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.x,\n    pop.y,\n    count(tpopmassn.id) AS anzahl_massnahmen\n   FROM (((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     LEFT JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n  GROUP BY ap.id, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text, pop.id, pop.nr, pop.name, pop_status_werte.text, pop.status_unklar, pop.status_unklar_begruendung, pop.bekannt_seit, pop.x, pop.y\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_berjahrundmassnjahr AS\n SELECT tpop.id,\n    tpopber.jahr\n   FROM (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\nUNION\n SELECT tpop.id,\n    tpopmassnber.jahr\n   FROM (apflora.tpop\n     JOIN apflora.tpopmassnber ON ((tpop.id = tpopmassnber.tpop_id)))\n  ORDER BY 2;\n\n\n\n\nCREATE VIEW apflora.v_pop_berjahrundmassnjahrvontpop AS\n SELECT tpop.pop_id,\n    v_tpop_berjahrundmassnjahr.jahr\n   FROM (apflora.v_tpop_berjahrundmassnjahr\n     JOIN apflora.tpop ON ((v_tpop_berjahrundmassnjahr.id = tpop.id)))\n  GROUP BY tpop.pop_id, v_tpop_berjahrundmassnjahr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_pop_berundmassnjahre AS\n SELECT pop.id,\n    popber.jahr\n   FROM (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id)))\nUNION\n SELECT pop.id,\n    popmassnber.jahr\n   FROM (apflora.pop\n     JOIN apflora.popmassnber ON ((pop.id = popmassnber.pop_id)))\n  ORDER BY 2;\n\n\n\n\nCREATE VIEW apflora.v_pop_fuergis_read AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    (pop.id)::text AS id,\n    pop.nr,\n    pop.name,\n    pop_status_werte.text AS status,\n    pop.bekannt_seit,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.x,\n    pop.y,\n    (pop.changed)::timestamp without time zone AS changed,\n    pop.changed_by\n   FROM (((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n  WHERE ((pop.x > 0) AND (pop.y > 0))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_pop_fuergis_write AS\n SELECT (pop.ap_id)::text AS ap_id,\n    (pop.id)::text AS id,\n    pop.nr,\n    pop.name,\n    pop.status,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.bekannt_seit,\n    pop.x,\n    pop.y,\n    (pop.changed)::timestamp without time zone AS changed,\n    pop.changed_by\n   FROM apflora.pop;\n\n\n\n\nCREATE VIEW apflora.v_pop_kml AS\n SELECT ae_eigenschaften.artname AS \"Art\",\n    pop.nr AS \"Label\",\n    \"substring\"(concat('Population: ', pop.nr, ' ', pop.name), 1, 225) AS \"Inhalte\",\n    round(((((((2.6779094 + (4.728982 * (((pop.x - 600000))::numeric / (1000000)::numeric))) + ((0.791484 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) + (((0.1306 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0436 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Laengengrad\",\n    round((((((((16.9023892 + (3.238272 * (((pop.y - 200000))::numeric / (1000000)::numeric))) - ((0.270978 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric))) - ((0.002528 * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0447 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.014 * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Breitengrad\",\n    concat('http://www.apflora.ch/Projekte/4635372c-431c-11e8-bb30-e77f6cdd35a6/Aktionspläne/', ap.id, '/Populationen/', pop.id) AS url\n   FROM (apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((pop.y IS NOT NULL) AND (pop.y IS NOT NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name;\n\n\n\n\nCREATE VIEW apflora.v_pop_kmlnamen AS\n SELECT ae_eigenschaften.artname AS \"Art\",\n    concat(ae_eigenschaften.artname, ' ', pop.nr) AS \"Label\",\n    \"substring\"(concat('Population: ', pop.nr, ' ', pop.name), 1, 225) AS \"Inhalte\",\n    round(((((((2.6779094 + (4.728982 * (((pop.x - 600000))::numeric / (1000000)::numeric))) + ((0.791484 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) + (((0.1306 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0436 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Laengengrad\",\n    round((((((((16.9023892 + (3.238272 * (((pop.y - 200000))::numeric / (1000000)::numeric))) - ((0.270978 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric))) - ((0.002528 * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0447 * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.x - 600000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) - (((0.014 * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric)) * (((pop.y - 200000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Breitengrad\",\n    concat('http://www.apflora.ch/Projekte/4635372c-431c-11e8-bb30-e77f6cdd35a6/Aktionspläne/', ap.id, '/Populationen/', pop.id) AS url\n   FROM (apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((pop.y IS NOT NULL) AND (pop.y IS NOT NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name;\n\n\n\n\nCREATE VIEW apflora.v_pop_letzterpopber0_overall AS\n SELECT popber.pop_id,\n    max(popber.jahr) AS jahr\n   FROM apflora.popber\n  WHERE (popber.jahr IS NOT NULL)\n  GROUP BY popber.pop_id;\n\n\n\n\nCREATE VIEW apflora.v_pop_letzterpopber_overall AS\n SELECT pop.ap_id,\n    pop.id,\n    v_pop_letzterpopber0_overall.jahr\n   FROM ((apflora.pop\n     JOIN apflora.v_pop_letzterpopber0_overall ON ((pop.id = v_pop_letzterpopber0_overall.pop_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.apber_relevant = 1) AND (pop.status <> 300))\n  GROUP BY pop.ap_id, pop.id, v_pop_letzterpopber0_overall.jahr;\n\n\n\n\nCREATE VIEW apflora.v_pop_letzterpopbermassn AS\n SELECT popmassnber.pop_id AS id,\n    max(popmassnber.jahr) AS jahr\n   FROM apflora.popmassnber\n  WHERE (popmassnber.jahr IS NOT NULL)\n  GROUP BY popmassnber.pop_id;\n\n\n\n\nCREATE VIEW apflora.v_pop_massnseitbeginnap AS\n SELECT tpopmassn.tpop_id\n   FROM (apflora.ap\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpopmassn.jahr >= ap.start_jahr)\n  GROUP BY tpopmassn.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_pop_mit_letzter_popber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_status,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    pop.changed AS pop_changed,\n    pop.changed_by AS pop_changed_by,\n    popber.id AS popber_id,\n    popber.jahr AS popber_jahr,\n    tpop_entwicklung_werte.text AS popber_entwicklung,\n    popber.bemerkungen AS popber_bemerkungen,\n    popber.changed AS popber_changed,\n    popber.changed_by AS popber_changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN ((apflora.pop\n     LEFT JOIN (apflora.v_pop_letzterpopber0_overall\n     LEFT JOIN (apflora.popber\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((popber.entwicklung = tpop_entwicklung_werte.code))) ON (((v_pop_letzterpopber0_overall.jahr = popber.jahr) AND (v_pop_letzterpopber0_overall.pop_id = popber.pop_id)))) ON ((pop.id = v_pop_letzterpopber0_overall.pop_id)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, v_pop_letzterpopber0_overall.jahr;\n\n\n\n\nCREATE VIEW apflora.v_pop_mit_letzter_popmassnber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_status,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    pop.changed AS pop_changed,\n    pop.changed_by AS pop_changed_by,\n    popmassnber.id AS popmassnber_id,\n    popmassnber.jahr AS popmassnber_jahr,\n    tpopmassn_erfbeurt_werte.text AS popmassnber_entwicklung,\n    popmassnber.bemerkungen AS popmassnber_bemerkungen,\n    popmassnber.changed AS popmassnber_changed,\n    popmassnber.changed_by AS popmassnber_changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN ((apflora.pop\n     LEFT JOIN (apflora.v_pop_letzterpopbermassn\n     LEFT JOIN (apflora.popmassnber\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((popmassnber.beurteilung = tpopmassn_erfbeurt_werte.code))) ON (((v_pop_letzterpopbermassn.jahr = popmassnber.jahr) AND (v_pop_letzterpopbermassn.id = popmassnber.pop_id)))) ON ((pop.id = v_pop_letzterpopbermassn.id)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, v_pop_letzterpopbermassn.jahr;\n\n\n\n\nCREATE VIEW apflora.v_pop_ohnekoord AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id,\n    pop.nr,\n    pop.name,\n    pop_status_werte.text AS status,\n    pop.bekannt_seit,\n    pop.status_unklar,\n    pop.status_unklar_begruendung,\n    pop.x,\n    pop.y,\n    pop.changed,\n    pop.changed_by\n   FROM (((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n  WHERE ((pop.x IS NULL) OR (pop.y IS NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_pop_popberundmassnber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    pop.changed AS pop_changed,\n    pop.changed_by AS pop_changed_by,\n    v_pop_berundmassnjahre.jahr,\n    popber.id AS popber_id,\n    popber.jahr AS popber_jahr,\n    tpop_entwicklung_werte.text AS popber_entwicklung,\n    popber.bemerkungen AS popber_bemerkungen,\n    popber.changed AS popber_changed,\n    popber.changed_by AS popber_changed_by,\n    popmassnber.id AS popmassnber_id,\n    popmassnber.jahr AS popmassnber_jahr,\n    tpopmassn_erfbeurt_werte.text AS popmassnber_entwicklung,\n    popmassnber.bemerkungen AS popmassnber_bemerkungen,\n    popmassnber.changed AS popmassnber_changed,\n    popmassnber.changed_by AS popmassnber_changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN (((apflora.pop\n     LEFT JOIN (apflora.v_pop_berundmassnjahre\n     LEFT JOIN (apflora.popmassnber\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((popmassnber.beurteilung = tpopmassn_erfbeurt_werte.code))) ON (((v_pop_berundmassnjahre.jahr = popmassnber.jahr) AND (v_pop_berundmassnjahre.id = popmassnber.pop_id)))) ON ((pop.id = v_pop_berundmassnjahre.id)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN (apflora.popber\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((popber.entwicklung = tpop_entwicklung_werte.code))) ON (((v_pop_berundmassnjahre.jahr = popber.jahr) AND (v_pop_berundmassnjahre.id = popber.pop_id)))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, v_pop_berundmassnjahre.jahr;\n\n\n\n\nCREATE VIEW apflora.v_pop_vonapohnestatus AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap.bearbeitung AS ap_bearbeitung,\n    pop.id,\n    pop.nr,\n    pop.name,\n    pop.status\n   FROM (apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ap.bearbeitung = 3) AND (pop.status IS NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_popber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    popber.id,\n    popber.jahr,\n    tpop_entwicklung_werte.text AS entwicklung,\n    popber.bemerkungen,\n    popber.changed,\n    popber.changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN apflora.popber ON ((pop.id = popber.pop_id)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((popber.entwicklung = tpop_entwicklung_werte.code)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, popber.jahr, tpop_entwicklung_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_popber_angezapbestjahr0 AS\n SELECT ap.id AS ap_id,\n    pop.id AS pop_id,\n    popber.id,\n    ae_eigenschaften.artname AS \"Artname\",\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS status,\n    popber.jahr AS \"PopBerJahr\",\n    tpop_entwicklung_werte.text AS \"PopBerEntwicklung\",\n    popber.bemerkungen AS \"PopBerTxt\"\n   FROM (((apflora.ae_eigenschaften\n     JOIN ((apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((popber.entwicklung = tpop_entwicklung_werte.code)));\n\n\n\n\nCREATE VIEW apflora.v_popmassnber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    pop.changed AS pop_changed,\n    pop.changed_by AS pop_changed_by,\n    popmassnber.id,\n    popmassnber.jahr,\n    tpopmassn_erfbeurt_werte.text AS beurteilung,\n    popmassnber.bemerkungen,\n    popmassnber.changed,\n    popmassnber.changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN apflora.popmassnber ON ((pop.id = popmassnber.pop_id)))\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((popmassnber.beurteilung = tpopmassn_erfbeurt_werte.code)))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_popmassnber_anzmassn0 AS\n SELECT popmassnber.pop_id,\n    popmassnber.jahr,\n    count(tpopmassn.id) AS anzahl_massnahmen\n   FROM (apflora.popmassnber\n     JOIN (apflora.tpop\n     LEFT JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((popmassnber.pop_id = tpop.pop_id)))\n  WHERE ((tpopmassn.jahr = popmassnber.jahr) OR (tpopmassn.jahr IS NULL))\n  GROUP BY popmassnber.pop_id, popmassnber.jahr\n  ORDER BY popmassnber.pop_id, popmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_popmassnber_anzmassn AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_status,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    pop.changed AS pop_changed,\n    pop.changed_by AS pop_changed_by,\n    popmassnber.id AS popmassnber_id,\n    popmassnber.jahr AS popmassnber_jahr,\n    tpopmassn_erfbeurt_werte.text AS popmassnber_entwicklung,\n    popmassnber.bemerkungen AS popmassnber_bemerkungen,\n    popmassnber.changed AS popmassnber_changed,\n    popmassnber.changed_by AS popmassnber_changed_by,\n    v_popmassnber_anzmassn0.anzahl_massnahmen\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN (apflora.popmassnber\n     LEFT JOIN apflora.v_popmassnber_anzmassn0 ON (((v_popmassnber_anzmassn0.pop_id = popmassnber.pop_id) AND (v_popmassnber_anzmassn0.jahr = popmassnber.jahr)))) ON ((pop.id = popmassnber.pop_id)))\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((popmassnber.beurteilung = tpopmassn_erfbeurt_werte.code)))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_q_ziel_ohnejahr AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    ziel.id\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE (ziel.jahr IS NULL)\n  ORDER BY ziel.id;\n\n\n\n\nCREATE VIEW apflora.v_q_ziel_ohnetyp AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    ziel.id,\n    ziel.jahr\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE (ziel.typ IS NULL)\n  ORDER BY ziel.jahr;\n\n\n\n\nCREATE VIEW apflora.v_q_ziel_ohneziel AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    ziel.id,\n    ziel.jahr\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE (ziel.bezeichnung IS NULL)\n  ORDER BY ziel.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_apber_ohnebeurteilung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'AP-Bericht ohne Beurteilung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'AP-Berichte'::text, (apber.id)::text] AS url,\n    ARRAY[concat('AP-Bericht (Jahr): ', apber.jahr)] AS text,\n    apber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN apflora.apber ON ((ap.id = apber.ap_id)))\n  WHERE ((apber.beurteilung IS NULL) AND (apber.jahr IS NOT NULL))\n  ORDER BY apber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_apber_ohnejahr AS\nSELECT\n    NULL::uuid AS proj_id,\n    NULL::uuid AS ap_id,\n    NULL::text AS hw,\n    NULL::text[] AS url,\n    NULL::text[] AS text;\n\n\n\n\nCREATE VIEW apflora.v_qk_apber_ohnevergleichvorjahrgesamtziel AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'AP-Bericht ohne Vergleich Vorjahr - Gesamtziel:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'AP-Berichte'::text, (apber.id)::text] AS url,\n    ARRAY[concat('AP-Bericht (Jahr): ', apber.jahr)] AS text,\n    apber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN apflora.apber ON ((ap.id = apber.ap_id)))\n  WHERE ((apber.vergleich_vorjahr_gesamtziel IS NULL) AND (apber.jahr IS NOT NULL))\n  ORDER BY apber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_assozart_ohneart AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Assoziierte Art ohne Art:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'assoziierte-Arten'::text, (assozart.id)::text] AS url,\n    ARRAY[concat('Assoziierte Art (id): ', assozart.id)] AS text\n   FROM (apflora.ap\n     JOIN apflora.assozart ON ((ap.id = assozart.ap_id)))\n  WHERE (assozart.ae_id IS NULL)\n  ORDER BY assozart.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_erfkrit_ohnebeurteilung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Erfolgskriterium ohne Beurteilung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Erfolgskriterien'::text, (erfkrit.id)::text] AS url,\n    ARRAY[concat('Erfolgskriterium (id): ', erfkrit.id)] AS text\n   FROM (apflora.ap\n     JOIN apflora.erfkrit ON ((ap.id = erfkrit.ap_id)))\n  WHERE (erfkrit.erfolg IS NULL)\n  ORDER BY erfkrit.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_erfkrit_ohnekriterien AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Erfolgskriterium ohne Kriterien:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Erfolgskriterien'::text, (erfkrit.id)::text] AS url,\n    ARRAY[concat('Erfolgskriterium (id): ', erfkrit.id)] AS text\n   FROM (apflora.ap\n     JOIN apflora.erfkrit ON ((ap.id = erfkrit.ap_id)))\n  WHERE (erfkrit.kriterien IS NULL)\n  ORDER BY erfkrit.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontr_ohnebearb AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Feldkontrolle ohne BearbeiterIn:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Kontrolle (id): ', tpopkontr.id)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontr.bearbeiter IS NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontr_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Feldkontrolle ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontr.jahr IS NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontr_ohnetyp AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Feldkontrolle ohne Typ:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (((tpopkontr.typ IS NULL) OR ((tpopkontr.typ)::text = 'Erfolgskontrolle'::text)) AND (tpopkontr.jahr IS NOT NULL))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontr_ohnezaehlung AS\nSELECT\n    NULL::uuid AS proj_id,\n    NULL::uuid AS ap_id,\n    NULL::text AS hw,\n    NULL::text[] AS url,\n    NULL::text[] AS text,\n    NULL::smallint AS \"Berichtjahr\";\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontrzaehlung_ohneanzahl AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Anzahl (Feldkontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.anzahl IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontrzaehlung_ohneeinheit AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Zaehleinheit (Feldkontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.einheit IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_feldkontrzaehlung_ohnemethode AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Methode (Feldkontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.methode IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontr_ohnebearb AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Freiwilligen-Kontrolle ohne BearbeiterIn:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (id): ', tpopkontr.id)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontr.bearbeiter IS NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopkontr.bearbeiter;\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontr_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Freiwilligen-Kontrolle ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (id): ', tpopkontr.id)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontr.jahr IS NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontr_ohnezaehlung AS\nSELECT\n    NULL::uuid AS proj_id,\n    NULL::uuid AS ap_id,\n    NULL::text AS hw,\n    NULL::text[] AS url,\n    NULL::text[] AS text,\n    NULL::smallint AS \"Berichtjahr\";\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontrzaehlung_ohneanzahl AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Anzahl (Freiwilligen-Kontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.anzahl IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontrzaehlung_ohneeinheit AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Zaehleinheit (Freiwilligen-Kontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.einheit IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_freiwkontrzaehlung_ohnemethode AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Zaehlung ohne Methode (Freiwilligen-Kontrolle):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text, 'Zählungen'::text, (tpopkontrzaehl.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr), concat('Zählung (id): ', tpopkontrzaehl.id)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopkontrzaehl.methode IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_massn_ohnebearb AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Massnahme ohne BearbeiterIn:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Massnahmen'::text, (tpopmassn.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Massnahme (id): ', tpopmassn.id)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpopmassn.bearbeiter IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopmassn.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_massn_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Massnahme ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Massnahmen'::text, (tpopmassn.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Massnahme: ', tpopmassn.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpopmassn.jahr IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopmassn.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_massn_ohnetyp AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Massnahmen ohne Typ:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Massnahmen'::text, (tpopmassn.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Massnahme (Jahr): ', tpopmassn.jahr)] AS text,\n    tpopmassn.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopmassn.typ IS NULL) AND (tpopmassn.jahr IS NOT NULL))\n  ORDER BY pop.nr, tpop.nr, tpopmassn.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_massnber_ohneerfbeurt AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Massnahmen-Bericht ohne Entwicklung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Massnahmen-Berichte'::text, (tpopmassnber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Massnahmen-Bericht (Jahr): ', tpopmassnber.jahr)] AS text,\n    tpopmassnber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopmassnber ON ((tpop.id = tpopmassnber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopmassnber.beurteilung IS NULL) AND (tpopmassnber.jahr IS NOT NULL))\n  ORDER BY pop.nr, tpop.nr, tpopmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_massnber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Massnahmen-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Massnahmen-Berichte'::text, (tpopmassnber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Massnahmen-Bericht (Jahr): ', tpopmassnber.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopmassnber ON ((tpop.id = tpopmassnber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpopmassnber.jahr IS NULL)\n  ORDER BY pop.nr, tpop.nr, tpopmassnber.jahr, tpopmassnber.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_koordentsprechenkeinertpop AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Koordinaten entsprechen keiner Teilpopulation:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text,\n    pop.x AS \"XKoord\",\n    pop.y AS \"YKoord\"\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.x IS NOT NULL) AND (pop.y IS NOT NULL) AND (NOT (pop.id IN ( SELECT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.x = tpop.x) AND (tpop.y = tpop.y))))))\n  ORDER BY ap.proj_id, pop.ap_id;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_mit_ber_abnehmend_ohne_tpopber_abnehmend AS\n SELECT DISTINCT ap.proj_id,\n    ap.id AS ap_id,\n    pop.id AS pop_id,\n    popber.jahr AS \"Berichtjahr\",\n    'Populationen mit Bericht \"abnehmend\" ohne Teil-Population mit Bericht \"abnehmend\":'::text AS hw,\n    ARRAY['Projekte'::text, (ap.proj_id)::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popber.entwicklung = 1) AND (NOT (popber.pop_id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\n          WHERE ((tpopber.entwicklung = 1) AND (tpopber.jahr = popber.jahr))))))\n  ORDER BY ap.proj_id, ap.id, pop.id, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_mit_ber_erloschen_ohne_tpopber_erloschen AS\n SELECT DISTINCT ap.proj_id,\n    ap.id AS ap_id,\n    pop.id AS pop_id,\n    popber.jahr AS \"Berichtjahr\",\n    'Populationen mit Bericht \"erloschen\" ohne Teil-Population mit Bericht \"erloschen\":'::text AS hw,\n    ARRAY['Projekte'::text, (ap.proj_id)::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popber.entwicklung = 8) AND (NOT (popber.pop_id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\n          WHERE ((tpopber.entwicklung = 8) AND (tpopber.jahr = popber.jahr))))))\n  ORDER BY ap.proj_id, ap.id, pop.id, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_mit_ber_erloschen_und_tpopber_nicht_erloschen AS\n SELECT DISTINCT ap.proj_id,\n    ap.id AS ap_id,\n    pop.id AS pop_id,\n    popber.jahr AS \"Berichtjahr\",\n    'Populationen mit Bericht \"erloschen\" und mindestens einer gemäss Bericht nicht erloschenen Teil-Population:'::text AS hw,\n    ARRAY['Projekte'::text, (ap.proj_id)::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popber.entwicklung = 8) AND (popber.pop_id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\n          WHERE ((tpopber.entwicklung < 8) AND (tpopber.jahr = popber.jahr)))))\n  ORDER BY ap.proj_id, ap.id, pop.id, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_mit_ber_zunehmend_ohne_tpopber_zunehmend AS\n SELECT DISTINCT ap.proj_id,\n    ap.id AS ap_id,\n    pop.id AS pop_id,\n    popber.jahr AS \"Berichtjahr\",\n    'Populationen mit Bericht \"zunehmend\" ohne Teil-Population mit Bericht \"zunehmend\":'::text AS hw,\n    ARRAY['Projekte'::text, (ap.proj_id)::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popber.entwicklung = 3) AND (NOT (popber.pop_id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\n          WHERE ((tpopber.entwicklung = 3) AND (tpopber.jahr = popber.jahr))))))\n  ORDER BY ap.proj_id, ap.id, pop.id, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_mitstatusunklarohnebegruendung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population mit \"Status unklar\", ohne Begruendung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE ((pop.status_unklar = true) AND (pop.status_unklar_begruendung IS NULL))\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnebekanntseit AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population ohne \"bekannt seit\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE (pop.bekannt_seit IS NULL)\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnekoord AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population: Mindestens eine Koordinate fehlt:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE ((pop.x IS NULL) OR (pop.y IS NULL))\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnepopname AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population ohne Name:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population: ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE (pop.name IS NULL)\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnepopnr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population ohne Nr.:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Name): ', pop.name)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE (pop.nr IS NULL)\n  ORDER BY ap.id, pop.name;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnepopstatus AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population ohne Status:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n  WHERE (pop.status IS NULL)\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnetpop AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Population ohne Teilpopulation:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     LEFT JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.id IS NULL)\n  ORDER BY ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_ohnetpopmitgleichemstatus AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Keine Teil-Population hat den Status der Population:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND ((tpop.status = pop.status) OR ((tpop.status = 200) AND (pop.status = 210)) OR ((tpop.status = 210) AND (pop.status = 200)) OR ((tpop.status = 202) AND (pop.status = 211)) OR ((tpop.status = 211) AND (pop.status = 202)))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_popnrmehrdeutig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Die Nr. ist mehrdeutig:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.ap_id IN ( SELECT DISTINCT pop_1.ap_id\n           FROM apflora.pop pop_1\n          GROUP BY pop_1.ap_id, pop_1.nr\n         HAVING (count(*) > 1))) AND (pop.nr IN ( SELECT DISTINCT pop_1.nr\n           FROM apflora.pop pop_1\n          GROUP BY pop_1.ap_id, pop_1.nr\n         HAVING (count(*) > 1))))\n  ORDER BY projekt.id, ap.id, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status101tpopstatusanders AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"ursprünglich, erloschen\". Es gibt Teil-Populationen (ausser potentiellen Wuchs-/Ansiedlungsorten) mit abweichendem Status:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 101) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status <> ALL (ARRAY[101, 300]))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status200tpopstatusunzulaessig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"angesiedelt nach Beginn AP, aktuell\". Es gibt Teil-Populationen mit nicht zulässigen Stati (\"ursprünglich\", \"angesiedelt vor Beginn AP, aktuell\"):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 200) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status = ANY (ARRAY[100, 101, 210]))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status201tpopstatusunzulaessig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"Ansaatversuch\". Es gibt Teil-Populationen mit nicht zulässigen Stati (\"ursprünglich\" oder \"angesiedelt, aktuell\"):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 201) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status = ANY (ARRAY[100, 101, 200, 210]))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status202tpopstatusanders AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"angesiedelt nach Beginn AP, erloschen/nicht etabliert\". Es gibt Teil-Populationen mit abweichendem Status:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 202) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status <> 202)))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status210tpopstatusunzulaessig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"angesiedelt vor Beginn AP, aktuell\". Es gibt Teil-Populationen mit nicht zulässigen Stati (\"ursprünglich\"):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 210) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status = ANY (ARRAY[100, 101]))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status211tpopstatusunzulaessig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"angesiedelt vor Beginn AP, erloschen/nicht etabliert\". Es gibt Teil-Populationen mit nicht zulässigen Stati (\"ursprünglich\", \"angesiedelt, aktuell\", \"Ansaatversuch\", \"potentieller Wuchsort\"):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 211) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status = ANY (ARRAY[100, 101, 210, 200, 201, 300]))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_status300tpopstatusanders AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"potentieller Wuchs-/Ansiedlungsort\". Es gibt aber Teil-Populationen mit abweichendem Status:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = 300) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE ((tpop.pop_id = pop.id) AND (tpop.status <> 300)))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statusaktuellletzterpopbererloschen AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"aktuell\" (ursprünglich oder angesiedelt) oder potentieller Wuchsort; der letzte Populations-Bericht meldet aber \"erloschen\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[100, 200, 210, 300])) AND (lastpopber.entwicklung = 8) AND (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statusangesiedeltmittpopurspruenglich AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"angesiedelt\", es gibt aber eine Teilpopulation mit Status \"urspruenglich\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[200, 201, 202, 210, 211])) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status = 100))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statusansaatversuchalletpoperloschen AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"angesiedelt, Ansaatversuch\", alle Teilpopulationen sind gemäss Status erloschen:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = 201) AND (EXISTS ( SELECT 1\n           FROM apflora.tpop tpop_1\n          WHERE ((tpop_1.status = ANY (ARRAY[101, 202, 211])) AND (tpop_1.pop_id = pop.id)))) AND (NOT (EXISTS ( SELECT 1\n           FROM apflora.tpop tpop_1\n          WHERE ((tpop_1.status <> ALL (ARRAY[101, 202, 211])) AND (tpop_1.pop_id = pop.id))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statusansaatversuchmitaktuellentpop AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"angesiedelt, Ansaatversuch\", es gibt aber eine aktuelle Teilpopulation oder eine ursprüngliche erloschene:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = 201) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status = ANY (ARRAY[100, 101, 200, 210])))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statusansaatversuchmittpopursprerloschen AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"angesiedelt, Ansaatversuch\", es gibt aber eine Teilpopulation mit Status \"urspruenglich, erloschen\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = 201) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status = 101))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopberabnehmend AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Populations-Bericht meldet aber \"abnehmend\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lastpopber.entwicklung = 1) AND (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopberaktuell AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"erloschen\", der letzte Populations-Bericht meldet aber \"aktuell\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN ((apflora.pop\n     JOIN (apflora.popber\n     JOIN apflora.v_pop_letzterpopber0_overall ON (((v_pop_letzterpopber0_overall.jahr = popber.jahr) AND (v_pop_letzterpopber0_overall.pop_id = popber.pop_id)))) ON ((popber.pop_id = pop.id)))\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((popber.entwicklung < 8) AND (pop.status = ANY (ARRAY[101, 202, 211])) AND (tpop.apber_relevant = 1));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopbererloschenmitansiedlung AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"erloschen\" (ursprünglich oder angesiedelt); der letzte Populations-Bericht meldet \"erloschen\". Seither gab es aber eine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 202, 211])) AND (lastpopber.entwicklung = 8) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr)))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopberstabil AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Populations-Bericht meldet aber \"stabil\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lastpopber.entwicklung = 2) AND (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopberunsicher AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"erloschen\" (ursprünglich oder angesiedelt) oder potentieller Wuchsort; der letzte Populations-Bericht meldet aber \"unsicher\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 202, 211, 300])) AND (lastpopber.entwicklung = 4) AND (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenletzterpopberzunehmend AS\n WITH lastpopber AS (\n         SELECT DISTINCT ON (popber.pop_id) popber.pop_id,\n            popber.jahr,\n            popber.entwicklung\n           FROM apflora.popber\n          WHERE (popber.jahr IS NOT NULL)\n          ORDER BY popber.pop_id, popber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Population: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Populations-Bericht meldet aber \"zunehmend\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN lastpopber ON ((pop.id = lastpopber.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lastpopber.entwicklung = 3) AND (NOT (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM (apflora.tpop\n             JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n          WHERE ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3) AND (tpopmassn.jahr > lastpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenmittpopaktuell AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"erloschen\" (urspruenglich oder angesiedelt), es gibt aber eine Teilpopulation mit Status \"aktuell\" (urspruenglich oder angesiedelt):'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 202, 211])) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status = ANY (ARRAY[100, 200, 210])))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuserloschenmittpopansaatversuch AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"erloschen\" (urspruenglich oder angesiedelt), es gibt aber eine Teilpopulation mit Status \"angesiedelt, Ansaatversuch\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = ANY (ARRAY[101, 202, 211])) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status = 201))));\n\n\n\n\nCREATE VIEW apflora.v_qk_pop_statuspotwuchsortmittpopanders AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Population: Status ist \"potenzieller Wuchs-/Ansiedlungsort\", es gibt aber eine Teilpopulation mit Status \"angesiedelt\" oder \"urspruenglich\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text] AS url,\n    ARRAY[concat('Population (Nr): ', pop.nr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.pop ON ((pop.ap_id = ap.id)))\n  WHERE ((pop.status = 300) AND (pop.id IN ( SELECT DISTINCT tpop.pop_id\n           FROM apflora.tpop\n          WHERE (tpop.status < 300))));\n\n\n\n\nCREATE VIEW apflora.v_qk_popber_ohneentwicklung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Populations-Bericht ohne Entwicklung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Kontroll-Berichte'::text, (popber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Populations-Bericht (Jahr): ', popber.jahr)] AS text,\n    popber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popber.entwicklung IS NULL) AND (popber.jahr IS NOT NULL))\n  ORDER BY pop.nr, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_popber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Populations-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Kontroll-Berichte'::text, (popber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Populations-Bericht (Jahr): ', popber.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popber ON ((pop.id = popber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (popber.jahr IS NULL)\n  ORDER BY pop.nr, popber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_popmassnber_ohneentwicklung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Populations-Massnahmen-Bericht ohne Entwicklung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Massnahmen-Berichte'::text, (popmassnber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Populations-Massnahmen-Bericht (Jahr): ', popmassnber.jahr)] AS text,\n    popmassnber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popmassnber ON ((pop.id = popmassnber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((popmassnber.beurteilung IS NULL) AND (popmassnber.jahr IS NOT NULL))\n  ORDER BY pop.nr, popmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_popmassnber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Populations-Massnahmen-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Massnahmen-Berichte'::text, (popmassnber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Populations-Massnahmen-Bericht (Jahr): ', popmassnber.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.popmassnber ON ((pop.id = popmassnber.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (popmassnber.jahr IS NULL)\n  ORDER BY pop.nr, popmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_erloschenundrelevantaberletztebeobvor1950_maxbeobjahr AS\n SELECT beob.tpop_id AS id,\n    max(date_part('year'::text, beob.datum)) AS \"MaxJahr\"\n   FROM apflora.beob\n  WHERE ((beob.datum IS NOT NULL) AND (beob.tpop_id IS NOT NULL))\n  GROUP BY beob.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_erloschenundrelevantaberletztebeobvor1950 AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'erloschene Teilpopulation \"Fuer AP-Bericht relevant\" aber letzte Beobachtung vor 1950:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 202, 211])) AND (tpop.apber_relevant = 1) AND (NOT (tpop.id IN ( SELECT DISTINCT tpopkontr.tpop_id\n           FROM (apflora.tpopkontr\n             JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n          WHERE (((tpopkontr.typ)::text <> ALL (ARRAY[('Zwischenziel'::character varying)::text, ('Ziel'::character varying)::text])) AND (tpopkontrzaehl.anzahl > 0))))) AND (tpop.id IN ( SELECT beob.tpop_id\n           FROM (apflora.beob\n             JOIN apflora.v_qk_tpop_erloschenundrelevantaberletztebeobvor1950_maxbeobjahr ON ((beob.tpop_id = v_qk_tpop_erloschenundrelevantaberletztebeobvor1950_maxbeobjahr.id)))\n          WHERE (v_qk_tpop_erloschenundrelevantaberletztebeobvor1950_maxbeobjahr.\"MaxJahr\" < (1950)::double precision))))\n  ORDER BY pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_mitstatusaktuellundtpopbererloschen_maxtpopberjahr AS\n SELECT tpopber.tpop_id,\n    max(tpopber.jahr) AS \"MaxTPopBerJahr\"\n   FROM apflora.tpopber\n  GROUP BY tpopber.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_letztesjahr AS\n SELECT tpop.id,\n    max(tpopkontr.jahr) AS \"MaxTPopKontrJahr\",\n    count(tpopkontr.id) AS \"AnzTPopKontr\"\n   FROM (apflora.tpop\n     LEFT JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id)))\n  WHERE ((((tpopkontr.typ)::text <> ALL (ARRAY[('Ziel'::character varying)::text, ('Zwischenziel'::character varying)::text])) AND (tpopkontr.jahr IS NOT NULL)) OR ((tpopkontr.typ IS NULL) AND (tpopkontr.jahr IS NULL)))\n  GROUP BY tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_letzteid AS\n SELECT v_tpopkontr_letztesjahr.id,\n    max((tpopkontr.id)::text) AS \"MaxTPopKontrId\",\n    max(v_tpopkontr_letztesjahr.\"AnzTPopKontr\") AS \"AnzTPopKontr\"\n   FROM (apflora.tpopkontr\n     JOIN apflora.v_tpopkontr_letztesjahr ON (((v_tpopkontr_letztesjahr.\"MaxTPopKontrJahr\" = tpopkontr.jahr) AND (tpopkontr.tpop_id = v_tpopkontr_letztesjahr.id))))\n  GROUP BY v_tpopkontr_letztesjahr.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_mitstatusansaatversuchundzaehlungmitanzahl AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    pop.id AS pop_id,\n    tpop.id,\n    'Teilpopulation mit Status \"Ansaatversuch\", bei denen in der letzten Kontrolle eine Anzahl festgestellt wurde:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((tpop.status = 201) AND (tpop.id IN ( SELECT DISTINCT tpopkontr.tpop_id\n           FROM ((apflora.tpopkontr\n             JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n             JOIN apflora.v_tpopkontr_letzteid ON (((v_tpopkontr_letzteid.id = tpopkontr.tpop_id) AND (v_tpopkontr_letzteid.\"MaxTPopKontrId\" = (tpopkontr.id)::text))))\n          WHERE (((tpopkontr.typ)::text <> ALL (ARRAY[('Zwischenziel'::character varying)::text, ('Ziel'::character varying)::text])) AND (tpopkontrzaehl.anzahl > 0)))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_mitstatuspotentiellundmassnansiedlung AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    pop.id AS pop_id,\n    tpop.id,\n    'Teilpopulation mit Status \"potentieller Wuchs-/Ansiedlungsort\", bei der eine Massnahme des Typs \"Ansiedlung\" existiert:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((tpop.status = 300) AND (tpop.id IN ( SELECT DISTINCT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE (tpopmassn.typ < 4))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_mitstatuspotentiellundzaehlungmitanzahl AS\n SELECT DISTINCT projekt.id AS proj_id,\n    pop.ap_id,\n    pop.id AS pop_id,\n    tpop.id,\n    'Teilpopulation mit Status \"potentieller Wuchs-/Ansiedlungsort\", bei denen in einer Kontrolle eine Anzahl festgestellt wurde:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = 300) AND (tpop.id IN ( SELECT DISTINCT tpopkontr.tpop_id\n           FROM (apflora.tpopkontr\n             JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n          WHERE (((tpopkontr.typ)::text <> ALL (ARRAY[('Zwischenziel'::character varying)::text, ('Ziel'::character varying)::text])) AND (tpopkontrzaehl.anzahl > 0)))))\n  ORDER BY pop.id, tpop.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_mitstatusunklarohnebegruendung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation mit \"Status unklar\", ohne Begruendung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.status_unklar = true) AND (tpop.status_unklar_grund IS NULL))\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohneapberrelevant AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation ohne \"Fuer AP-Bericht relevant\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.apber_relevant IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohnebekanntseit AS\n SELECT ap.id AS ap_id,\n    'Teilpopulation ohne \"bekannt seit\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.bekannt_seit IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohneflurname AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation ohne Flurname:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.flurname IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohnekoordinaten AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Mindestens eine Koordinate fehlt:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.x IS NULL) OR (tpop.y IS NULL))\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohnenr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation ohne Nr.:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.nr IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_ohnestatus AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation ohne Status:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpop.status IS NULL)\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_popnrtpopnrmehrdeutig AS\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Die TPop.-Nr. ist mehrdeutig:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.pop_id IN ( SELECT DISTINCT tpop_1.pop_id\n           FROM apflora.tpop tpop_1\n          GROUP BY tpop_1.pop_id, tpop_1.nr\n         HAVING (count(*) > 1))) AND (tpop.nr IN ( SELECT tpop_1.nr\n           FROM apflora.tpop tpop_1\n          GROUP BY tpop_1.pop_id, tpop_1.nr\n         HAVING (count(*) > 1))))\n  ORDER BY projekt.id, ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statusaktuellletztertpopbererloschen AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"aktuell\" (ursprünglich oder angesiedelt) oder potentieller Wuchsort; der letzte Teilpopulations-Bericht meldet aber \"erloschen\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[100, 200, 210, 300])) AND (lasttpopber.entwicklung = 8) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztertpopber0_overall AS\n SELECT tpopber.tpop_id,\n    max(tpopber.jahr) AS tpopber_jahr\n   FROM apflora.tpopber\n  WHERE (tpopber.jahr IS NOT NULL)\n  GROUP BY tpopber.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletzterpopberaktuell AS\n SELECT DISTINCT ap.proj_id,\n    pop.ap_id,\n    'Teilpopulation: Status ist \"erloschen\", der letzte Teilpopulations-Bericht meldet aber \"aktuell\":'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora.v_tpop_letztertpopber0_overall ON (((v_tpop_letztertpopber0_overall.tpopber_jahr = tpopber.jahr) AND (v_tpop_letztertpopber0_overall.tpop_id = tpopber.tpop_id)))) ON ((tpopber.tpop_id = tpop.id))) ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id)))\n  WHERE ((tpopber.entwicklung < 8) AND (tpop.status = ANY (ARRAY[101, 202, 211])) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > tpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletztertpopberabnehmend AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Teilpopulations-Bericht meldet aber \"abnehmend\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lasttpopber.entwicklung = 1) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletztertpopbererloschenmitansiedlung AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"erloschen\" (ursprünglich oder angesiedelt); der letzte Teilpopulations-Bericht meldet \"erloschen\". Seither gab es aber eine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 202, 211])) AND (lasttpopber.entwicklung = 8) AND (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr)))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletztertpopberstabil AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Teilpopulations-Bericht meldet aber \"stabil\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lasttpopber.entwicklung = 2) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletztertpopberunsicher AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"erloschen\" (ursprünglich oder angesiedelt) oder potentieller Wuchsort; der letzte Teilpopulations-Bericht meldet aber \"unsicher\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 202, 211, 300])) AND (lasttpopber.entwicklung = 4) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuserloschenletztertpopberzunehmend AS\n WITH lasttpopber AS (\n         SELECT DISTINCT ON (tpopber.tpop_id) tpopber.tpop_id,\n            tpopber.jahr,\n            tpopber.entwicklung\n           FROM apflora.tpopber\n          WHERE (tpopber.jahr IS NOT NULL)\n          ORDER BY tpopber.tpop_id, tpopber.jahr DESC\n        )\n SELECT projekt.id AS proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation: Status ist \"erloschen\" (ursprünglich oder angesiedelt), Ansaatversuch oder potentieller Wuchsort; der letzte Teilpopulations-Bericht meldet aber \"zunehmend\" und es gab seither keine Ansiedlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.projekt\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN lasttpopber ON ((tpop.id = lasttpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((projekt.id = ap.proj_id)))\n  WHERE ((tpop.status = ANY (ARRAY[101, 201, 202, 211, 300])) AND (lasttpopber.entwicklung = 3) AND (NOT (tpop.id IN ( SELECT tpopmassn.tpop_id\n           FROM apflora.tpopmassn\n          WHERE ((tpopmassn.tpop_id = tpop.id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr IS NOT NULL) AND (tpopmassn.jahr > lasttpopber.jahr))))));\n\n\n\n\nCREATE VIEW apflora.v_qk_tpop_statuspotentiellfuerapberrelevant AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulation mit Status \"potenzieller Wuchs-/Ansiedlungsort\" und \"Fuer AP-Bericht relevant?\" = ja:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.status = 300) AND (tpop.apber_relevant = 1))\n  ORDER BY ap.id, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpopber_ohneentwicklung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulations-Bericht ohne Entwicklung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Kontroll-Berichte'::text, (tpopber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Teilpopulations-Bericht (Jahr): ', tpopber.jahr)] AS text,\n    tpopber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpopber.entwicklung IS NULL) AND (tpopber.jahr IS NOT NULL))\n  ORDER BY pop.nr, tpop.nr, tpopber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_tpopber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Teilpopulations-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Kontroll-Berichte'::text, (tpopber.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Teilpopulations-Bericht (id): ', tpopber.id)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (tpopber.jahr IS NULL)\n  ORDER BY pop.nr, tpop.nr, tpopber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_ziel_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Ziel ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Ziele'::text, (ziel.id)::text] AS url,\n    ARRAY[concat('Ziel (id): ', ziel.id)] AS text\n   FROM (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id)))\n  WHERE ((ziel.jahr IS NULL) OR (ziel.jahr = 1))\n  ORDER BY ziel.id;\n\n\n\n\nCREATE VIEW apflora.v_qk_ziel_ohnetyp AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Ziel ohne Typ:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Ziele'::text, (ziel.id)::text] AS url,\n    ARRAY[concat('Ziel (Jahr): ', ziel.jahr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id)))\n  WHERE (ziel.typ IS NULL)\n  ORDER BY ziel.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_ziel_ohneziel AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Ziel ohne Ziel:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Ziele'::text, (ziel.id)::text] AS url,\n    ARRAY[concat('Ziel (Jahr): ', ziel.jahr)] AS text\n   FROM (apflora.ap\n     JOIN apflora.ziel ON ((ap.id = ziel.ap_id)))\n  WHERE (ziel.bezeichnung IS NULL)\n  ORDER BY ziel.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_zielber_ohneentwicklung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Ziel-Bericht ohne Entwicklung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Ziele'::text, (ziel.id)::text, 'Berichte'::text, (zielber.id)::text] AS url,\n    ARRAY[concat('Ziel (Jahr): ', ziel.jahr), concat('Ziel-Bericht (Jahr): ', zielber.jahr)] AS text,\n    zielber.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.ziel\n     JOIN apflora.zielber ON ((ziel.id = zielber.ziel_id))) ON ((ap.id = ziel.ap_id)))\n  WHERE ((zielber.erreichung IS NULL) AND (zielber.jahr IS NOT NULL))\n  ORDER BY ziel.jahr, ziel.id, zielber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_qk_zielber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Ziel-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Ziele'::text, (ziel.id)::text, 'Berichte'::text, (zielber.id)::text] AS url,\n    ARRAY[concat('Ziel (Jahr): ', ziel.jahr), concat('Ziel-Bericht (Jahr): ', zielber.jahr)] AS text\n   FROM (apflora.ap\n     JOIN (apflora.ziel\n     JOIN apflora.zielber ON ((ziel.id = zielber.ziel_id))) ON ((ap.id = ziel.ap_id)))\n  WHERE (zielber.jahr IS NULL)\n  ORDER BY ziel.jahr, ziel.id, zielber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_tpop AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.familie,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    pop_status_werte_2.text AS status,\n    tpop.bekannt_seit,\n    tpop.status_unklar,\n    tpop.status_unklar_grund,\n    tpop.x,\n    tpop.y,\n    tpop.radius,\n    tpop.hoehe,\n    tpop.exposition,\n    tpop.klima,\n    tpop.neigung,\n    tpop.beschreibung,\n    tpop.kataster_nr,\n    tpop.apber_relevant,\n    tpop.eigentuemer,\n    tpop.kontakt,\n    tpop.nutzungszone,\n    tpop.bewirtschafter,\n    tpop.bewirtschaftung,\n    tpop.changed,\n    tpop.changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_letztekontrid AS\n SELECT tpop.id,\n    v_tpopkontr_letzteid.\"MaxTPopKontrId\",\n    v_tpopkontr_letzteid.\"AnzTPopKontr\"\n   FROM (apflora.tpop\n     LEFT JOIN apflora.v_tpopkontr_letzteid ON ((tpop.id = v_tpopkontr_letzteid.id)));\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.familie,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    apflora_adresse_1.name AS ap_bearbeiter,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    pop_status_werte_2.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopkontr.id,\n    tpopkontr.jahr,\n    tpopkontr.datum,\n    tpopkontr_typ_werte.text AS typ,\n    adresse.name AS bearbeiter,\n    tpopkontr.ueberlebensrate,\n    tpopkontr.vitalitaet,\n    tpop_entwicklung_werte.text AS entwicklung,\n    tpopkontr.ursachen,\n    tpopkontr.erfolgsbeurteilung,\n    tpopkontr.umsetzung_aendern,\n    tpopkontr.kontrolle_aendern,\n    tpopkontr.bemerkungen,\n    tpopkontr.lr_delarze,\n    tpopkontr.lr_umgebung_delarze,\n    tpopkontr.vegetationstyp,\n    tpopkontr.konkurrenz,\n    tpopkontr.moosschicht,\n    tpopkontr.krautschicht,\n    tpopkontr.strauchschicht,\n    tpopkontr.baumschicht,\n    tpopkontr.boden_typ,\n    tpopkontr.boden_kalkgehalt,\n    tpopkontr.boden_durchlaessigkeit,\n    tpopkontr.boden_humus,\n    tpopkontr.boden_naehrstoffgehalt,\n    tpopkontr.boden_abtrag,\n    tpopkontr.wasserhaushalt,\n    tpopkontr_idbiotuebereinst_werte.text AS idealbiotop_uebereinstimmung,\n    tpopkontr.handlungsbedarf,\n    tpopkontr.flaeche_ueberprueft,\n    tpopkontr.flaeche,\n    tpopkontr.plan_vorhanden,\n    tpopkontr.deckung_vegetation,\n    tpopkontr.deckung_nackter_boden,\n    tpopkontr.deckung_ap_art,\n    tpopkontr.jungpflanzen_vorhanden,\n    tpopkontr.vegetationshoehe_maximum,\n    tpopkontr.vegetationshoehe_mittel,\n    tpopkontr.gefaehrdung,\n    tpopkontr.changed,\n    tpopkontr.changed_by,\n    array_to_string(array_agg(tpopkontrzaehl.anzahl), ', '::text) AS zaehlung_anzahlen,\n    string_agg((tpopkontrzaehl_einheit_werte.text)::text, ', '::text) AS zaehlung_einheiten,\n    string_agg((tpopkontrzaehl_methode_werte.text)::text, ', '::text) AS zaehlung_methoden\n   FROM (apflora.pop_status_werte pop_status_werte_2\n     RIGHT JOIN (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN ((((((apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontr_typ_werte ON (((tpopkontr.typ)::text = (tpopkontr_typ_werte.text)::text)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopkontr.entwicklung = tpop_entwicklung_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n     LEFT JOIN apflora.tpopkontrzaehl_einheit_werte ON ((tpopkontrzaehl.einheit = tpopkontrzaehl_einheit_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl_methode_werte ON ((tpopkontrzaehl.methode = tpopkontrzaehl_methode_werte.code))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.tpopkontr_idbiotuebereinst_werte ON ((tpopkontr.idealbiotop_uebereinstimmung = tpopkontr_idbiotuebereinst_werte.code)))\n     LEFT JOIN apflora.adresse apflora_adresse_1 ON ((ap.bearbeiter = apflora_adresse_1.id))) ON ((pop_status_werte_2.code = tpop.status)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  GROUP BY ap.id, ae_eigenschaften.familie, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text, apflora_adresse_1.name, pop.id, pop.nr, pop.name, pop_status_werte.text, pop.bekannt_seit, tpop.id, tpop.nr, tpop.gemeinde, tpop.flurname, pop_status_werte_2.text, tpop.bekannt_seit, tpop.status_unklar, tpop.status_unklar_grund, tpop.x, tpop.y, tpop.radius, tpop.hoehe, tpop.exposition, tpop.klima, tpop.neigung, tpop.beschreibung, tpop.kataster_nr, tpop.apber_relevant, tpop.eigentuemer, tpop.kontakt, tpop.nutzungszone, tpop.bewirtschafter, tpop.bewirtschaftung, tpopkontr.id, tpopkontr.tpop_id, tpopkontr.jahr, tpopkontr.datum, tpopkontr_typ_werte.text, adresse.name, tpopkontr.ueberlebensrate, tpopkontr.vitalitaet, tpop_entwicklung_werte.text, tpopkontr.ursachen, tpopkontr.erfolgsbeurteilung, tpopkontr.umsetzung_aendern, tpopkontr.kontrolle_aendern, tpopkontr.bemerkungen, tpopkontr.lr_delarze, tpopkontr.lr_umgebung_delarze, tpopkontr.vegetationstyp, tpopkontr.konkurrenz, tpopkontr.moosschicht, tpopkontr.krautschicht, tpopkontr.strauchschicht, tpopkontr.baumschicht, tpopkontr.boden_typ, tpopkontr.boden_kalkgehalt, tpopkontr.boden_durchlaessigkeit, tpopkontr.boden_humus, tpopkontr.boden_naehrstoffgehalt, tpopkontr.boden_abtrag, tpopkontr.wasserhaushalt, tpopkontr_idbiotuebereinst_werte.text, tpopkontr.handlungsbedarf, tpopkontr.flaeche_ueberprueft, tpopkontr.flaeche, tpopkontr.plan_vorhanden, tpopkontr.deckung_vegetation, tpopkontr.deckung_nackter_boden, tpopkontr.deckung_ap_art, tpopkontr.jungpflanzen_vorhanden, tpopkontr.vegetationshoehe_maximum, tpopkontr.vegetationshoehe_mittel, tpopkontr.gefaehrdung, tpopkontr.changed, tpopkontr.changed_by\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_anzkontrinklletzter AS\n SELECT v_tpop.ap_id,\n    v_tpop.familie,\n    v_tpop.artname,\n    v_tpop.ap_bearbeitung,\n    v_tpop.ap_start_jahr,\n    v_tpop.ap_umsetzung,\n    v_tpop.ap_bearbeiter,\n    v_tpop.pop_id,\n    v_tpop.pop_nr,\n    v_tpop.pop_name,\n    v_tpop.pop_status,\n    v_tpop.pop_bekannt_seit,\n    v_tpop.pop_status_unklar,\n    v_tpop.pop_status_unklar_begruendung,\n    v_tpop.pop_x,\n    v_tpop.pop_y,\n    v_tpop.id,\n    v_tpop.nr,\n    v_tpop.gemeinde,\n    v_tpop.flurname,\n    v_tpop.status,\n    v_tpop.bekannt_seit,\n    v_tpop.status_unklar,\n    v_tpop.status_unklar_grund,\n    v_tpop.x,\n    v_tpop.y,\n    v_tpop.radius,\n    v_tpop.hoehe,\n    v_tpop.exposition,\n    v_tpop.klima,\n    v_tpop.neigung,\n    v_tpop.beschreibung,\n    v_tpop.kataster_nr,\n    v_tpop.apber_relevant,\n    v_tpop.eigentuemer,\n    v_tpop.kontakt,\n    v_tpop.nutzungszone,\n    v_tpop.bewirtschafter,\n    v_tpop.bewirtschaftung,\n    v_tpop.changed,\n    v_tpop.changed_by,\n    v_tpop_letztekontrid.\"AnzTPopKontr\" AS anzahl_kontrollen,\n    v_tpopkontr.id AS kontr_id,\n    v_tpopkontr.jahr AS kontr_jahr,\n    v_tpopkontr.datum AS kontr_datum,\n    v_tpopkontr.typ AS kontr_typ,\n    v_tpopkontr.bearbeiter AS kontr_bearbeiter,\n    v_tpopkontr.ueberlebensrate AS kontr_ueberlebensrate,\n    v_tpopkontr.vitalitaet AS kontr_vitalitaet,\n    v_tpopkontr.entwicklung AS kontr_entwicklung,\n    v_tpopkontr.ursachen AS kontr_ursachen,\n    v_tpopkontr.erfolgsbeurteilung AS kontr_erfolgsbeurteilung,\n    v_tpopkontr.umsetzung_aendern AS kontr_umsetzung_aendern,\n    v_tpopkontr.kontrolle_aendern AS kontr_kontrolle_aendern,\n    v_tpopkontr.bemerkungen AS kontr_bemerkungen,\n    v_tpopkontr.lr_delarze AS kontr_lr_delarze,\n    v_tpopkontr.lr_umgebung_delarze AS kontr_lr_umgebung_delarze,\n    v_tpopkontr.vegetationstyp AS kontr_vegetationstyp,\n    v_tpopkontr.konkurrenz AS kontr_konkurrenz,\n    v_tpopkontr.moosschicht AS kontr_moosschicht,\n    v_tpopkontr.krautschicht AS kontr_krautschicht,\n    v_tpopkontr.strauchschicht AS kontr_strauchschicht,\n    v_tpopkontr.baumschicht AS kontr_baumschicht,\n    v_tpopkontr.boden_typ AS kontr_boden_typ,\n    v_tpopkontr.boden_kalkgehalt AS kontr_boden_kalkgehalt,\n    v_tpopkontr.boden_durchlaessigkeit AS kontr_boden_durchlaessigkeit,\n    v_tpopkontr.boden_humus AS kontr_boden_humus,\n    v_tpopkontr.boden_naehrstoffgehalt AS kontr_boden_naehrstoffgehalt,\n    v_tpopkontr.boden_abtrag AS kontr_boden_abtrag,\n    v_tpopkontr.wasserhaushalt AS kontr_wasserhaushalt,\n    v_tpopkontr.idealbiotop_uebereinstimmung AS kontr_idealbiotop_uebereinstimmung,\n    v_tpopkontr.handlungsbedarf AS kontr_handlungsbedarf,\n    v_tpopkontr.flaeche_ueberprueft AS kontr_flaeche_ueberprueft,\n    v_tpopkontr.flaeche AS kontr_flaeche,\n    v_tpopkontr.plan_vorhanden AS kontr_plan_vorhanden,\n    v_tpopkontr.deckung_vegetation AS kontr_deckung_vegetation,\n    v_tpopkontr.deckung_nackter_boden AS kontr_deckung_nackter_boden,\n    v_tpopkontr.deckung_ap_art AS kontr_deckung_ap_art,\n    v_tpopkontr.jungpflanzen_vorhanden AS kontr_jungpflanzen_vorhanden,\n    v_tpopkontr.vegetationshoehe_maximum AS kontr_vegetationshoehe_maximum,\n    v_tpopkontr.vegetationshoehe_mittel AS kontr_vegetationshoehe_mittel,\n    v_tpopkontr.gefaehrdung AS kontr_gefaehrdung,\n    v_tpopkontr.changed AS kontr_changed,\n    v_tpopkontr.changed_by AS kontr_changed_by,\n    v_tpopkontr.zaehlung_anzahlen,\n    v_tpopkontr.zaehlung_einheiten,\n    v_tpopkontr.zaehlung_methoden\n   FROM ((apflora.v_tpop_letztekontrid\n     LEFT JOIN apflora.v_tpopkontr ON ((v_tpop_letztekontrid.\"MaxTPopKontrId\" = (v_tpopkontr.id)::text)))\n     JOIN apflora.v_tpop ON ((v_tpop_letztekontrid.id = v_tpop.id)));\n\n\n\n\nCREATE VIEW apflora.v_tpopber_letzteid AS\n SELECT tpopkontr.tpop_id,\n    ( SELECT tpopber_1.id\n           FROM apflora.tpopber tpopber_1\n          WHERE (tpopber_1.tpop_id = tpopkontr.tpop_id)\n          ORDER BY tpopber_1.changed DESC\n         LIMIT 1) AS tpopber_letzte_id,\n    max(tpopber.jahr) AS tpopber_jahr_max,\n    count(tpopber.id) AS tpopber_anz\n   FROM (apflora.tpopkontr\n     JOIN apflora.tpopber ON ((tpopkontr.tpop_id = tpopber.tpop_id)))\n  WHERE (((tpopkontr.typ)::text <> ALL (ARRAY[('Ziel'::character varying)::text, ('Zwischenziel'::character varying)::text])) AND (tpopber.jahr IS NOT NULL))\n  GROUP BY tpopkontr.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_tpopber_mitletzterid AS\n SELECT tpopber.tpop_id,\n    v_tpopber_letzteid.tpopber_anz,\n    tpopber.id,\n    tpopber.jahr,\n    tpop_entwicklung_werte.text AS entwicklung,\n    tpopber.bemerkungen,\n    tpopber.changed,\n    tpopber.changed_by\n   FROM ((apflora.v_tpopber_letzteid\n     JOIN apflora.tpopber ON (((v_tpopber_letzteid.tpopber_letzte_id = tpopber.id) AND (v_tpopber_letzteid.tpop_id = tpopber.tpop_id))))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopber.entwicklung = tpop_entwicklung_werte.code)));\n\n\n\n\nCREATE VIEW apflora.v_tpop_anzkontrinklletzterundletztertpopber AS\n SELECT v_tpop_anzkontrinklletzter.ap_id,\n    v_tpop_anzkontrinklletzter.familie,\n    v_tpop_anzkontrinklletzter.artname,\n    v_tpop_anzkontrinklletzter.ap_bearbeitung,\n    v_tpop_anzkontrinklletzter.ap_start_jahr,\n    v_tpop_anzkontrinklletzter.ap_umsetzung,\n    v_tpop_anzkontrinklletzter.ap_bearbeiter,\n    v_tpop_anzkontrinklletzter.pop_id,\n    v_tpop_anzkontrinklletzter.pop_nr,\n    v_tpop_anzkontrinklletzter.pop_name,\n    v_tpop_anzkontrinklletzter.pop_status,\n    v_tpop_anzkontrinklletzter.pop_bekannt_seit,\n    v_tpop_anzkontrinklletzter.pop_status_unklar,\n    v_tpop_anzkontrinklletzter.pop_status_unklar_begruendung,\n    v_tpop_anzkontrinklletzter.pop_x,\n    v_tpop_anzkontrinklletzter.pop_y,\n    v_tpop_anzkontrinklletzter.id,\n    v_tpop_anzkontrinklletzter.nr,\n    v_tpop_anzkontrinklletzter.gemeinde,\n    v_tpop_anzkontrinklletzter.flurname,\n    v_tpop_anzkontrinklletzter.status,\n    v_tpop_anzkontrinklletzter.bekannt_seit,\n    v_tpop_anzkontrinklletzter.status_unklar,\n    v_tpop_anzkontrinklletzter.status_unklar_grund,\n    v_tpop_anzkontrinklletzter.x,\n    v_tpop_anzkontrinklletzter.y,\n    v_tpop_anzkontrinklletzter.radius,\n    v_tpop_anzkontrinklletzter.hoehe,\n    v_tpop_anzkontrinklletzter.exposition,\n    v_tpop_anzkontrinklletzter.klima,\n    v_tpop_anzkontrinklletzter.neigung,\n    v_tpop_anzkontrinklletzter.beschreibung,\n    v_tpop_anzkontrinklletzter.kataster_nr,\n    v_tpop_anzkontrinklletzter.apber_relevant,\n    v_tpop_anzkontrinklletzter.eigentuemer,\n    v_tpop_anzkontrinklletzter.kontakt,\n    v_tpop_anzkontrinklletzter.nutzungszone,\n    v_tpop_anzkontrinklletzter.bewirtschafter,\n    v_tpop_anzkontrinklletzter.bewirtschaftung,\n    v_tpop_anzkontrinklletzter.changed,\n    v_tpop_anzkontrinklletzter.changed_by,\n    v_tpop_anzkontrinklletzter.anzahl_kontrollen,\n    v_tpop_anzkontrinklletzter.kontr_id,\n    v_tpop_anzkontrinklletzter.kontr_jahr,\n    v_tpop_anzkontrinklletzter.kontr_datum,\n    v_tpop_anzkontrinklletzter.kontr_typ,\n    v_tpop_anzkontrinklletzter.kontr_bearbeiter,\n    v_tpop_anzkontrinklletzter.kontr_ueberlebensrate,\n    v_tpop_anzkontrinklletzter.kontr_vitalitaet,\n    v_tpop_anzkontrinklletzter.kontr_entwicklung,\n    v_tpop_anzkontrinklletzter.kontr_ursachen,\n    v_tpop_anzkontrinklletzter.kontr_erfolgsbeurteilung,\n    v_tpop_anzkontrinklletzter.kontr_umsetzung_aendern,\n    v_tpop_anzkontrinklletzter.kontr_kontrolle_aendern,\n    v_tpop_anzkontrinklletzter.kontr_bemerkungen,\n    v_tpop_anzkontrinklletzter.kontr_lr_delarze,\n    v_tpop_anzkontrinklletzter.kontr_lr_umgebung_delarze,\n    v_tpop_anzkontrinklletzter.kontr_vegetationstyp,\n    v_tpop_anzkontrinklletzter.kontr_konkurrenz,\n    v_tpop_anzkontrinklletzter.kontr_moosschicht,\n    v_tpop_anzkontrinklletzter.kontr_krautschicht,\n    v_tpop_anzkontrinklletzter.kontr_strauchschicht,\n    v_tpop_anzkontrinklletzter.kontr_baumschicht,\n    v_tpop_anzkontrinklletzter.kontr_boden_typ,\n    v_tpop_anzkontrinklletzter.kontr_boden_kalkgehalt,\n    v_tpop_anzkontrinklletzter.kontr_boden_durchlaessigkeit,\n    v_tpop_anzkontrinklletzter.kontr_boden_humus,\n    v_tpop_anzkontrinklletzter.kontr_boden_naehrstoffgehalt,\n    v_tpop_anzkontrinklletzter.kontr_boden_abtrag,\n    v_tpop_anzkontrinklletzter.kontr_wasserhaushalt,\n    v_tpop_anzkontrinklletzter.kontr_idealbiotop_uebereinstimmung,\n    v_tpop_anzkontrinklletzter.kontr_handlungsbedarf,\n    v_tpop_anzkontrinklletzter.kontr_flaeche_ueberprueft,\n    v_tpop_anzkontrinklletzter.kontr_flaeche,\n    v_tpop_anzkontrinklletzter.kontr_plan_vorhanden,\n    v_tpop_anzkontrinklletzter.kontr_deckung_vegetation,\n    v_tpop_anzkontrinklletzter.kontr_deckung_nackter_boden,\n    v_tpop_anzkontrinklletzter.kontr_deckung_ap_art,\n    v_tpop_anzkontrinklletzter.kontr_jungpflanzen_vorhanden,\n    v_tpop_anzkontrinklletzter.kontr_vegetationshoehe_maximum,\n    v_tpop_anzkontrinklletzter.kontr_vegetationshoehe_mittel,\n    v_tpop_anzkontrinklletzter.kontr_gefaehrdung,\n    v_tpop_anzkontrinklletzter.kontr_changed,\n    v_tpop_anzkontrinklletzter.kontr_changed_by,\n    v_tpop_anzkontrinklletzter.zaehlung_anzahlen,\n    v_tpop_anzkontrinklletzter.zaehlung_einheiten,\n    v_tpop_anzkontrinklletzter.zaehlung_methoden,\n    v_tpopber_mitletzterid.tpopber_anz,\n    v_tpopber_mitletzterid.id AS tpopber_id,\n    v_tpopber_mitletzterid.jahr AS tpopber_jahr,\n    v_tpopber_mitletzterid.entwicklung AS tpopber_entwicklung,\n    v_tpopber_mitletzterid.bemerkungen AS tpopber_bemerkungen,\n    v_tpopber_mitletzterid.changed AS tpopber_changed,\n    v_tpopber_mitletzterid.changed_by AS tpopber_changed_by\n   FROM (apflora.v_tpop_anzkontrinklletzter\n     LEFT JOIN apflora.v_tpopber_mitletzterid ON ((v_tpop_anzkontrinklletzter.id = v_tpopber_mitletzterid.tpop_id)));\n\n\n\n\nCREATE VIEW apflora.v_tpop_anzmassn AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.familie,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    pop_status_werte_2.text AS status,\n    tpop.bekannt_seit,\n    tpop.status_unklar,\n    tpop.status_unklar_grund,\n    tpop.x,\n    tpop.y,\n    tpop.radius,\n    tpop.hoehe,\n    tpop.exposition,\n    tpop.klima,\n    tpop.neigung,\n    tpop.beschreibung,\n    tpop.kataster_nr,\n    tpop.apber_relevant,\n    tpop.eigentuemer,\n    tpop.kontakt,\n    tpop.nutzungszone,\n    tpop.bewirtschafter,\n    tpop.bewirtschaftung,\n    count(tpopmassn.id) AS anzahl_massnahmen\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.tpopmassn ON ((tpop.id = tpopmassn.tpop_id)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code))) ON ((ae_eigenschaften.id = ap.art_id)))\n  GROUP BY ap.id, ae_eigenschaften.familie, ae_eigenschaften.artname, ap_bearbstand_werte.text, ap.start_jahr, ap_umsetzung_werte.text, pop.id, pop.nr, pop.name, pop_status_werte.text, pop.bekannt_seit, pop.status_unklar, pop.status_unklar_begruendung, pop.x, pop.y, tpop.id, tpop.nr, tpop.gemeinde, tpop.flurname, pop_status_werte_2.text, tpop.bekannt_seit, tpop.status_unklar, tpop.status_unklar_grund, tpop.x, tpop.y, tpop.radius, tpop.hoehe, tpop.exposition, tpop.klima, tpop.neigung, tpop.beschreibung, tpop.kataster_nr, tpop.apber_relevant, tpop.eigentuemer, tpop.kontakt, tpop.nutzungszone, tpop.bewirtschafter, tpop.bewirtschaftung\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_for_ap AS\n SELECT tpop.id_old,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    tpop.x,\n    tpop.y,\n    tpop.radius,\n    tpop.hoehe,\n    tpop.exposition,\n    tpop.klima,\n    tpop.neigung,\n    tpop.beschreibung,\n    tpop.kataster_nr,\n    tpop.status,\n    tpop.status_unklar_grund,\n    tpop.apber_relevant,\n    tpop.bekannt_seit,\n    tpop.eigentuemer,\n    tpop.kontakt,\n    tpop.nutzungszone,\n    tpop.bewirtschafter,\n    tpop.bewirtschaftung,\n    tpop.bemerkungen,\n    tpop.changed,\n    tpop.changed_by,\n    tpop.id,\n    tpop.pop_id,\n    tpop.status_unklar,\n    ap.id AS ap_id\n   FROM ((apflora.ap\n     JOIN apflora.pop ON ((ap.id = pop.ap_id)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)));\n\n\n\n\nCREATE VIEW apflora.v_tpop_fuergis_read AS\n SELECT (ap.id)::text AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    (pop.id)::text AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    (tpop.id)::text AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    pop_status_werte_2.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    (tpop.changed)::timestamp without time zone AS tpop_changed,\n    tpop.changed_by AS tpop_changed_by\n   FROM ((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n  WHERE ((tpop.y > 0) AND (tpop.x > 0))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_fuergis_write AS\n SELECT (tpop.pop_id)::text AS pop_id,\n    (tpop.id)::text AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpop.status AS tpop_status,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpop.bemerkungen AS tpop_bemerkungen,\n    (tpop.changed)::timestamp without time zone AS tpop_changed,\n    tpop.changed_by AS tpop_changed_by\n   FROM apflora.tpop;\n\n\n\n\nCREATE VIEW apflora.v_tpop_kml AS\n SELECT ae_eigenschaften.artname AS \"Art\",\n    concat(pop.nr, '/', tpop.nr) AS \"Label\",\n    \"substring\"(concat('Population: ', pop.nr, ' ', pop.name, '<br /> Teilpopulation: ', tpop.nr, ' ', tpop.gemeinde, ' ', tpop.flurname), 1, 225) AS \"Inhalte\",\n    round(((((((2.6779094 + (4.728982 * (((tpop.x - 600000))::numeric / (1000000)::numeric))) + ((0.791484 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) + (((0.1306 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0436 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Laengengrad\",\n    round((((((((16.9023892 + (3.238272 * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - ((0.270978 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric))) - ((0.002528 * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0447 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.014 * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Breitengrad\",\n    concat('http://www.apflora.ch/Projekte/4635372c-431c-11e8-bb30-e77f6cdd35a6/Aktionspläne/', ap.id, '/Populationen/', pop.id, '/Teil-Populationen/', tpop.id) AS url\n   FROM ((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.y IS NOT NULL) AND (tpop.y IS NOT NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_tpop_kmlnamen AS\n SELECT ae_eigenschaften.artname AS \"Art\",\n    concat(ae_eigenschaften.artname, ' ', pop.nr, '/', tpop.nr) AS \"Label\",\n    \"substring\"(concat('Population: ', pop.nr, ' ', pop.name, '<br /> Teilpopulation: ', tpop.nr, ' ', tpop.gemeinde, ' ', tpop.flurname), 1, 225) AS \"Inhalte\",\n    round(((((((2.6779094 + (4.728982 * (((tpop.x - 600000))::numeric / (1000000)::numeric))) + ((0.791484 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) + (((0.1306 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0436 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Laengengrad\",\n    round((((((((16.9023892 + (3.238272 * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - ((0.270978 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric))) - ((0.002528 * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.0447 * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.x - 600000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) - (((0.014 * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric)) * (((tpop.y - 200000))::numeric / (1000000)::numeric))) * (100)::numeric) / (36)::numeric), 10) AS \"Breitengrad\",\n    concat('http://www.apflora.ch/Projekte/4635372c-431c-11e8-bb30-e77f6cdd35a6/Aktionspläne/', ap.id, '/Populationen/', pop.id, '/Teil-Populationen/', tpop.id) AS url\n   FROM ((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.y IS NOT NULL) AND (tpop.y IS NOT NULL))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_tpop_kontrjahrundberjahrundmassnjahr AS\n SELECT tpop.id,\n    tpopber.jahr AS \"Jahr\"\n   FROM (apflora.tpop\n     JOIN apflora.tpopber ON ((tpop.id = tpopber.tpop_id)))\nUNION\n SELECT tpop.id,\n    tpopmassnber.jahr AS \"Jahr\"\n   FROM (apflora.tpop\n     JOIN apflora.tpopmassnber ON ((tpop.id = tpopmassnber.tpop_id)))\nUNION\n SELECT tpop.id,\n    tpopkontr.jahr AS \"Jahr\"\n   FROM (apflora.tpop\n     JOIN apflora.tpopkontr ON ((tpop.id = tpopkontr.tpop_id)))\n  ORDER BY 2;\n\n\n\n\nCREATE VIEW apflora.v_tpop_mitapaberohnestatus AS\n SELECT ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    tpop.nr,\n    tpop.flurname,\n    tpop.status\n   FROM ((apflora.ap_bearbstand_werte\n     JOIN (apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id))) ON ((ap_bearbstand_werte.code = ap.bearbeitung)))\n     JOIN ((apflora.pop\n     JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.status IS NULL) AND (ap.bearbeitung = 3))\n  ORDER BY ae_eigenschaften.artname, pop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_ohneapberichtrelevant AS\n SELECT ae_eigenschaften.artname AS \"Artname\",\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.id,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    tpop.apber_relevant\n   FROM (apflora.ae_eigenschaften\n     JOIN (apflora.ap\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((tpop.pop_id = pop.id))) ON ((pop.ap_id = ap.id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE (tpop.apber_relevant IS NULL)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpop_ohnebekanntseit AS\n SELECT ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    tpop.bekannt_seit\n   FROM (((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE ((tpop.bekannt_seit IS NULL) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_tpop_ohnekoord AS\n SELECT ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    tpop.x,\n    tpop.y\n   FROM (((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  WHERE (((tpop.x IS NULL) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))) OR ((tpop.y IS NULL) AND ((ap.bearbeitung >= 1) AND (ap.bearbeitung <= 3))))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_tpop_popberundmassnber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    \"domPopHerkunft_1\".text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopber.id AS tpopber_id,\n    tpopber.jahr AS tpopber_jahr,\n    tpop_entwicklung_werte.text AS tpopber_entwicklung,\n    tpopber.bemerkungen AS tpopber_bemerkungen,\n    tpopber.changed AS tpopber_changed,\n    tpopber.changed_by AS tpopber_changed_by,\n    tpopmassnber.id AS tpopmassnber_id,\n    tpopmassnber.jahr AS tpopmassnber_jahr,\n    tpopmassn_erfbeurt_werte.text AS tpopmassnber_entwicklung,\n    tpopmassnber.bemerkungen AS tpopmassnber_bemerkungen,\n    tpopmassnber.changed AS tpopmassnber_changed,\n    tpopmassnber.changed_by AS tpopmassnber_changed_by\n   FROM (((((((((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     RIGHT JOIN (apflora.pop\n     RIGHT JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte \"domPopHerkunft_1\" ON ((tpop.status = \"domPopHerkunft_1\".code)))\n     LEFT JOIN apflora.v_tpop_berjahrundmassnjahr ON ((tpop.id = v_tpop_berjahrundmassnjahr.id)))\n     LEFT JOIN apflora.tpopmassnber ON (((v_tpop_berjahrundmassnjahr.id = tpopmassnber.tpop_id) AND (v_tpop_berjahrundmassnjahr.jahr = tpopmassnber.jahr))))\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((tpopmassnber.beurteilung = tpopmassn_erfbeurt_werte.code)))\n     LEFT JOIN apflora.tpopber ON (((v_tpop_berjahrundmassnjahr.jahr = tpopber.jahr) AND (v_tpop_berjahrundmassnjahr.id = tpopber.tpop_id))))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopber.entwicklung = tpop_entwicklung_werte.code)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, v_tpop_berjahrundmassnjahr.jahr;\n\n\n\n\nCREATE VIEW apflora.v_tpopber_letzterber AS\n SELECT tpopber.tpop_id,\n    max(tpopber.jahr) AS jahr\n   FROM apflora.tpopber\n  GROUP BY tpopber.tpop_id;\n\n\n\n\nCREATE VIEW apflora.v_tpop_statuswidersprichtbericht AS\n SELECT ae_eigenschaften.artname AS \"Art\",\n    ap_bearbstand_werte.text AS \"Bearbeitungsstand AP\",\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.nr,\n    tpop.gemeinde,\n    tpop.flurname,\n    tpop.status,\n    tpopber.entwicklung AS \"TPopBerEntwicklung\",\n    tpopber.jahr AS tpopber_jahr\n   FROM (((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopber\n     JOIN apflora.v_tpopber_letzterber ON (((tpopber.tpop_id = v_tpopber_letzterber.tpop_id) AND (tpopber.jahr = v_tpopber_letzterber.jahr)))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n  WHERE (((ap.bearbeitung < 4) AND ((tpop.status = 101) OR (tpop.status = 202)) AND (tpopber.entwicklung <> 8)) OR ((ap.bearbeitung < 4) AND (tpop.status <> ALL (ARRAY[101, 202])) AND (tpopber.entwicklung = 8)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, pop.name, tpop.nr, tpop.gemeinde, tpop.flurname;\n\n\n\n\nCREATE VIEW apflora.v_tpop_webgisbun AS\n SELECT ap.id AS \"APARTID\",\n    ae_eigenschaften.artname AS \"APART\",\n    ap_bearbstand_werte.text AS \"APSTATUS\",\n    ap.start_jahr AS \"APSTARTJAHR\",\n    ap_umsetzung_werte.text AS \"APSTANDUMSETZUNG\",\n    pop.id AS \"POPGUID\",\n    pop.nr AS \"POPNR\",\n    pop.name AS \"POPNAME\",\n    pop_status_werte.text AS \"POPSTATUS\",\n    pop.status_unklar AS \"POPSTATUSUNKLAR\",\n    pop.status_unklar_begruendung AS \"POPUNKLARGRUND\",\n    pop.bekannt_seit AS \"POPBEKANNTSEIT\",\n    pop.x AS \"POP_X\",\n    pop.y AS \"POP_Y\",\n    tpop.id AS \"TPOPID\",\n    tpop.id AS \"TPOPGUID\",\n    tpop.nr AS \"TPOPNR\",\n    tpop.gemeinde AS \"TPOPGEMEINDE\",\n    tpop.flurname AS \"TPOPFLURNAME\",\n    pop_status_werte_2.text AS \"TPOPSTATUS\",\n    tpop.status_unklar AS \"TPOPSTATUSUNKLAR\",\n    tpop.status_unklar_grund AS \"TPOPUNKLARGRUND\",\n    tpop.x AS \"TPOP_X\",\n    tpop.y AS \"TPOP_Y\",\n    tpop.radius AS \"TPOPRADIUS\",\n    tpop.hoehe AS \"TPOPHOEHE\",\n    tpop.exposition AS \"TPOPEXPOSITION\",\n    tpop.klima AS \"TPOPKLIMA\",\n    tpop.neigung AS \"TPOPHANGNEIGUNG\",\n    tpop.beschreibung AS \"TPOPBESCHREIBUNG\",\n    tpop.kataster_nr AS \"TPOPKATASTERNR\",\n    adresse.name AS \"TPOPVERANTWORTLICH\",\n    tpop.apber_relevant AS \"TPOPBERICHTSRELEVANZ\",\n    tpop.bekannt_seit AS \"TPOPBEKANNTSEIT\",\n    tpop.eigentuemer AS \"TPOPEIGENTUEMERIN\",\n    tpop.kontakt AS \"TPOPKONTAKT_VO\",\n    tpop.nutzungszone AS \"TPOP_NUTZUNGSZONE\",\n    tpop.bewirtschafter AS \"TPOPBEWIRTSCHAFTER\",\n    tpop.bewirtschaftung AS \"TPOPBEWIRTSCHAFTUNG\",\n    to_char((tpop.changed)::timestamp with time zone, 'DD.MM.YY'::text) AS \"TPOPCHANGEDAT\",\n    tpop.changed_by AS \"TPOPCHANGEBY\"\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpopber AS\n SELECT ap.id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpop_status_werte.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopber.id AS tpopber_id,\n    tpopber.jahr AS tpopber_jahr,\n    tpop_entwicklung_werte.text AS tpopber_entwicklung,\n    tpopber.bemerkungen AS tpopber_bemerkungen,\n    tpopber.changed AS tpopber_changed,\n    tpopber.changed_by AS tpopber_changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte tpop_status_werte ON ((tpop.status = tpop_status_werte.code)))\n     RIGHT JOIN (apflora.tpopber\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopber.entwicklung = tpop_entwicklung_werte.code))) ON ((tpop.id = tpopber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopber.jahr, tpop_entwicklung_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_fuergis_read AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS apherkunft,\n    ap.start_jahr AS apjahr,\n    ap_umsetzung_werte.text AS apumsetzung,\n    (pop.id)::character varying(50) AS popid,\n    pop.nr AS popnr,\n    pop.name AS popname,\n    pop_status_werte.text AS popherkunft,\n    pop.bekannt_seit AS popbekanntseit,\n    (tpop.id)::character varying(50) AS tpopid,\n    tpop.nr AS tpopnr,\n    tpop.gemeinde AS tpopgemeinde,\n    tpop.flurname AS tpopflurname,\n    tpop.x AS tpopxkoord,\n    tpop.y AS tpopykoord,\n    tpop.bekannt_seit AS tpopbekanntseit,\n    (tpopkontr.id)::character varying(50) AS tpopkontrid,\n    tpopkontr.jahr AS tpopkontrjahr,\n    (tpopkontr.datum)::timestamp without time zone AS tpopkontrdatum,\n    tpopkontr_typ_werte.text AS tpopkontrtyp,\n    adresse.name AS tpopkontrbearb,\n    tpopkontr.ueberlebensrate AS tpopkontrueberleb,\n    tpopkontr.vitalitaet AS tpopkontrvitalitaet,\n    tpop_entwicklung_werte.text AS tpopkontrentwicklung,\n    tpopkontr.ursachen AS tpopkontrursach,\n    tpopkontr.erfolgsbeurteilung AS tpopkontrurteil,\n    tpopkontr.umsetzung_aendern AS tpopkontraendums,\n    tpopkontr.kontrolle_aendern AS tpopkontraendkontr,\n    tpopkontr.lr_delarze AS tpopkontrleb,\n    tpopkontr.flaeche AS tpopkontrflaeche,\n    tpopkontr.lr_umgebung_delarze AS tpopkontrlebumg,\n    tpopkontr.vegetationstyp AS tpopkontrvegtyp,\n    tpopkontr.konkurrenz AS tpopkontrkonkurrenz,\n    tpopkontr.moosschicht AS tpopkontrmoosschicht,\n    tpopkontr.krautschicht AS tpopkontrkrautschicht,\n    tpopkontr.strauchschicht AS tpopkontrstrauchschicht,\n    tpopkontr.baumschicht AS tpopkontrbaumschicht,\n    tpopkontr.boden_typ AS tpopkontrbodentyp,\n    tpopkontr.boden_kalkgehalt AS tpopkontrbodenkalkgehalt,\n    tpopkontr.boden_durchlaessigkeit AS tpopkontrbodendurchlaessigkeit,\n    tpopkontr.boden_humus AS tpopkontrbodenhumus,\n    tpopkontr.boden_naehrstoffgehalt AS tpopkontrbodennaehrstoffgehalt,\n    tpopkontr.boden_abtrag AS tpopkontrbodenabtrag,\n    tpopkontr.wasserhaushalt AS tpopkontrwasserhaushalt,\n    tpopkontr_idbiotuebereinst_werte.text AS tpopkontridealbiotopuebereinst,\n    tpopkontr.flaeche_ueberprueft AS tpopkontruebflaeche,\n    tpopkontr.plan_vorhanden AS tpopkontrplan,\n    tpopkontr.deckung_vegetation AS tpopkontrveg,\n    tpopkontr.deckung_nackter_boden AS tpopkontrnabo,\n    tpopkontr.deckung_ap_art AS tpopkontruebpfl,\n    tpopkontr.jungpflanzen_vorhanden AS tpopkontrjungpfljn,\n    tpopkontr.vegetationshoehe_maximum AS tpopkontrveghoemax,\n    tpopkontr.vegetationshoehe_mittel AS tpopkontrveghoemit,\n    tpopkontr.gefaehrdung AS tpopkontrgefaehrdung,\n    (tpopkontr.changed)::timestamp without time zone AS mutwann,\n    tpopkontr.changed_by AS mutwer\n   FROM ((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (((apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontr_typ_werte ON (((tpopkontr.typ)::text = (tpopkontr_typ_werte.text)::text)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopkontr.entwicklung = tpop_entwicklung_werte.code))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.tpopkontr_idbiotuebereinst_werte ON ((tpopkontr.idealbiotop_uebereinstimmung = tpopkontr_idbiotuebereinst_werte.code)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopkontr.jahr, tpopkontr.datum;\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_fuergis_write AS\n SELECT (tpopkontr.id)::text AS id,\n    tpopkontr.typ,\n    tpopkontr.jahr,\n    (tpopkontr.datum)::timestamp without time zone AS datum,\n    tpopkontr.bearbeiter,\n    tpopkontr.jungpflanzen_anzahl,\n    tpopkontr.ueberlebensrate,\n    tpopkontr.entwicklung,\n    tpopkontr.vitalitaet,\n    tpopkontr.ursachen,\n    tpopkontr.erfolgsbeurteilung,\n    tpopkontr.umsetzung_aendern,\n    tpopkontr.kontrolle_aendern,\n    tpopkontr.lr_delarze,\n    tpopkontr.flaeche,\n    tpopkontr.lr_umgebung_delarze,\n    tpopkontr.vegetationstyp,\n    tpopkontr.konkurrenz,\n    tpopkontr.moosschicht,\n    tpopkontr.krautschicht,\n    tpopkontr.strauchschicht,\n    tpopkontr.baumschicht,\n    tpopkontr.boden_typ,\n    tpopkontr.boden_kalkgehalt,\n    tpopkontr.boden_durchlaessigkeit,\n    tpopkontr.boden_humus,\n    tpopkontr.boden_naehrstoffgehalt,\n    tpopkontr.boden_abtrag,\n    tpopkontr.wasserhaushalt,\n    tpopkontr.idealbiotop_uebereinstimmung,\n    tpopkontr.flaeche_ueberprueft,\n    tpopkontr.plan_vorhanden,\n    tpopkontr.deckung_vegetation,\n    tpopkontr.deckung_nackter_boden,\n    tpopkontr.deckung_ap_art,\n    tpopkontr.jungpflanzen_vorhanden,\n    tpopkontr.vegetationshoehe_maximum,\n    tpopkontr.vegetationshoehe_mittel,\n    tpopkontr.gefaehrdung,\n    tpopkontr.bemerkungen,\n    (tpopkontr.changed)::timestamp without time zone AS changed,\n    tpopkontr.changed_by\n   FROM apflora.tpopkontr;\n\n\n\n\nCREATE VIEW apflora.v_tpopkontr_webgisbun AS\n SELECT ap.id AS \"APARTID\",\n    ae_eigenschaften.artname AS \"APART\",\n    pop.id AS \"POPGUID\",\n    pop.nr AS \"POPNR\",\n    tpop.id AS \"TPOPGUID\",\n    tpop.nr AS \"TPOPNR\",\n    tpopkontr.id AS \"KONTRGUID\",\n    tpopkontr.jahr AS \"KONTRJAHR\",\n    to_char((tpopkontr.datum)::timestamp with time zone, 'DD.MM.YY'::text) AS \"KONTRDAT\",\n    tpopkontr_typ_werte.text AS \"KONTRTYP\",\n    pop_status_werte_2.text AS \"TPOPSTATUS\",\n    adresse.name AS \"KONTRBEARBEITER\",\n    tpopkontr.ueberlebensrate AS \"KONTRUEBERLEBENSRATE\",\n    tpopkontr.vitalitaet AS \"KONTRVITALITAET\",\n    tpop_entwicklung_werte.text AS \"KONTRENTWICKLUNG\",\n    tpopkontr.ursachen AS \"KONTRURSACHEN\",\n    tpopkontr.erfolgsbeurteilung AS \"KONTRERFOLGBEURTEIL\",\n    tpopkontr.umsetzung_aendern AS \"KONTRAENDUMSETZUNG\",\n    tpopkontr.kontrolle_aendern AS \"KONTRAENDKONTROLLE\",\n    tpop.x AS \"KONTR_X\",\n    tpop.y AS \"KONTR_Y\",\n    tpopkontr.bemerkungen AS \"KONTRBEMERKUNGEN\",\n    tpopkontr.lr_delarze AS \"KONTRLRMDELARZE\",\n    tpopkontr.lr_umgebung_delarze AS \"KONTRDELARZEANGRENZ\",\n    tpopkontr.vegetationstyp AS \"KONTRVEGTYP\",\n    tpopkontr.konkurrenz AS \"KONTRKONKURRENZ\",\n    tpopkontr.moosschicht AS \"KONTRMOOSE\",\n    tpopkontr.krautschicht AS \"KONTRKRAUTSCHICHT\",\n    tpopkontr.strauchschicht AS \"KONTRSTRAUCHSCHICHT\",\n    tpopkontr.baumschicht AS \"KONTRBAUMSCHICHT\",\n    tpopkontr.boden_typ AS \"KONTRBODENTYP\",\n    tpopkontr.boden_kalkgehalt AS \"KONTRBODENKALK\",\n    tpopkontr.boden_durchlaessigkeit AS \"KONTRBODENDURCHLAESSIGK\",\n    tpopkontr.boden_humus AS \"KONTRBODENHUMUS\",\n    tpopkontr.boden_naehrstoffgehalt AS \"KONTRBODENNAEHRSTOFF\",\n    tpopkontr.boden_abtrag AS \"KONTROBERBODENABTRAG\",\n    tpopkontr.wasserhaushalt AS \"KONTROBODENWASSERHAUSHALT\",\n    tpopkontr_idbiotuebereinst_werte.text AS \"KONTRUEBEREINSTIMMUNIDEAL\",\n    tpopkontr.handlungsbedarf AS \"KONTRHANDLUNGSBEDARF\",\n    tpopkontr.flaeche_ueberprueft AS \"KONTRUEBERPRUFTFLAECHE\",\n    tpopkontr.flaeche AS \"KONTRFLAECHETPOP\",\n    tpopkontr.plan_vorhanden AS \"KONTRAUFPLAN\",\n    tpopkontr.deckung_vegetation AS \"KONTRDECKUNGVEG\",\n    tpopkontr.deckung_nackter_boden AS \"KONTRDECKUNGBODEN\",\n    tpopkontr.deckung_ap_art AS \"KONTRDECKUNGART\",\n    tpopkontr.jungpflanzen_vorhanden AS \"KONTRJUNGEPLANZEN\",\n    tpopkontr.vegetationshoehe_maximum AS \"KONTRMAXHOEHEVEG\",\n    tpopkontr.vegetationshoehe_mittel AS \"KONTRMITTELHOEHEVEG\",\n    tpopkontr.gefaehrdung AS \"KONTRGEFAEHRDUNG\",\n    to_char((tpopkontr.changed)::timestamp with time zone, 'DD.MM.YY'::text) AS \"KONTRCHANGEDAT\",\n    tpopkontr.changed_by AS \"KONTRCHANGEBY\",\n    string_agg((tpopkontrzaehl_einheit_werte.text)::text, ', '::text) AS \"ZAEHLEINHEITEN\",\n    array_to_string(array_agg(tpopkontrzaehl.anzahl), ', '::text) AS \"ANZAHLEN\",\n    string_agg((tpopkontrzaehl_methode_werte.text)::text, ', '::text) AS \"METHODEN\"\n   FROM (((((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (apflora.pop\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     JOIN ((((((apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontr_typ_werte ON (((tpopkontr.typ)::text = (tpopkontr_typ_werte.text)::text)))\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     LEFT JOIN apflora.tpop_entwicklung_werte ON ((tpopkontr.entwicklung = tpop_entwicklung_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id)))\n     LEFT JOIN apflora.tpopkontrzaehl_einheit_werte ON ((tpopkontrzaehl.einheit = tpopkontrzaehl_einheit_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl_methode_werte ON ((tpopkontrzaehl.methode = tpopkontrzaehl_methode_werte.code))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     LEFT JOIN apflora.tpopkontr_idbiotuebereinst_werte ON ((tpopkontr.idealbiotop_uebereinstimmung = tpopkontr_idbiotuebereinst_werte.code)))\n     LEFT JOIN apflora.adresse apflora_adresse_1 ON ((ap.bearbeiter = apflora_adresse_1.id)))\n  WHERE (ae_eigenschaften.taxid > 150)\n  GROUP BY ap.id, ae_eigenschaften.artname, pop.id, pop.nr, tpop.id, tpop.nr, tpopkontr.tpop_id, tpopkontr.id, tpopkontr.jahr, tpopkontr.datum, tpopkontr_typ_werte.text, pop_status_werte_2.text, adresse.name, tpopkontr.ueberlebensrate, tpopkontr.vitalitaet, tpop_entwicklung_werte.text, tpopkontr.ursachen, tpopkontr.erfolgsbeurteilung, tpopkontr.umsetzung_aendern, tpopkontr.kontrolle_aendern, tpop.x, tpop.y, tpopkontr.bemerkungen, tpopkontr.lr_delarze, tpopkontr.lr_umgebung_delarze, tpopkontr.vegetationstyp, tpopkontr.konkurrenz, tpopkontr.moosschicht, tpopkontr.krautschicht, tpopkontr.strauchschicht, tpopkontr.baumschicht, tpopkontr.boden_typ, tpopkontr.boden_kalkgehalt, tpopkontr.boden_durchlaessigkeit, tpopkontr.boden_humus, tpopkontr.boden_naehrstoffgehalt, tpopkontr.boden_abtrag, tpopkontr.wasserhaushalt, tpopkontr_idbiotuebereinst_werte.text, tpopkontr.handlungsbedarf, tpopkontr.flaeche_ueberprueft, tpopkontr.flaeche, tpopkontr.plan_vorhanden, tpopkontr.deckung_vegetation, tpopkontr.deckung_nackter_boden, tpopkontr.deckung_ap_art, tpopkontr.jungpflanzen_vorhanden, tpopkontr.vegetationshoehe_maximum, tpopkontr.vegetationshoehe_mittel, tpopkontr.gefaehrdung, tpopkontr.changed, tpopkontr.changed_by\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr;\n\n\n\n\nCREATE VIEW apflora.v_tpopkoord AS\n SELECT DISTINCT pop.ap_id,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    tpop.id,\n    tpop.nr,\n    tpop.x,\n    tpop.y,\n    tpop.apber_relevant\n   FROM (apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n  WHERE ((tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL));\n\n\n\n\nCREATE VIEW apflora.v_tpopmassn_0 AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname AS \"Art\",\n    ap_bearbstand_werte.text AS \"Aktionsplan Bearbeitungsstand\",\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.flurname AS tpop_flurname,\n    tpopmassn.id,\n    tpopmassn.jahr AS \"Jahr\",\n    tpopmassn_typ_werte.text AS \"Massnahme\",\n    tpopmassn.beschreibung,\n    tpopmassn.datum,\n    adresse.name AS bearbeiter,\n    tpopmassn.bemerkungen,\n    tpopmassn.plan_vorhanden,\n    tpopmassn.plan_bezeichnung,\n    tpopmassn.flaeche,\n    tpopmassn.markierung,\n    tpopmassn.anz_triebe,\n    tpopmassn.anz_pflanzen,\n    tpopmassn.anz_pflanzstellen,\n    tpopmassn.wirtspflanze,\n    tpopmassn.herkunft_pop,\n    tpopmassn.sammeldatum,\n    tpopmassn.form,\n    tpopmassn.pflanzanordnung\n   FROM (((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     JOIN ((apflora.pop\n     JOIN apflora.tpop ON ((pop.id = tpop.pop_id)))\n     JOIN ((apflora.tpopmassn\n     LEFT JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassn.jahr, tpopmassn_typ_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_tpopmassn_fueraktap0 AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname AS \"Art\",\n    ap_bearbstand_werte.text AS \"Aktionsplan-Status\",\n    ap.start_jahr AS \"Aktionsplan-Jahr\",\n    ap_umsetzung_werte.text AS \"Aktionsplan-Umsetzung\",\n    pop.id AS pop_id,\n    pop.nr AS \"Population-Nr\",\n    pop.name AS \"Population-Name\",\n    pop_status_werte.text AS \"Population-Herkunft\",\n    pop.bekannt_seit AS \"Population - bekannt seit\",\n    tpop.id AS tpop_id,\n    tpop.nr AS \"Teilpopulation-Nr\",\n    tpop.gemeinde AS \"Teilpopulation-Gemeinde\",\n    tpop.flurname AS \"Teilpopulation-Flurname\",\n    tpop.x AS \"Teilpopulation-X-Koodinate\",\n    tpop.y AS \"Teilpopulation-Y-Koordinate\",\n    tpop.radius AS \"Teilpopulation-Radius\",\n    tpop.hoehe AS \"Teilpopulation-Höhe\",\n    tpop.beschreibung AS \"Teilpopulation-Beschreibung\",\n    tpop.kataster_nr AS \"Teilpopulation-Kataster-Nr\",\n    pop_status_werte_2.text AS \"Teilpopulation-Herkunft\",\n    tpop.status_unklar AS \"Teilpopulation - Herkunft unklar\",\n    tpop.status_unklar_grund AS \"Teilpopulation - Herkunft unklar Begruendung\",\n    tpop_apberrelevant_werte.text AS \"Teilpopulation - Fuer Bericht relevant\",\n    tpop.bekannt_seit AS \"Teilpopulation - bekannt seit\",\n    tpop.eigentuemer AS \"Teilpopulation-Eigentuemer\",\n    tpop.kontakt AS \"Teilpopulation-Kontakt\",\n    tpop.nutzungszone AS \"Teilpopulation-Nutzungszone\",\n    tpop.bewirtschafter AS \"Teilpopulation-Bewirtschafter\",\n    tpop.bewirtschaftung AS \"Teilpopulation-Bewirtschaftung\",\n    tpop.bemerkungen AS \"Teilpopulation-Bemerkungen\",\n    tpopmassn.id,\n    tpopmassn_typ_werte.text AS \"Massnahme-Typ\",\n    tpopmassn.beschreibung AS \"Massnahme-Beschreibung\",\n    tpopmassn.datum AS \"Massnahme-Datum\",\n    adresse.name AS \"Massnahme-BearbeiterIn\",\n    tpopmassn.bemerkungen AS \"Massnahme-Bemerkungen\",\n    tpopmassn.plan_vorhanden AS \"Massnahme-Plan\",\n    tpopmassn.plan_bezeichnung AS \"Massnahme-Planbezeichnung\",\n    tpopmassn.flaeche AS \"Massnahme-Flaeche\",\n    tpopmassn.markierung AS \"Massnahme-Markierung\",\n    tpopmassn.anz_triebe AS \"Massnahme - Ansiedlung Anzahl Triebe\",\n    tpopmassn.anz_pflanzen AS \"Massnahme - Ansiedlung Anzahl Pflanzen\",\n    tpopmassn.anz_pflanzstellen AS \"Massnahme - Ansiedlung Anzahl Pflanzstellen\",\n    tpopmassn.wirtspflanze AS \"Massnahme - Ansiedlung Wirtspflanzen\",\n    tpopmassn.herkunft_pop AS \"Massnahme - Ansiedlung Herkunftspopulation\",\n    tpopmassn.sammeldatum AS \"Massnahme - Ansiedlung Sammeldatum\",\n    tpopmassn.form AS \"Massnahme - Ansiedlung Form\",\n    tpopmassn.pflanzanordnung AS \"Massnahme - Ansiedlung Pflanzordnung\"\n   FROM ((apflora.ae_eigenschaften\n     JOIN ((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code))) ON ((ae_eigenschaften.id = ap.art_id)))\n     JOIN (((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.tpop_apberrelevant_werte ON ((tpop.apber_relevant = tpop_apberrelevant_werte.code))) ON ((pop.id = tpop.pop_id)))\n     JOIN ((apflora.tpopmassn\n     LEFT JOIN apflora.tpopmassn_typ_werte ON ((tpopmassn.typ = tpopmassn_typ_werte.code)))\n     LEFT JOIN apflora.adresse ON ((tpopmassn.bearbeiter = adresse.id))) ON ((tpop.id = tpopmassn.tpop_id))) ON ((ap.id = pop.ap_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassn_typ_werte.text;\n\n\n\n\nCREATE VIEW apflora.v_tpopmassnber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    pop.id AS pop_id,\n    pop.nr AS pop_nr,\n    pop.name AS pop_name,\n    pop_status_werte.text AS pop_status,\n    pop.bekannt_seit AS pop_bekannt_seit,\n    pop.status_unklar AS pop_status_unklar,\n    pop.status_unklar_begruendung AS pop_status_unklar_begruendung,\n    pop.x AS pop_x,\n    pop.y AS pop_y,\n    tpop.id AS tpop_id,\n    tpop.nr AS tpop_nr,\n    tpop.gemeinde AS tpop_gemeinde,\n    tpop.flurname AS tpop_flurname,\n    tpop_status_werte.text AS tpop_status,\n    tpop.bekannt_seit AS tpop_bekannt_seit,\n    tpop.status_unklar AS tpop_status_unklar,\n    tpop.status_unklar_grund AS tpop_status_unklar_grund,\n    tpop.x AS tpop_x,\n    tpop.y AS tpop_y,\n    tpop.radius AS tpop_radius,\n    tpop.hoehe AS tpop_hoehe,\n    tpop.exposition AS tpop_exposition,\n    tpop.klima AS tpop_klima,\n    tpop.neigung AS tpop_neigung,\n    tpop.beschreibung AS tpop_beschreibung,\n    tpop.kataster_nr AS tpop_kataster_nr,\n    tpop.apber_relevant AS tpop_apber_relevant,\n    tpop.eigentuemer AS tpop_eigentuemer,\n    tpop.kontakt AS tpop_kontakt,\n    tpop.nutzungszone AS tpop_nutzungszone,\n    tpop.bewirtschafter AS tpop_bewirtschafter,\n    tpop.bewirtschaftung AS tpop_bewirtschaftung,\n    tpopmassnber.id,\n    tpopmassnber.jahr,\n    tpopmassn_erfbeurt_werte.text AS entwicklung,\n    tpopmassnber.bemerkungen,\n    tpopmassnber.changed,\n    tpopmassnber.changed_by\n   FROM (apflora.ae_eigenschaften\n     JOIN (((apflora.ap\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN ((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte tpop_status_werte ON ((tpop.status = tpop_status_werte.code)))\n     JOIN (apflora.tpopmassnber\n     LEFT JOIN apflora.tpopmassn_erfbeurt_werte ON ((tpopmassnber.beurteilung = tpopmassn_erfbeurt_werte.code))) ON ((tpop.id = tpopmassnber.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id))) ON ((ae_eigenschaften.id = ap.art_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_tpopmassnber_fueraktap0 AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname AS \"Art\",\n    ap_bearbstand_werte.text AS \"Aktionsplan-Status\",\n    ap.start_jahr AS \"Aktionsplan-Jahr\",\n    ap_umsetzung_werte.text AS \"Aktionsplan-Umsetzung\",\n    pop.nr AS \"Population-Nr\",\n    pop.name AS \"Population-Name\",\n    pop_status_werte.text AS \"Population-Herkunft\",\n    pop.bekannt_seit AS \"Population - bekannt seit\",\n    tpop.nr AS \"Teilpopulation-Nr\",\n    tpop.gemeinde AS \"Teilpopulation-Gemeinde\",\n    tpop.flurname AS \"Teilpopulation-Flurname\",\n    tpop.x AS \"Teilpopulation-X-Koodinate\",\n    tpop.y AS \"Teilpopulation-Y-Koordinate\",\n    tpop.radius AS \"Teilpopulation-Radius\",\n    tpop.hoehe AS \"Teilpopulation-Hoehe\",\n    tpop.beschreibung AS \"Teilpopulation-Beschreibung\",\n    tpop.kataster_nr AS \"Teilpopulation-Kataster-Nr\",\n    pop_status_werte_2.text AS \"Teilpopulation-Herkunft\",\n    tpop.status_unklar AS \"Teilpopulation - Herkunft unklar\",\n    tpop.status_unklar_grund AS \"Teilpopulation - Herkunft unklar Begruendung\",\n    tpop_apberrelevant_werte.text AS \"Teilpopulation - Fuer Bericht relevant\",\n    tpop.bekannt_seit AS \"Teilpopulation - bekannt seit\",\n    tpop.eigentuemer AS \"Teilpopulation-Eigentuemer\",\n    tpop.kontakt AS \"Teilpopulation-Kontakt\",\n    tpop.nutzungszone AS \"Teilpopulation-Nutzungszone\",\n    tpop.bewirtschafter AS \"Teilpopulation-Bewirtschafter\",\n    tpop.bewirtschaftung AS \"Teilpopulation-Bewirtschaftung\",\n    tpop.bemerkungen AS \"Teilpopulation-Bemerkungen\",\n    tpopmassnber.jahr AS \"Massnahmenbericht-Jahr\",\n    tpopmassn_erfbeurt_werte.text AS \"Massnahmenbericht-Erfolgsberuteilung\",\n    tpopmassnber.bemerkungen AS \"Massnahmenbericht-Interpretation\"\n   FROM ((((apflora.ae_eigenschaften\n     JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     JOIN (((apflora.pop\n     LEFT JOIN apflora.pop_status_werte ON ((pop.status = pop_status_werte.code)))\n     JOIN ((apflora.tpop\n     LEFT JOIN apflora.pop_status_werte pop_status_werte_2 ON ((tpop.status = pop_status_werte_2.code)))\n     LEFT JOIN apflora.tpop_apberrelevant_werte ON ((tpop.apber_relevant = tpop_apberrelevant_werte.code))) ON ((pop.id = tpop.pop_id)))\n     JOIN (apflora.tpopmassnber\n     JOIN apflora.tpopmassn_erfbeurt_werte ON ((tpopmassnber.beurteilung = tpopmassn_erfbeurt_werte.code))) ON ((tpop.id = tpopmassnber.tpop_id))) ON ((ap.id = pop.ap_id)))\n  ORDER BY ae_eigenschaften.artname, pop.nr, tpop.nr, tpopmassnber.jahr;\n\n\n\n\nCREATE VIEW apflora.v_ziel AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    ziel.id,\n    ziel.jahr,\n    ziel_typ_werte.text AS typ,\n    ziel.bezeichnung\n   FROM ((((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     RIGHT JOIN apflora.ziel ON ((ap.id = ziel.ap_id)))\n     LEFT JOIN apflora.ziel_typ_werte ON ((ziel.typ = ziel_typ_werte.code)))\n  WHERE (ziel.typ = ANY (ARRAY[1, 2, 1170775556]))\n  ORDER BY ae_eigenschaften.artname, ziel.jahr, ziel_typ_werte.text, ziel.typ;\n\n\n\n\nCREATE VIEW apflora.v_zielber AS\n SELECT ap.id AS ap_id,\n    ae_eigenschaften.artname,\n    ap_bearbstand_werte.text AS ap_bearbeitung,\n    ap.start_jahr AS ap_start_jahr,\n    ap_umsetzung_werte.text AS ap_umsetzung,\n    adresse.name AS ap_bearbeiter,\n    ziel.id AS ziel_id,\n    ziel.jahr AS ziel_jahr,\n    ziel_typ_werte.text AS ziel_typ,\n    ziel.bezeichnung AS ziel_bezeichnung,\n    zielber.id,\n    zielber.jahr,\n    zielber.erreichung,\n    zielber.bemerkungen,\n    zielber.changed,\n    zielber.changed_by\n   FROM (((((((apflora.ae_eigenschaften\n     RIGHT JOIN apflora.ap ON ((ae_eigenschaften.id = ap.art_id)))\n     LEFT JOIN apflora.ap_bearbstand_werte ON ((ap.bearbeitung = ap_bearbstand_werte.code)))\n     LEFT JOIN apflora.ap_umsetzung_werte ON ((ap.umsetzung = ap_umsetzung_werte.code)))\n     LEFT JOIN apflora.adresse ON ((ap.bearbeiter = adresse.id)))\n     RIGHT JOIN apflora.ziel ON ((ap.id = ziel.ap_id)))\n     LEFT JOIN apflora.ziel_typ_werte ON ((ziel.typ = ziel_typ_werte.code)))\n     RIGHT JOIN apflora.zielber ON ((ziel.id = zielber.ziel_id)))\n  ORDER BY ae_eigenschaften.artname, ziel.jahr, ziel_typ_werte.text, ziel.typ, zielber.jahr;\n\n\n\n\nCREATE SEQUENCE apflora.\"ziel_ZielId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"ziel_ZielId_seq\" OWNED BY apflora.ziel.id_old;\n\n\n\nCREATE SEQUENCE apflora.\"zielber_ZielBerId_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n\n\nALTER SEQUENCE apflora.\"zielber_ZielBerId_seq\" OWNED BY apflora.zielber.id_old;\n\n\n\nCREATE TABLE public.migrations (\n    id integer NOT NULL,\n    name character varying(100) NOT NULL,\n    hash character varying(40) NOT NULL,\n    executed_at timestamp without time zone DEFAULT now()\n);\n\n\n\n\nALTER TABLE ONLY apflora._variable ALTER COLUMN \"KonstId\" SET DEFAULT nextval('apflora.\"_variable_KonstId_seq\"'::regclass);\n\n\n\nALTER TABLE ONLY apflora._variable\n    ADD CONSTRAINT _variable_pkey PRIMARY KEY (\"KonstId\");\n\n\n\nALTER TABLE ONLY apflora.ae_eigenschaften\n    ADD CONSTRAINT \"adb_eigenschaften_TaxonomieId_key\" UNIQUE (taxid);\n\n\n\nALTER TABLE ONLY apflora.ae_eigenschaften\n    ADD CONSTRAINT adb_eigenschaften_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ae_lrdelarze\n    ADD CONSTRAINT adb_lr_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.adresse\n    ADD CONSTRAINT adresse_id_key UNIQUE (id);\n\n\n\nALTER TABLE ONLY apflora.adresse\n    ADD CONSTRAINT adresse_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_art_key UNIQUE (art_id);\n\n\n\nALTER TABLE ONLY apflora.ap_bearbstand_werte\n    ADD CONSTRAINT ap_bearbstand_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.ap_bearbstand_werte\n    ADD CONSTRAINT ap_bearbstand_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ap_erfbeurtkrit_werte\n    ADD CONSTRAINT ap_erfbeurtkrit_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.ap_erfbeurtkrit_werte\n    ADD CONSTRAINT ap_erfbeurtkrit_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ap_erfkrit_werte\n    ADD CONSTRAINT ap_erfkrit_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.ap_erfkrit_werte\n    ADD CONSTRAINT ap_erfkrit_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ap_umsetzung_werte\n    ADD CONSTRAINT ap_umsetzung_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.ap_umsetzung_werte\n    ADD CONSTRAINT ap_umsetzung_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.apber\n    ADD CONSTRAINT apber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.apberuebersicht\n    ADD CONSTRAINT apberuebersicht_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.assozart\n    ADD CONSTRAINT assozart_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.beob\n    ADD CONSTRAINT beob_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.beob_quelle_werte\n    ADD CONSTRAINT beob_quelle_werte_id_key UNIQUE (id);\n\n\n\nALTER TABLE ONLY apflora.beob_quelle_werte\n    ADD CONSTRAINT beob_quelle_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.apart\n    ADD CONSTRAINT beobart_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ber\n    ADD CONSTRAINT ber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.erfkrit\n    ADD CONSTRAINT erfkrit_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.evab_typologie\n    ADD CONSTRAINT evab_typologie_pkey PRIMARY KEY (\"TYPO\");\n\n\n\nALTER TABLE ONLY apflora.gemeinde\n    ADD CONSTRAINT gemeinde_id_key UNIQUE (id);\n\n\n\nALTER TABLE ONLY apflora.gemeinde\n    ADD CONSTRAINT gemeinde_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.idealbiotop\n    ADD CONSTRAINT idealbiotop_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.message\n    ADD CONSTRAINT message_id_key UNIQUE (id);\n\n\n\nALTER TABLE ONLY apflora.message\n    ADD CONSTRAINT message_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.pop\n    ADD CONSTRAINT pop_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.pop_status_werte\n    ADD CONSTRAINT pop_status_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.pop_status_werte\n    ADD CONSTRAINT pop_status_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.popber\n    ADD CONSTRAINT popber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.popmassnber\n    ADD CONSTRAINT popmassnber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.projekt\n    ADD CONSTRAINT projekt_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpop_apberrelevant_werte\n    ADD CONSTRAINT tpop_apberrelevant_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpop_apberrelevant_werte\n    ADD CONSTRAINT tpop_apberrelevant_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpop_entwicklung_werte\n    ADD CONSTRAINT tpop_entwicklung_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpop_entwicklung_werte\n    ADD CONSTRAINT tpop_entwicklung_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpop\n    ADD CONSTRAINT tpop_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopber\n    ADD CONSTRAINT tpopber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr_idbiotuebereinst_werte\n    ADD CONSTRAINT tpopkontr_idbiotuebereinst_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr_idbiotuebereinst_werte\n    ADD CONSTRAINT tpopkontr_idbiotuebereinst_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr_typ_werte\n    ADD CONSTRAINT tpopkontr_typ_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr_typ_werte\n    ADD CONSTRAINT tpopkontr_typ_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontr_typ_werte\n    ADD CONSTRAINT tpopkontr_typ_werte_text_key UNIQUE (text);\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl_einheit_werte\n    ADD CONSTRAINT tpopkontrzaehl_einheit_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl_einheit_werte\n    ADD CONSTRAINT tpopkontrzaehl_einheit_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl_methode_werte\n    ADD CONSTRAINT tpopkontrzaehl_methode_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl_methode_werte\n    ADD CONSTRAINT tpopkontrzaehl_methode_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl\n    ADD CONSTRAINT tpopkontrzaehl_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopmassn_erfbeurt_werte\n    ADD CONSTRAINT tpopmassn_erfbeurt_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopmassn_erfbeurt_werte\n    ADD CONSTRAINT tpopmassn_erfbeurt_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopmassn\n    ADD CONSTRAINT tpopmassn_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopmassn_typ_werte\n    ADD CONSTRAINT tpopmassn_typ_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.tpopmassn_typ_werte\n    ADD CONSTRAINT tpopmassn_typ_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.tpopmassnber\n    ADD CONSTRAINT tpopmassnber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.\"user\"\n    ADD CONSTRAINT user_email_key UNIQUE (email);\n\n\n\nALTER TABLE ONLY apflora.\"user\"\n    ADD CONSTRAINT user_name_key UNIQUE (name);\n\n\n\nALTER TABLE ONLY apflora.\"user\"\n    ADD CONSTRAINT user_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.usermessage\n    ADD CONSTRAINT usermessage_id_key UNIQUE (id);\n\n\n\nALTER TABLE ONLY apflora.usermessage\n    ADD CONSTRAINT usermessage_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ziel\n    ADD CONSTRAINT ziel_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.ziel_typ_werte\n    ADD CONSTRAINT ziel_typ_werte_code_key UNIQUE (code);\n\n\n\nALTER TABLE ONLY apflora.ziel_typ_werte\n    ADD CONSTRAINT ziel_typ_werte_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY apflora.zielber\n    ADD CONSTRAINT zielber_pkey PRIMARY KEY (id);\n\n\n\nALTER TABLE ONLY public.migrations\n    ADD CONSTRAINT migrations_name_key UNIQUE (name);\n\n\n\nALTER TABLE ONLY public.migrations\n    ADD CONSTRAINT migrations_pkey PRIMARY KEY (id);\n\n\n\nCREATE INDEX \"_variable_ApArtId_idx\" ON apflora._variable USING btree (\"ApArtId\");\n\n\n\nCREATE INDEX \"_variable_JBerJahr_idx\" ON apflora._variable USING btree (apber_jahr);\n\n\n\nCREATE INDEX \"adb_eigenschaften_Artname_idx\" ON apflora.ae_eigenschaften USING btree (artname);\n\n\n\nCREATE INDEX \"adb_eigenschaften_TaxonomieId_idx\" ON apflora.ae_eigenschaften USING btree (taxid);\n\n\n\nCREATE INDEX adb_eigenschaften_id_idx ON apflora.ae_eigenschaften USING btree (id);\n\n\n\nCREATE INDEX \"adb_lr_Id_idx\" ON apflora.ae_lrdelarze USING btree (sort);\n\n\n\nCREATE INDEX \"adb_lr_Label_idx\" ON apflora.ae_lrdelarze USING btree (label);\n\n\n\nCREATE INDEX adb_lr_id_idx ON apflora.ae_lrdelarze USING btree (id);\n\n\n\nCREATE INDEX adresse_freiw_erfko_idx ON apflora.adresse USING btree (freiw_erfko);\n\n\n\nCREATE INDEX adresse_id_idx ON apflora.adresse USING btree (id);\n\n\n\nCREATE INDEX adresse_name_idx ON apflora.adresse USING btree (name);\n\n\n\nCREATE INDEX ap_art_id_idx ON apflora.ap USING btree (art_id);\n\n\n\nCREATE INDEX ap_bearbeiter_idx ON apflora.ap USING btree (bearbeiter);\n\n\n\nCREATE INDEX ap_bearbeitung_idx ON apflora.ap USING btree (bearbeitung);\n\n\n\nCREATE INDEX \"ap_bearbstand_werte_DomainCode_idx\" ON apflora.ap_bearbstand_werte USING btree (code);\n\n\n\nCREATE INDEX \"ap_bearbstand_werte_DomainOrd_idx\" ON apflora.ap_bearbstand_werte USING btree (sort);\n\n\n\nCREATE INDEX ap_bearbstand_werte_id_idx ON apflora.ap_bearbstand_werte USING btree (id);\n\n\n\nCREATE INDEX \"ap_erfbeurtkrit_werte_DomainCode_idx\" ON apflora.ap_erfbeurtkrit_werte USING btree (code);\n\n\n\nCREATE INDEX \"ap_erfbeurtkrit_werte_DomainOrd_idx\" ON apflora.ap_erfbeurtkrit_werte USING btree (sort);\n\n\n\nCREATE INDEX ap_erfbeurtkrit_werte_id_idx ON apflora.ap_erfbeurtkrit_werte USING btree (id);\n\n\n\nCREATE INDEX \"ap_erfkrit_werte_BeurteilId_idx\" ON apflora.ap_erfkrit_werte USING btree (code);\n\n\n\nCREATE INDEX \"ap_erfkrit_werte_BeurteilOrd_idx\" ON apflora.ap_erfkrit_werte USING btree (sort);\n\n\n\nCREATE INDEX ap_erfkrit_werte_id_idx ON apflora.ap_erfkrit_werte USING btree (id);\n\n\n\nCREATE INDEX ap_id_idx ON apflora.ap USING btree (id);\n\n\n\nCREATE INDEX ap_proj_id_idx ON apflora.ap USING btree (proj_id);\n\n\n\nCREATE INDEX ap_start_jahr_idx ON apflora.ap USING btree (start_jahr);\n\n\n\nCREATE INDEX ap_umsetzung_idx ON apflora.ap USING btree (umsetzung);\n\n\n\nCREATE INDEX \"ap_umsetzung_werte_DomainCode_idx\" ON apflora.ap_umsetzung_werte USING btree (code);\n\n\n\nCREATE INDEX \"ap_umsetzung_werte_DomainOrd_idx\" ON apflora.ap_umsetzung_werte USING btree (sort);\n\n\n\nCREATE INDEX ap_umsetzung_werte_id_idx ON apflora.ap_umsetzung_werte USING btree (id);\n\n\n\nCREATE INDEX apart_ap_id_idx ON apflora.apart USING btree (ap_id);\n\n\n\nCREATE INDEX apart_art_id_idx ON apflora.apart USING btree (art_id);\n\n\n\nCREATE INDEX apber_ap_id_idx ON apflora.apber USING btree (ap_id);\n\n\n\nCREATE INDEX apber_bearbeiter_idx ON apflora.apber USING btree (bearbeiter);\n\n\n\nCREATE INDEX apber_beurteilung_idx ON apflora.apber USING btree (beurteilung);\n\n\n\nCREATE INDEX apber_id_idx ON apflora.apber USING btree (id);\n\n\n\nCREATE INDEX apber_jahr_idx ON apflora.apber USING btree (jahr);\n\n\n\nCREATE INDEX apberuebersicht_id_idx ON apflora.apberuebersicht USING btree (id);\n\n\n\nCREATE INDEX apberuebersicht_jahr_idx ON apflora.apberuebersicht USING btree (jahr);\n\n\n\nCREATE INDEX apberuebersicht_proj_id_idx ON apflora.apberuebersicht USING btree (proj_id);\n\n\n\nCREATE INDEX assozart_ae_id_idx ON apflora.assozart USING btree (ae_id);\n\n\n\nCREATE INDEX assozart_ap_id_idx ON apflora.assozart USING btree (ap_id);\n\n\n\nCREATE INDEX assozart_id_idx ON apflora.assozart USING btree (id);\n\n\n\nCREATE INDEX beob_art_id_idx ON apflora.beob USING btree (art_id);\n\n\n\nCREATE INDEX beob_id_idx ON apflora.beob USING btree (id);\n\n\n\nCREATE INDEX beob_nicht_zuordnen_idx ON apflora.beob USING btree (nicht_zuordnen);\n\n\n\nCREATE INDEX beob_quelle_id_idx ON apflora.beob USING btree (quelle_id);\n\n\n\nCREATE INDEX beob_quelle_werte_id_idx ON apflora.beob_quelle_werte USING btree (id);\n\n\n\nCREATE INDEX beob_tpop_id_idx ON apflora.beob USING btree (tpop_id);\n\n\n\nCREATE INDEX beob_x_idx ON apflora.beob USING btree (x);\n\n\n\nCREATE INDEX beob_y_idx ON apflora.beob USING btree (y);\n\n\n\nCREATE INDEX beobart_id_idx ON apflora.apart USING btree (id);\n\n\n\nCREATE INDEX ber_ap_id_idx ON apflora.ber USING btree (ap_id);\n\n\n\nCREATE INDEX ber_id_idx ON apflora.ber USING btree (id);\n\n\n\nCREATE INDEX ber_jahr_idx ON apflora.ber USING btree (jahr);\n\n\n\nCREATE INDEX erfkrit_ap_id_idx ON apflora.erfkrit USING btree (ap_id);\n\n\n\nCREATE INDEX erfkrit_erfolg_idx ON apflora.erfkrit USING btree (erfolg);\n\n\n\nCREATE INDEX erfkrit_id_idx ON apflora.erfkrit USING btree (id);\n\n\n\nCREATE INDEX idealbiotop_ap_id_idx ON apflora.idealbiotop USING btree (ap_id);\n\n\n\nCREATE INDEX idealbiotop_id_idx ON apflora.idealbiotop USING btree (id);\n\n\n\nCREATE INDEX message_id_idx ON apflora.message USING btree (id);\n\n\n\nCREATE INDEX message_time_idx ON apflora.message USING btree (\"time\");\n\n\n\nCREATE INDEX pop_ap_id_idx ON apflora.pop USING btree (ap_id);\n\n\n\nCREATE INDEX pop_bekannt_seit_idx ON apflora.pop USING btree (bekannt_seit);\n\n\n\nCREATE INDEX pop_id_idx ON apflora.pop USING btree (id);\n\n\n\nCREATE INDEX pop_name_idx ON apflora.pop USING btree (name);\n\n\n\nCREATE INDEX pop_nr_idx ON apflora.pop USING btree (nr);\n\n\n\nCREATE INDEX pop_status_idx ON apflora.pop USING btree (status);\n\n\n\nCREATE INDEX \"pop_status_werte_HerkunftId_idx\" ON apflora.pop_status_werte USING btree (code);\n\n\n\nCREATE INDEX \"pop_status_werte_HerkunftOrd_idx\" ON apflora.pop_status_werte USING btree (sort);\n\n\n\nCREATE INDEX \"pop_status_werte_HerkunftTxt_idx\" ON apflora.pop_status_werte USING btree (text);\n\n\n\nCREATE INDEX pop_status_werte_id_idx ON apflora.pop_status_werte USING btree (id);\n\n\n\nCREATE INDEX pop_x_idx ON apflora.pop USING btree (x);\n\n\n\nCREATE INDEX pop_y_idx ON apflora.pop USING btree (y);\n\n\n\nCREATE INDEX popber_entwicklung_idx ON apflora.popber USING btree (entwicklung);\n\n\n\nCREATE INDEX popber_id_idx ON apflora.popber USING btree (id);\n\n\n\nCREATE INDEX popber_jahr_idx ON apflora.popber USING btree (jahr);\n\n\n\nCREATE INDEX popber_pop_id_idx ON apflora.popber USING btree (pop_id);\n\n\n\nCREATE INDEX popmassnber_beurteilung_idx ON apflora.popmassnber USING btree (beurteilung);\n\n\n\nCREATE INDEX popmassnber_id_idx ON apflora.popmassnber USING btree (id);\n\n\n\nCREATE INDEX popmassnber_jahr_idx ON apflora.popmassnber USING btree (jahr);\n\n\n\nCREATE INDEX popmassnber_pop_id_idx ON apflora.popmassnber USING btree (pop_id);\n\n\n\nCREATE INDEX projekt_id_idx ON apflora.projekt USING btree (id);\n\n\n\nCREATE INDEX projekt_name_idx ON apflora.projekt USING btree (name);\n\n\n\nCREATE INDEX tpop_apber_relevant_idx ON apflora.tpop USING btree (apber_relevant);\n\n\n\nCREATE INDEX \"tpop_apberrelevant_werte_DomainCode_idx\" ON apflora.tpop_apberrelevant_werte USING btree (code);\n\n\n\nCREATE INDEX \"tpop_apberrelevant_werte_DomainTxt_idx\" ON apflora.tpop_apberrelevant_werte USING btree (text);\n\n\n\nCREATE INDEX tpop_apberrelevant_werte_id_idx ON apflora.tpop_apberrelevant_werte USING btree (id);\n\n\n\nCREATE INDEX \"tpop_entwicklung_werte_EntwicklungCode_idx\" ON apflora.tpop_entwicklung_werte USING btree (code);\n\n\n\nCREATE INDEX \"tpop_entwicklung_werte_EntwicklungOrd_idx\" ON apflora.tpop_entwicklung_werte USING btree (sort);\n\n\n\nCREATE INDEX tpop_entwicklung_werte_id_idx ON apflora.tpop_entwicklung_werte USING btree (id);\n\n\n\nCREATE INDEX tpop_flurname_idx ON apflora.tpop USING btree (flurname);\n\n\n\nCREATE INDEX tpop_id_idx ON apflora.tpop USING btree (id);\n\n\n\nCREATE INDEX tpop_nr_idx ON apflora.tpop USING btree (nr);\n\n\n\nCREATE INDEX tpop_pop_id_idx ON apflora.tpop USING btree (pop_id);\n\n\n\nCREATE INDEX tpop_status_idx ON apflora.tpop USING btree (status);\n\n\n\nCREATE INDEX tpop_x_idx ON apflora.tpop USING btree (x);\n\n\n\nCREATE INDEX tpop_y_idx ON apflora.tpop USING btree (y);\n\n\n\nCREATE INDEX tpopber_entwicklung_idx ON apflora.tpopber USING btree (entwicklung);\n\n\n\nCREATE INDEX tpopber_id_idx ON apflora.tpopber USING btree (id);\n\n\n\nCREATE INDEX tpopber_jahr_idx ON apflora.tpopber USING btree (jahr);\n\n\n\nCREATE INDEX tpopber_tpop_id_idx ON apflora.tpopber USING btree (tpop_id);\n\n\n\nCREATE INDEX tpopkontr_bearbeiter_idx ON apflora.tpopkontr USING btree (bearbeiter);\n\n\n\nCREATE INDEX tpopkontr_datum_idx ON apflora.tpopkontr USING btree (datum);\n\n\n\nCREATE INDEX tpopkontr_entwicklung_idx ON apflora.tpopkontr USING btree (entwicklung);\n\n\n\nCREATE INDEX tpopkontr_id_idx ON apflora.tpopkontr USING btree (id);\n\n\n\nCREATE INDEX tpopkontr_idbiotuebereinst_werte_code_idx ON apflora.tpopkontr_idbiotuebereinst_werte USING btree (code);\n\n\n\nCREATE INDEX tpopkontr_idbiotuebereinst_werte_id_idx ON apflora.tpopkontr_idbiotuebereinst_werte USING btree (id);\n\n\n\nCREATE INDEX tpopkontr_idbiotuebereinst_werte_sort_idx ON apflora.tpopkontr_idbiotuebereinst_werte USING btree (sort);\n\n\n\nCREATE INDEX tpopkontr_idealbiotop_uebereinstimmung_idx ON apflora.tpopkontr USING btree (idealbiotop_uebereinstimmung);\n\n\n\nCREATE INDEX tpopkontr_jahr_idx ON apflora.tpopkontr USING btree (jahr);\n\n\n\nCREATE INDEX tpopkontr_tpop_id_idx ON apflora.tpopkontr USING btree (tpop_id);\n\n\n\nCREATE INDEX tpopkontr_typ_idx ON apflora.tpopkontr USING btree (typ);\n\n\n\nCREATE INDEX tpopkontr_typ_werte_id_idx ON apflora.tpopkontr_typ_werte USING btree (id);\n\n\n\nCREATE UNIQUE INDEX tpopkontr_zeit_id_idx ON apflora.tpopkontr USING btree (zeit_id);\n\n\n\nCREATE INDEX tpopkontrzaehl_anzahl_idx ON apflora.tpopkontrzaehl USING btree (anzahl);\n\n\n\nCREATE INDEX tpopkontrzaehl_einheit_idx ON apflora.tpopkontrzaehl USING btree (einheit);\n\n\n\nCREATE INDEX tpopkontrzaehl_einheit_werte_code_idx ON apflora.tpopkontrzaehl_einheit_werte USING btree (code);\n\n\n\nCREATE INDEX tpopkontrzaehl_einheit_werte_id_idx ON apflora.tpopkontrzaehl_einheit_werte USING btree (id);\n\n\n\nCREATE INDEX tpopkontrzaehl_einheit_werte_sort_idx ON apflora.tpopkontrzaehl_einheit_werte USING btree (sort);\n\n\n\nCREATE INDEX tpopkontrzaehl_id_idx ON apflora.tpopkontrzaehl USING btree (id);\n\n\n\nCREATE INDEX tpopkontrzaehl_methode_idx ON apflora.tpopkontrzaehl USING btree (methode);\n\n\n\nCREATE INDEX tpopkontrzaehl_methode_werte_code_idx ON apflora.tpopkontrzaehl_methode_werte USING btree (code);\n\n\n\nCREATE INDEX tpopkontrzaehl_methode_werte_id_idx ON apflora.tpopkontrzaehl_methode_werte USING btree (id);\n\n\n\nCREATE INDEX tpopkontrzaehl_methode_werte_sort_idx ON apflora.tpopkontrzaehl_methode_werte USING btree (sort);\n\n\n\nCREATE INDEX tpopkontrzaehl_tpopkontr_id_idx2 ON apflora.tpopkontrzaehl USING btree (tpopkontr_id);\n\n\n\nCREATE INDEX tpopmassn_bearbeiter_idx ON apflora.tpopmassn USING btree (bearbeiter);\n\n\n\nCREATE INDEX tpopmassn_erfbeurt_werte_code_idx ON apflora.tpopmassn_erfbeurt_werte USING btree (code);\n\n\n\nCREATE INDEX tpopmassn_erfbeurt_werte_id_idx ON apflora.tpopmassn_erfbeurt_werte USING btree (id);\n\n\n\nCREATE INDEX tpopmassn_erfbeurt_werte_sort_idx ON apflora.tpopmassn_erfbeurt_werte USING btree (sort);\n\n\n\nCREATE UNIQUE INDEX tpopmassn_id_idx ON apflora.tpopmassn USING btree (id);\n\n\n\nCREATE INDEX tpopmassn_jahr_idx ON apflora.tpopmassn USING btree (jahr);\n\n\n\nCREATE INDEX tpopmassn_tpop_id_idx ON apflora.tpopmassn USING btree (tpop_id);\n\n\n\nCREATE INDEX tpopmassn_typ_idx ON apflora.tpopmassn USING btree (typ);\n\n\n\nCREATE INDEX tpopmassn_typ_werte_code_idx ON apflora.tpopmassn_typ_werte USING btree (code);\n\n\n\nCREATE INDEX tpopmassn_typ_werte_id_idx ON apflora.tpopmassn_typ_werte USING btree (id);\n\n\n\nCREATE INDEX tpopmassn_typ_werte_sort_idx ON apflora.tpopmassn_typ_werte USING btree (sort);\n\n\n\nCREATE INDEX tpopmassnber_beurteilung_idx ON apflora.tpopmassnber USING btree (beurteilung);\n\n\n\nCREATE INDEX tpopmassnber_id_idx ON apflora.tpopmassnber USING btree (id);\n\n\n\nCREATE INDEX tpopmassnber_jahr_idx ON apflora.tpopmassnber USING btree (jahr);\n\n\n\nCREATE INDEX tpopmassnber_tpop_id_idx ON apflora.tpopmassnber USING btree (tpop_id);\n\n\n\nCREATE INDEX user_id_idx ON apflora.\"user\" USING btree (id);\n\n\n\nCREATE INDEX user_name_idx ON apflora.\"user\" USING btree (name);\n\n\n\nCREATE INDEX usermessage_id_idx ON apflora.usermessage USING btree (id);\n\n\n\nCREATE INDEX usermessage_message_id_idx ON apflora.usermessage USING btree (message_id);\n\n\n\nCREATE INDEX usermessage_user_name_idx ON apflora.usermessage USING btree (user_name);\n\n\n\nCREATE INDEX ziel_ap_id_idx ON apflora.ziel USING btree (ap_id);\n\n\n\nCREATE INDEX ziel_id_idx ON apflora.ziel USING btree (id);\n\n\n\nCREATE INDEX ziel_jahr_idx ON apflora.ziel USING btree (jahr);\n\n\n\nCREATE INDEX ziel_typ_idx ON apflora.ziel USING btree (typ);\n\n\n\nCREATE INDEX ziel_typ_werte_code_idx ON apflora.ziel_typ_werte USING btree (code);\n\n\n\nCREATE INDEX ziel_typ_werte_id_idx ON apflora.ziel_typ_werte USING btree (id);\n\n\n\nCREATE INDEX ziel_typ_werte_sort_idx ON apflora.ziel_typ_werte USING btree (sort);\n\n\n\nCREATE INDEX zielber_id_idx ON apflora.zielber USING btree (id);\n\n\n\nCREATE INDEX zielber_jahr_idx ON apflora.zielber USING btree (jahr);\n\n\n\nCREATE INDEX zielber_ziel_id_idx ON apflora.zielber USING btree (ziel_id);\n\n\n\nCREATE OR REPLACE VIEW apflora.v_exportevab_beob AS\n SELECT tpopkontr.zeit_id AS \"fkZeitpunkt\",\n    tpopkontr.id AS \"idBeobachtung\",\n    COALESCE(adresse.evab_id_person, 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AS fkautor,\n    ap.art_id AS fkart,\n    18 AS fkartgruppe,\n    1 AS fkaa1,\n        CASE\n            WHEN (tpop.status < 200) THEN 4\n            WHEN (EXISTS ( SELECT tpopmassn.tpop_id\n               FROM apflora.tpopmassn\n              WHERE ((tpopmassn.tpop_id = tpopkontr.tpop_id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr <= tpopkontr.jahr)))) THEN 6\n            WHEN (tpop.status_unklar = true) THEN 3\n            ELSE 5\n        END AS \"fkAAINTRODUIT\",\n        CASE\n            WHEN ((v_tpopkontr_maxanzahl.anzahl = 0) AND (EXISTS ( SELECT tpopber.tpop_id\n               FROM apflora.tpopber\n              WHERE ((tpopber.tpop_id = tpopkontr.tpop_id) AND (tpopber.entwicklung = 8) AND (tpopber.jahr = tpopkontr.jahr))))) THEN 2\n            WHEN (v_tpopkontr_maxanzahl.anzahl = 0) THEN 3\n            ELSE 1\n        END AS \"fkAAPRESENCE\",\n    \"substring\"(tpopkontr.gefaehrdung, 1, 244) AS \"MENACES\",\n    \"substring\"(tpopkontr.vitalitaet, 1, 200) AS \"VITALITE_PLANTE\",\n    \"substring\"(tpop.beschreibung, 1, 244) AS \"STATION\",\n    \"substring\"(concat('Anzahlen: ', array_to_string(array_agg(tpopkontrzaehl.anzahl), ', '::text), ', Zaehleinheiten: ', string_agg((tpopkontrzaehl_einheit_werte.text)::text, ', '::text), ', Methoden: ', string_agg((tpopkontrzaehl_methode_werte.text)::text, ', '::text)), 1, 244) AS \"ABONDANCE\",\n    'C'::text AS \"EXPERTISE_INTRODUIT\",\n        CASE\n            WHEN (apflora_adresse_2.evab_id_person IS NOT NULL) THEN \"substring\"(apflora_adresse_2.name, 1, 99)\n            ELSE 'topos Marti & Müller AG Zürich'::text\n        END AS \"EXPERTISE_INTRODUITE_NOM\"\n   FROM (((apflora.ap\n     LEFT JOIN apflora.adresse apflora_adresse_2 ON ((ap.bearbeiter = apflora_adresse_2.id)))\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (((apflora.tpopkontr\n     LEFT JOIN apflora.adresse ON ((tpopkontr.bearbeiter = adresse.id)))\n     JOIN apflora.v_tpopkontr_maxanzahl ON ((v_tpopkontr_maxanzahl.id = tpopkontr.id)))\n     LEFT JOIN ((apflora.tpopkontrzaehl\n     LEFT JOIN apflora.tpopkontrzaehl_einheit_werte ON ((tpopkontrzaehl.einheit = tpopkontrzaehl_einheit_werte.code)))\n     LEFT JOIN apflora.tpopkontrzaehl_methode_werte ON ((tpopkontrzaehl.methode = tpopkontrzaehl_methode_werte.code))) ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n     JOIN apflora.ae_eigenschaften ON ((ae_eigenschaften.id = ap.art_id)))\n  WHERE ((ae_eigenschaften.taxid > 150) AND (ae_eigenschaften.taxid < 1000000) AND (tpop.x IS NOT NULL) AND (tpop.y IS NOT NULL) AND ((tpopkontr.typ)::text = ANY (ARRAY[('Ausgangszustand'::character varying)::text, ('Zwischenbeurteilung'::character varying)::text, ('Freiwilligen-Erfolgskontrolle'::character varying)::text])) AND (tpop.status <> 201) AND (tpopkontr.bearbeiter IS NOT NULL) AND (tpopkontr.bearbeiter <> 'a1146ae4-4e03-4032-8aa8-bc46ba02f468'::uuid) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.jahr)::double precision <> date_part('year'::text, ('now'::text)::date)) AND (tpop.bekannt_seit IS NOT NULL) AND ((tpop.status = ANY (ARRAY[100, 101])) OR ((tpopkontr.jahr - tpop.bekannt_seit) > 5)) AND (tpop.flurname IS NOT NULL) AND (ap.id IN ( SELECT v_exportevab_projekt.\"idProjekt\"\n           FROM apflora.v_exportevab_projekt)) AND (pop.id IN ( SELECT v_exportevab_raum.\"idRaum\"\n           FROM apflora.v_exportevab_raum)) AND (tpop.id IN ( SELECT v_exportevab_ort.\"idOrt\"\n           FROM apflora.v_exportevab_ort)) AND (tpopkontr.zeit_id IN ( SELECT v_exportevab_zeit.\"idZeitpunkt\"\n           FROM apflora.v_exportevab_zeit)))\n  GROUP BY tpopkontr.zeit_id, tpopkontr.tpop_id, tpopkontr.id, tpopkontr.jahr, adresse.evab_id_person, ap.id,\n        CASE\n            WHEN (tpop.status < 200) THEN 4\n            WHEN (EXISTS ( SELECT tpopmassn.tpop_id\n               FROM apflora.tpopmassn\n              WHERE ((tpopmassn.tpop_id = tpopkontr.tpop_id) AND ((tpopmassn.typ >= 1) AND (tpopmassn.typ <= 3)) AND (tpopmassn.jahr <= tpopkontr.jahr)))) THEN 6\n            WHEN (tpop.status_unklar = true) THEN 3\n            ELSE 5\n        END, v_tpopkontr_maxanzahl.anzahl, tpopkontr.gefaehrdung, tpopkontr.vitalitaet, tpop.beschreibung, apflora_adresse_2.evab_id_person, apflora_adresse_2.name;\n\n\n\nCREATE OR REPLACE VIEW apflora.v_qk_apber_ohnejahr AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'AP-Bericht ohne Jahr:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'AP-Berichte'::text, (apber.id)::text] AS url,\n    ARRAY[concat('AP-Bericht (id): ', apber.id)] AS text\n   FROM (apflora.ap\n     JOIN apflora.apber ON ((ap.id = apber.ap_id)))\n  GROUP BY ap.id, apber.id\n HAVING (apber.jahr IS NULL)\n  ORDER BY apber.id;\n\n\n\nCREATE OR REPLACE VIEW apflora.v_qk_feldkontr_ohnezaehlung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Feldkontrolle ohne Zaehlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Feld-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  GROUP BY ap.id, pop.id, tpop.id, tpop.nr, tpopkontr.id, tpopkontrzaehl.id\n HAVING ((tpopkontrzaehl.id IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text <> 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\nCREATE OR REPLACE VIEW apflora.v_qk_freiwkontr_ohnezaehlung AS\n SELECT ap.proj_id,\n    ap.id AS ap_id,\n    'Freiwilligen-Kontrolle ohne Zaehlung:'::text AS hw,\n    ARRAY['Projekte'::text, '4635372c-431c-11e8-bb30-e77f6cdd35a6'::text, 'Aktionspläne'::text, (ap.id)::text, 'Populationen'::text, (pop.id)::text, 'Teil-Populationen'::text, (tpop.id)::text, 'Freiwilligen-Kontrollen'::text, (tpopkontr.id)::text] AS url,\n    ARRAY[concat('Population (Nr.): ', pop.nr), concat('Teil-Population (Nr.): ', tpop.nr), concat('Feld-Kontrolle (Jahr): ', tpopkontr.jahr)] AS text,\n    tpopkontr.jahr AS \"Berichtjahr\"\n   FROM (apflora.ap\n     JOIN (apflora.pop\n     JOIN (apflora.tpop\n     JOIN (apflora.tpopkontr\n     LEFT JOIN apflora.tpopkontrzaehl ON ((tpopkontr.id = tpopkontrzaehl.tpopkontr_id))) ON ((tpop.id = tpopkontr.tpop_id))) ON ((pop.id = tpop.pop_id))) ON ((ap.id = pop.ap_id)))\n  GROUP BY ap.id, pop.id, tpop.id, tpop.nr, tpopkontr.id, tpopkontrzaehl.id\n HAVING ((tpopkontrzaehl.id IS NULL) AND (tpopkontr.jahr IS NOT NULL) AND ((tpopkontr.typ)::text = 'Freiwilligen-Erfolgskontrolle'::text))\n  ORDER BY ap.id, pop.nr, tpop.nr, tpopkontr.jahr;\n\n\n\nCREATE TRIGGER adresse_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.adresse FOR EACH ROW EXECUTE PROCEDURE public.adresse_on_update_set_mut();\n\n\n\nCREATE TRIGGER ap_bearbstand_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ap_bearbstand_werte FOR EACH ROW EXECUTE PROCEDURE public.ap_bearbstand_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER ap_erfbeurtkrit_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ap_erfbeurtkrit_werte FOR EACH ROW EXECUTE PROCEDURE public.ap_erfbeurtkrit_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER ap_erfkrit_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ap_erfkrit_werte FOR EACH ROW EXECUTE PROCEDURE public.ap_erfkrit_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER ap_insert_add_apart AFTER INSERT ON apflora.ap FOR EACH ROW EXECUTE PROCEDURE apflora.ap_insert_add_apart();\n\n\n\nCREATE TRIGGER ap_insert_add_idealbiotop AFTER INSERT ON apflora.ap FOR EACH ROW EXECUTE PROCEDURE apflora.ap_insert_add_idealbiotop();\n\n\n\nCREATE TRIGGER ap_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ap FOR EACH ROW EXECUTE PROCEDURE public.ap_on_update_set_mut();\n\n\n\nCREATE TRIGGER ap_umsetzung_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ap_umsetzung_werte FOR EACH ROW EXECUTE PROCEDURE public.ap_umsetzung_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER apber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.apber FOR EACH ROW EXECUTE PROCEDURE public.apber_on_update_set_mut();\n\n\n\nCREATE TRIGGER apberuebersicht_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.apberuebersicht FOR EACH ROW EXECUTE PROCEDURE public.apberuebersicht_on_update_set_mut();\n\n\n\nCREATE TRIGGER assozart_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.assozart FOR EACH ROW EXECUTE PROCEDURE public.assozart_on_update_set_mut();\n\n\n\nCREATE TRIGGER beob_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.beob FOR EACH ROW EXECUTE PROCEDURE public.beob_on_update_set_mut();\n\n\n\nCREATE TRIGGER ber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ber FOR EACH ROW EXECUTE PROCEDURE public.ber_on_update_set_mut();\n\n\n\nCREATE TRIGGER encrypt_pass BEFORE INSERT OR UPDATE ON apflora.\"user\" FOR EACH ROW EXECUTE PROCEDURE auth.encrypt_pass();\n\n\n\nCREATE CONSTRAINT TRIGGER ensure_user_role_exists AFTER INSERT OR UPDATE ON apflora.\"user\" NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE auth.check_role_exists();\n\n\n\nCREATE TRIGGER erfkrit_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.erfkrit FOR EACH ROW EXECUTE PROCEDURE public.erfkrit_on_update_set_mut();\n\n\n\nCREATE TRIGGER idealbiotop_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.idealbiotop FOR EACH ROW EXECUTE PROCEDURE public.idealbiotop_on_update_set_mut();\n\n\n\nCREATE TRIGGER pop_max_one_massnber_per_year BEFORE INSERT OR UPDATE ON apflora.popmassnber FOR EACH ROW EXECUTE PROCEDURE apflora.pop_max_one_massnber_per_year();\n\n\n\nCREATE TRIGGER pop_max_one_popber_per_year BEFORE INSERT OR UPDATE ON apflora.popber FOR EACH ROW EXECUTE PROCEDURE apflora.pop_max_one_popber_per_year();\n\n\n\nCREATE TRIGGER pop_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.pop FOR EACH ROW EXECUTE PROCEDURE public.pop_on_update_set_mut();\n\n\n\nCREATE TRIGGER pop_status_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.pop_status_werte FOR EACH ROW EXECUTE PROCEDURE public.pop_status_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER popber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.popber FOR EACH ROW EXECUTE PROCEDURE public.popber_on_update_set_mut();\n\n\n\nCREATE TRIGGER popmassnber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.popmassnber FOR EACH ROW EXECUTE PROCEDURE public.popmassnber_on_update_set_mut();\n\n\n\nCREATE TRIGGER projekt_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.projekt FOR EACH ROW EXECUTE PROCEDURE public.projekt_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpop_apberrelevant_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpop_apberrelevant_werte FOR EACH ROW EXECUTE PROCEDURE public.tpop_apberrelevant_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpop_entwicklung_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpop_entwicklung_werte FOR EACH ROW EXECUTE PROCEDURE public.tpop_entwicklung_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpop_max_one_massnber_per_year BEFORE INSERT OR UPDATE ON apflora.tpopmassnber FOR EACH ROW EXECUTE PROCEDURE apflora.tpop_max_one_massnber_per_year();\n\n\n\nCREATE TRIGGER tpop_max_one_tpopber_per_year BEFORE INSERT OR UPDATE ON apflora.tpopber FOR EACH ROW EXECUTE PROCEDURE apflora.tpop_max_one_tpopber_per_year();\n\n\n\nCREATE TRIGGER tpop_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpop FOR EACH ROW EXECUTE PROCEDURE public.tpop_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopber FOR EACH ROW EXECUTE PROCEDURE public.tpopber_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontr_idbiotuebereinst_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontr_idbiotuebereinst_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopkontr_idbiotuebereinst_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontr_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontr FOR EACH ROW EXECUTE PROCEDURE public.tpopkontr_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontr_typ_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontr_typ_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopkontr_typ_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontrzaehl_einheit_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontrzaehl_einheit_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopkontrzaehl_einheit_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontrzaehl_methode_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontrzaehl_methode_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopkontrzaehl_methode_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopkontrzaehl_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopkontrzaehl FOR EACH ROW EXECUTE PROCEDURE public.tpopkontrzaehl_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopmassn_erfbeurt_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopmassn_erfbeurt_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopmassn_erfbeurt_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopmassn_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopmassn FOR EACH ROW EXECUTE PROCEDURE public.tpopmassn_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopmassn_typ_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopmassn_typ_werte FOR EACH ROW EXECUTE PROCEDURE public.tpopmassn_typ_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER tpopmassnber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.tpopmassnber FOR EACH ROW EXECUTE PROCEDURE public.tpopmassnber_on_update_set_mut();\n\n\n\nCREATE TRIGGER ziel_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ziel FOR EACH ROW EXECUTE PROCEDURE public.ziel_on_update_set_mut();\n\n\n\nCREATE TRIGGER ziel_typ_werte_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.ziel_typ_werte FOR EACH ROW EXECUTE PROCEDURE public.ziel_typ_werte_on_update_set_mut();\n\n\n\nCREATE TRIGGER zielber_on_update_set_mut BEFORE INSERT OR UPDATE ON apflora.zielber FOR EACH ROW EXECUTE PROCEDURE public.zielber_on_update_set_mut();\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_fk_adresse FOREIGN KEY (bearbeiter) REFERENCES apflora.adresse(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_fk_ae_eigenschaften FOREIGN KEY (art_id) REFERENCES apflora.ae_eigenschaften(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_fk_ap_bearbstand_werte FOREIGN KEY (bearbeitung) REFERENCES apflora.ap_bearbstand_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_fk_ap_umsetzung_werte FOREIGN KEY (umsetzung) REFERENCES apflora.ap_umsetzung_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.ap\n    ADD CONSTRAINT ap_proj_id_fkey FOREIGN KEY (proj_id) REFERENCES apflora.projekt(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.apart\n    ADD CONSTRAINT apart_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.apart\n    ADD CONSTRAINT apart_fk_ae_eigenschaften FOREIGN KEY (art_id) REFERENCES apflora.ae_eigenschaften(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.apber\n    ADD CONSTRAINT apber_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.apber\n    ADD CONSTRAINT apber_fk_adresse FOREIGN KEY (bearbeiter) REFERENCES apflora.adresse(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.apber\n    ADD CONSTRAINT apber_fk_ap_erfkrit_werte FOREIGN KEY (beurteilung) REFERENCES apflora.ap_erfkrit_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.apberuebersicht\n    ADD CONSTRAINT apberuebersicht_proj_id_fkey FOREIGN KEY (proj_id) REFERENCES apflora.projekt(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.assozart\n    ADD CONSTRAINT assozart_ae_id_fkey FOREIGN KEY (ae_id) REFERENCES apflora.ae_eigenschaften(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.assozart\n    ADD CONSTRAINT assozart_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.assozart\n    ADD CONSTRAINT assozart_fk_ae_eigenschaften FOREIGN KEY (ae_id) REFERENCES apflora.ae_eigenschaften(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.beob\n    ADD CONSTRAINT beob_fk_ae_eigenschaften FOREIGN KEY (art_id) REFERENCES apflora.ae_eigenschaften(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.beob\n    ADD CONSTRAINT beob_fk_beob_quelle_werte FOREIGN KEY (quelle_id) REFERENCES apflora.beob_quelle_werte(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.beob\n    ADD CONSTRAINT beob_tpop_id_fkey FOREIGN KEY (tpop_id) REFERENCES apflora.tpop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.ber\n    ADD CONSTRAINT ber_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.erfkrit\n    ADD CONSTRAINT erfkrit_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.erfkrit\n    ADD CONSTRAINT erfkrit_fk_ap_erfkrit_werte FOREIGN KEY (erfolg) REFERENCES apflora.ap_erfkrit_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.usermessage\n    ADD CONSTRAINT fk_user FOREIGN KEY (user_name) REFERENCES apflora.\"user\"(name) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.idealbiotop\n    ADD CONSTRAINT idealbiotop_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.pop\n    ADD CONSTRAINT pop_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.pop\n    ADD CONSTRAINT pop_fk_pop_status_werte FOREIGN KEY (status) REFERENCES apflora.pop_status_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.popber\n    ADD CONSTRAINT popber_fk_tpop_entwicklung_werte FOREIGN KEY (entwicklung) REFERENCES apflora.tpop_entwicklung_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.popber\n    ADD CONSTRAINT popber_pop_id_fkey FOREIGN KEY (pop_id) REFERENCES apflora.pop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.popmassnber\n    ADD CONSTRAINT popmassnber_fk_tpopmassn_erfbeurt_werte FOREIGN KEY (beurteilung) REFERENCES apflora.tpopmassn_erfbeurt_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.popmassnber\n    ADD CONSTRAINT popmassnber_pop_id_fkey FOREIGN KEY (pop_id) REFERENCES apflora.pop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpop\n    ADD CONSTRAINT tpop_fk_tpop_apberrelevant_werte FOREIGN KEY (apber_relevant) REFERENCES apflora.tpop_apberrelevant_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpop\n    ADD CONSTRAINT tpop_fk_tpop_status_werte FOREIGN KEY (status) REFERENCES apflora.pop_status_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpop\n    ADD CONSTRAINT tpop_pop_id_fkey FOREIGN KEY (pop_id) REFERENCES apflora.pop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpopber\n    ADD CONSTRAINT tpopber_fk_tpop_entwicklung_werte FOREIGN KEY (entwicklung) REFERENCES apflora.tpop_entwicklung_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopber\n    ADD CONSTRAINT tpopber_tpop_id_fkey FOREIGN KEY (tpop_id) REFERENCES apflora.tpop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_fk_adresse FOREIGN KEY (bearbeiter) REFERENCES apflora.adresse(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_fk_tpop_entwicklung_werte FOREIGN KEY (entwicklung) REFERENCES apflora.tpop_entwicklung_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_fk_tpopkontr_idbiotuebereinst_werte FOREIGN KEY (idealbiotop_uebereinstimmung) REFERENCES apflora.tpopkontr_idbiotuebereinst_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_fk_tpopkontr_typ_werte FOREIGN KEY (typ) REFERENCES apflora.tpopkontr_typ_werte(text) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontr\n    ADD CONSTRAINT tpopkontr_tpop_id_fkey FOREIGN KEY (tpop_id) REFERENCES apflora.tpop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl\n    ADD CONSTRAINT tpopkontrzaehl_fk_tpopkontrzaehl_einheit_werte FOREIGN KEY (einheit) REFERENCES apflora.tpopkontrzaehl_einheit_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl\n    ADD CONSTRAINT tpopkontrzaehl_fk_tpopkontrzaehl_methode_werte FOREIGN KEY (methode) REFERENCES apflora.tpopkontrzaehl_methode_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopkontrzaehl\n    ADD CONSTRAINT tpopkontrzaehl_tpopkontr_id_fkey FOREIGN KEY (tpopkontr_id) REFERENCES apflora.tpopkontr(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpopmassn\n    ADD CONSTRAINT tpopmassn_fk_adresse FOREIGN KEY (bearbeiter) REFERENCES apflora.adresse(id) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopmassn\n    ADD CONSTRAINT tpopmassn_fk_tpopmassn_typ_werte FOREIGN KEY (typ) REFERENCES apflora.tpopmassn_typ_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopmassn\n    ADD CONSTRAINT tpopmassn_tpop_id_fkey FOREIGN KEY (tpop_id) REFERENCES apflora.tpop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.tpopmassnber\n    ADD CONSTRAINT tpopmassnber_fk_tpopmassn_erfbeurt_werte FOREIGN KEY (beurteilung) REFERENCES apflora.tpopmassn_erfbeurt_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.tpopmassnber\n    ADD CONSTRAINT tpopmassnber_tpop_id_fkey FOREIGN KEY (tpop_id) REFERENCES apflora.tpop(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.usermessage\n    ADD CONSTRAINT usermessage_message_id_fkey FOREIGN KEY (message_id) REFERENCES apflora.message(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.ziel\n    ADD CONSTRAINT ziel_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES apflora.ap(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY apflora.ziel\n    ADD CONSTRAINT ziel_fk_ziel_typ_werte FOREIGN KEY (typ) REFERENCES apflora.ziel_typ_werte(code) ON UPDATE CASCADE ON DELETE SET NULL;\n\n\n\nALTER TABLE ONLY apflora.zielber\n    ADD CONSTRAINT zielber_ziel_id_fkey FOREIGN KEY (ziel_id) REFERENCES apflora.ziel(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\nALTER TABLE apflora.\"user\" ENABLE ROW LEVEL SECURITY;\n\n\nCREATE SCHEMA fuzzysearch;\n\n-- Create many tables to test fuzzy string search\n-- computing hints for non existing tables\nDO\n$$\nDECLARE\n  r record;\nBEGIN\n  FOR r IN\n    SELECT\n      format('CREATE TABLE fuzzysearch.unknown_table_%s ()', n) AS ct\n    FROM\n      generate_series(1, 499) n\n    LOOP\n      EXECUTE r.ct;\n    END LOOP;\nEND\n$$;\n\nDROP ROLE IF EXISTS postgrest_test_anonymous;\nCREATE ROLE postgrest_test_anonymous;\n\nGRANT postgrest_test_anonymous TO :PGUSER;\n\nGRANT USAGE ON SCHEMA apflora TO postgrest_test_anonymous;\nGRANT USAGE ON SCHEMA fuzzysearch TO postgrest_test_anonymous;\n\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA apflora\nTO postgrest_test_anonymous;\n\ncreate or replace function apflora.notify_pgrst() returns void as $$\n  notify pgrst;\n$$ language sql;\n"
  },
  {
    "path": "test/io/fixtures/database.sql",
    "content": "SET client_min_messages = WARNING; -- suppress \"NOTICE: ...\" messages which pollute the log\nSET check_function_bodies = false; -- to allow conditionals based on the pg version\nSET search_path = public;\n"
  },
  {
    "path": "test/io/fixtures/db_config.sql",
    "content": "CREATE ROLE db_config_authenticator LOGIN NOINHERIT;\n\n-- reloadable config options\n-- these settings will override the values in configs/no-defaults.config, so they must be different\nALTER ROLE db_config_authenticator SET pgrst.client_error_verbosity = 'minimal';\nALTER ROLE db_config_authenticator SET pgrst.db_aggregates_enabled = 'false';\nALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'anonymous';\nALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions';\nALTER ROLE db_config_authenticator SET pgrst.db_max_rows = '500';\nALTER ROLE db_config_authenticator SET pgrst.db_plan_enabled = 'false';\nALTER ROLE db_config_authenticator SET pgrst.db_pre_config = 'postgrest.preconf';\nALTER ROLE db_config_authenticator SET pgrst.db_pre_request = 'test.custom_headers';\nALTER ROLE db_config_authenticator SET pgrst.db_prepared_statements = 'false';\nALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root';\nALTER ROLE db_config_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2';\nALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override';\nALTER ROLE db_config_authenticator SET pgrst.jwt_aud = 'https://example.org';\nALTER ROLE db_config_authenticator SET pgrst.jwt_cache_max_entries = '86400';\nALTER ROLE db_config_authenticator SET pgrst.jwt_role_claim_key = '.\"a\".\"role\"';\nALTER ROLE db_config_authenticator SET pgrst.jwt_secret = 'REALLY=REALLY=REALLY=REALLY=VERY=SAFE';\nALTER ROLE db_config_authenticator SET pgrst.jwt_secret_is_base64 = 'false';\nALTER ROLE db_config_authenticator SET pgrst.not_existing = 'should be ignored';\nALTER ROLE db_config_authenticator SET pgrst.openapi_server_proxy_uri = 'https://example.org/api';\nALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://origin.com';\nALTER ROLE db_config_authenticator SET pgrst.server_timing_enabled = 'false';\nALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';\nALTER ROLE db_config_authenticator SET pgrst.db_hoisted_tx_settings = 'autovacuum_work_mem';\n\n-- override with database specific setting\nALTER ROLE db_config_authenticator IN DATABASE :DBNAME SET pgrst.db_extra_search_path = 'public, extensions, private';\nALTER ROLE db_config_authenticator IN DATABASE :DBNAME SET pgrst.jwt_secret = 'OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE';\nALTER ROLE db_config_authenticator IN DATABASE :DBNAME SET pgrst.not_existing = 'should be ignored';\n\n-- other database settings that should be ignored\nCREATE DATABASE other;\nALTER ROLE db_config_authenticator IN DATABASE other SET pgrst.db_max_rows = '1111';\n\n-- non-reloadable configs\nALTER ROLE db_config_authenticator SET pgrst.admin_server_host = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.admin_server_port = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_channel = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_channel_enabled = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_config = 'true';\nALTER ROLE db_config_authenticator SET pgrst.db_pool = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_pool_acquisition_timeout = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_pool_timeout = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_pool_max_idletime = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_pool_max_lifetime = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.db_uri = 'postgresql://ignored';\nALTER ROLE db_config_authenticator SET pgrst.log_level = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.log_query = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.server_host = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.server_port = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.server_unix_socket = 'ignored';\nALTER ROLE db_config_authenticator SET pgrst.server_unix_socket_mode = 'ignored';\n\n-- other authenticator reloadable config options\n-- these settings will override the values in configs/no-defaults.config, so they must be different\nCREATE ROLE other_authenticator LOGIN NOINHERIT;\nALTER ROLE other_authenticator SET pgrst.client_error_verbosity = 'minimal';\nALTER ROLE other_authenticator SET pgrst.db_aggregates_enabled = 'false';\nALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';\nALTER ROLE other_authenticator SET pgrst.db_max_rows = '100';\nALTER ROLE other_authenticator SET pgrst.db_plan_enabled = 'true';\nALTER ROLE other_authenticator SET pgrst.db_pre_config = 'postgrest.other_preconf';\nALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_headers';\nALTER ROLE other_authenticator SET pgrst.db_prepared_statements = 'false';\nALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root';\nALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2';\nALTER ROLE other_authenticator SET pgrst.jwt_aud = 'https://otherexample.org';\nALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';\nALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'false';\nALTER ROLE other_authenticator SET pgrst.jwt_cache_max_entries = '86400';\nALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled';\nALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false';\nALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://otherexample.org/api';\nALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://otherorigin.com';\nALTER ROLE other_authenticator SET pgrst.server_timing_enabled = 'true';\nALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';\nALTER ROLE other_authenticator SET pgrst.db_hoisted_tx_settings = 'maintenance_work_mem';\n\ncreate schema postgrest;\ngrant usage on schema postgrest to db_config_authenticator;\ngrant usage on schema postgrest to other_authenticator;\n\n-- pre-config hook\ncreate or replace function postgrest.pre_config()\nreturns void as $$\nbegin\n  if current_user = 'other_authenticator' then\n    perform\n      set_config('pgrst.jwt_role_claim_key', '.\"other\".\"pre_config_role\"', true)\n    , set_config('pgrst.db_anon_role', 'pre_config_role', true)\n    , set_config('pgrst.db_schemas', 'will be overriden with the above ALTER ROLE.. db_schemas', true)\n    , set_config('pgrst.db_tx_end', 'rollback-allow-override', true);\n  else\n    null;\n  end if;\nend $$ language plpgsql;\n\ncreate or replace function postgrest.preconf()\nreturns void as $$\nbegin\n  null;\nend $$ language plpgsql;\n\ncreate or replace function postgrest.other_preconf()\nreturns void as $$\nbegin\n  perform postgrest.pre_config();\nend $$ language plpgsql;\n\n-- authenticator used for tests that manipulate statement timeout\nCREATE ROLE timeout_authenticator LOGIN NOINHERIT;\n\ncreate function set_statement_timeout(role text, milliseconds int) returns void as $_$\nbegin\n  execute format($$\n    alter role %I set statement_timeout to %L;\n  $$, role, milliseconds);\nend $_$ volatile security definer language plpgsql;\n\n-- authenticator used for test-independent database manipulation\nCREATE ROLE meta_authenticator LOGIN NOINHERIT;\n"
  },
  {
    "path": "test/io/fixtures/fixtures.yaml",
    "content": "cli:\n# success: valid commands\n  - name: help long\n    args: ['--help']\n  - name: help short\n    args: ['-h']\n  - name: version long\n    args: ['--version']\n  - name: version short\n    args: ['-v']\n  - name: example long\n    args: ['--example']\n  - name: example short\n    args: ['-e']\n  - name: dump config\n    args: ['--dump-config']\n  - name: dump schema\n    args: ['--dump-schema']\n    use_defaultenv: true\n  - name: no config\n# failures: config files\n  - name: non-existant config file\n    expect: error\n    args: ['does_not_exist.conf']\n  - name: invalid config file\n    expect: error\n    args: ['test/io-tests/configs/invalid.yaml']\n# failures: wrong config values\n  - name: invalid server-unix-socket-mode not octal\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_SERVER_UNIX_SOCKET_MODE: '800'\n  - name: invalid server-unix-socket-mode < 600\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_SERVER_UNIX_SOCKET_MODE: '599'\n  - name: invalid server-unix-socket-mode > 777\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_SERVER_UNIX_SOCKET_MODE: '778'\n  - name: invalid jwt-aud\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_JWT_AUD: 'http://%%localhorst.invalid'\n  - name: invalid log-level\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: never\n  - name: invalid db-tx-end\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: random\n  - name: invalid openapi-server-proxy-uri\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_OPENAPI_SERVER_PROXY_URI: 'htp:/@@localhorst.invalid'\n  - name: invalid jwt-secret not base64\n    expect: error\n    use_defaultenv: true\n    env:\n      PGRST_JWT_SECRET_IS_BASE64: 'true'\n      PGRST_JWT_SECRET: 'no base-64!'\n# success: parsing config values\n  - name: log-level=\n    expect: 'log-level = \"error\"'\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: \"\"\n  - name: log-level=crit\n    expect: 'log-level = \"crit\"'\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: crit\n  - name: log-level=error\n    expect: 'log-level = \"error\"'\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: error\n  - name: log-level=warn\n    expect: 'log-level = \"warn\"'\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: warn\n  - name: log-level=info\n    expect: 'log-level = \"info\"'\n    use_defaultenv: true\n    env:\n      PGRST_LOG_LEVEL: info\n  - name: db-tx-end=\n    expect: 'db-tx-end = \"commit\"'\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: \"\"\n  - name: db-tx-end=commit\n    expect: 'db-tx-end = \"commit\"'\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: commit\n  - name: db-tx-end=commit-allow-override\n    expect: 'db-tx-end = \"commit-allow-override\"'\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: commit-allow-override\n  - name: db-tx-end=rollback-allow-override\n    expect: 'db-tx-end = \"rollback-allow-override\"'\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: rollback-allow-override\n  - name: db-tx-end=rollback\n    expect: 'db-tx-end = \"rollback\"'\n    use_defaultenv: true\n    env:\n      PGRST_DB_TX_END: rollback\n\nroleclaims:\n  - key: '.postgrest.a_role'\n    data:\n      postgrest:\n        a_role: postgrest_test_author\n      other: claims\n    expected_status: 200\n  - key: '.customObject.manyRoles[1]'\n    data:\n      customObject:\n        manyRoles:\n          - other\n          - postgrest_test_author\n      other: {}\n    expected_status: 200\n  - key: '.\"https://www.example.com/roles\"[0].value'\n    data:\n      'https://www.example.com/roles':\n        - value: postgrest_test_author\n      other: 666\n    expected_status: 200\n  - key: '.myDomain[3]'\n    data:\n      myDomain:\n        - other\n        - postgrest_test_author\n      other: 1.23\n    expected_status: 401\n  - key: '.myRole'\n    data:\n      role: postgrest_test_author\n      other: true\n    expected_status: 401\n  # https://github.com/PostgREST/postgrest/pull/3813\n  - key: '.realm_access.roles[?(@ == \"postgrest_test_author\")]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - postgrest_test_author\n    expected_status: 200\n  - key: '.realm_access.roles[?(@ != \"other\")]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - postgrest_test_author\n    expected_status: 200\n  - key: '.realm_access.roles[?(@ ^== \"postgrest_te\")]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - postgrest_test_author\n    expected_status: 200\n  - key: '.realm_access.roles[?(@ ==^ \"st_test_author\")]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - postgrest_test_author\n    expected_status: 200\n  - key: '.realm_access.roles[?(@ *== \"_test_\")]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - postgrest_test_author\n    expected_status: 200\n  - key: '.realm_access.roles[?(@ == \"string\")]'\n    data:\n      realm_access:\n        roles:\n          - obj_key: obj_value\n    expected_status: 401 # fails because it compares an object with a string\n  - key: '.realm_access.roles[0][7:]'\n    data:\n      realm_access:\n        roles:\n          - prefix_postgrest_test_author\n    expected_status: 200 # passes because it removes the \"prefix_\" part using slice\n  - key: '.realm_access.roles[0][:-7]'\n    data:\n      realm_access:\n        roles:\n          - postgrest_test_author_suffix\n    expected_status: 200 # passes because it removes the \"_suffix\" part using slice\n  - key: '.realm_access.roles[0][7:-7]'\n    data:\n      realm_access:\n        roles:\n          - prefix_postgrest_test_author_suffix\n    expected_status: 200 # passes because it removes the \"prefix_\" and \"_suffix\" part using slice\n  - key: '.realm_access.roles[0][:]'\n    data:\n      realm_access:\n        roles:\n          - postgrest_test_author\n    expected_status: 200 # passes because nothing gets sliced\n  - key: '.realm_access.roles[?(@ *== \"_test_\")][7:]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - prefix_postgrest_test_author\n    expected_status: 200 # passes on both comparison operators and slicing\n  - key: '.realm_access.roles[?(@ *== \"_test_\")][200:]'\n    data:\n      realm_access:\n        roles:\n          - other\n          - prefix_postgrest_test_author\n    expected_status: 401 # fails due to slicing results in empty string\n\njwtaudroleclaims:\n  - key: '.aud'\n    data:\n      aud: postgrest_test_author\n    expected_status: 200\n  - key: '.aud'\n    data:\n      aud: postgrest_test_invalid\n    expected_status: 401\n  - key: '.aud[0]'\n    data:\n      aud: [postgrest_test_author]\n    expected_status: 200\n  - key: '.aud[1]' # succeeds the aud claims check, but fail when hits the db\n    data:\n      aud: [postgrest_test_author, postgrest_test_invalid]\n    expected_status: 401\n\ninvalidroleclaimkeys:\n  - 'role.other'\n  - '.role##'\n  - '.my_role;;domain'\n  - '.#$$%&$%/'\n  - '1234'\n  - '.role[?(@ =)]'\n\ninvalidopenapimodes:\n  - 'follow-'\n  - 'ignore-'\n  - '.#$$%&$%/'\n\ninvalidjointypes:\n  - 'left!'\n  - 'right'\n  - '.#$$%&$%/'\n\nspecialhostvalues:\n  - '*4'\n  - '!4'\n  - '*6'\n  - '!6'\n  - '*'\n\nrestrictedschemas:\n  - 'pg_catalog'\n  - 'information_schema'\n"
  },
  {
    "path": "test/io/fixtures/load.sql",
    "content": "-- Load all IO tests fixtures for PostgREST\n\n\\set ON_ERROR_STOP on\n\n\\ir database.sql\n\\ir db_config.sql\n\\ir roles.sql\n\\ir schema.sql\n\\ir privileges.sql\n"
  },
  {
    "path": "test/io/fixtures/privileges.sql",
    "content": "GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;\nGRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;\n\nGRANT SELECT ON authors_only TO postgrest_test_author;\nGRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;\nGRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;\n\nGRANT ALL ON cats TO postgrest_test_anonymous;\nGRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;\n"
  },
  {
    "path": "test/io/fixtures/replica.sql",
    "content": "create schema replica;\n\ncreate or replace function replica.is_replica() returns bool as $$\n  select pg_is_in_recovery();\n$$ language sql;\n\ncreate or replace function replica.get_replica_slot() returns name as $$\n  select slot_name from pg_replication_slots limit 1;\n$$ language sql;\n\ncreate table replica.items as select x as id from generate_series(1, 10) x;\n\nDROP ROLE IF EXISTS postgrest_test_anonymous;\nCREATE ROLE postgrest_test_anonymous;\n\nGRANT postgrest_test_anonymous TO :PGUSER;\n\nGRANT USAGE ON SCHEMA replica TO postgrest_test_anonymous;\n\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA replica\nTO postgrest_test_anonymous;\n"
  },
  {
    "path": "test/io/fixtures/roles.sql",
    "content": "DROP ROLE IF EXISTS\n  postgrest_test_anonymous, postgrest_test_author,\n  postgrest_test_serializable, postgrest_test_repeatable_read,\n  postgrest_test_w_superuser_settings;\n\nCREATE ROLE postgrest_test_anonymous;\nCREATE ROLE postgrest_test_author;\nCREATE ROLE postgrest_test_serializable;\nCREATE ROLE postgrest_test_repeatable_read;\nCREATE ROLE postgrest_test_w_superuser_settings;\n\nGRANT\n  postgrest_test_anonymous, postgrest_test_author,\n  postgrest_test_serializable, postgrest_test_repeatable_read,\n  postgrest_test_w_superuser_settings TO :PGUSER;\n\nALTER ROLE :PGUSER SET pgrst.db_anon_role = 'postgrest_test_anonymous';\nALTER ROLE postgrest_test_serializable SET default_transaction_isolation = 'serializable';\nALTER ROLE postgrest_test_repeatable_read SET default_transaction_isolation = 'REPEATABLE READ';\n\nALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_statement = 1;\nALTER ROLE postgrest_test_w_superuser_settings SET log_min_messages = 'fatal';\n\nALTER ROLE postgrest_test_anonymous SET statement_timeout TO '5s';\nALTER ROLE postgrest_test_author SET statement_timeout TO '10s';\n"
  },
  {
    "path": "test/io/fixtures/schema.sql",
    "content": "DROP SCHEMA IF EXISTS v1, test;\n\nCREATE SCHEMA v1;\nCREATE SCHEMA test;\n\nCREATE TABLE authors_only ();\nCREATE TABLE projects AS SELECT FROM generate_series(1,5);\nCREATE TABLE cats(id uuid primary key, name text);\nCREATE TABLE items AS SELECT x AS id FROM generate_series(1,5) x;\n\n-- directors and films table can be used for resource embedding tests\nCREATE TABLE directors (\n  id int primary key,\n  name text\n);\n\nCREATE TABLE films (\n  id int primary key,\n  title text,\n  director_id int,\n\n  constraint fk_director\n    foreign key (director_id) references directors (id)\n    on update cascade\n    on delete cascade\n);\n\n-- data to test resource embedding\nTRUNCATE TABLE directors CASCADE;\nINSERT INTO directors\nVALUES (1, 'quentin tarantino'),\n       (2, 'christopher nolan'),\n       (3, 'yorgos lathinmos');\n\nTRUNCATE TABLE films CASCADE;\nINSERT INTO films\nVALUES (1, 'pulp fiction', 1),\n       (2, 'intersteller',2),\n       (3, 'dogtooth',3),\n       (4, 'reservoir dogs', 1);\n\nDO $do$BEGIN\n  IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN\n    ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;\n    GRANT SET ON PARAMETER log_min_duration_sample to postgrest_test_authenticator;\n  END IF;\nEND$do$;\n\ncreate function get_guc_value(name text) returns text as $$\n  select nullif(current_setting(name), '')::text;\n$$ language sql;\n\ncreate function v1.get_guc_value(name text) returns text as $$\n  select nullif(current_setting(name), '')::text;\n$$ language sql;\n\ncreate function uses_prepared_statements() returns bool as $$\n  select count(name) > 0 from pg_catalog.pg_prepared_statements\n$$ language sql;\n\ncreate function change_max_rows_config(val int, notify bool default false) returns void as $_$\nbegin\n  execute format($$\n    alter role postgrest_test_authenticator set pgrst.db_max_rows = %L;\n  $$, val);\n  if notify then\n    perform pg_notify('pgrst', 'reload config');\n  end if;\nend $_$ volatile security definer language plpgsql ;\n\ncreate function reset_max_rows_config() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator reset pgrst.db_max_rows;\nend $_$ volatile security definer language plpgsql ;\n\ncreate function change_db_schema_and_full_reload(schemas text) returns void as $_$\nbegin\n  execute format($$\n    alter role postgrest_test_authenticator set pgrst.db_schemas = %L;\n  $$, schemas);\n  perform pg_notify('pgrst', 'reload config');\n  perform pg_notify('pgrst', 'reload schema');\nend $_$ volatile security definer language plpgsql ;\n\ncreate function v1.reset_db_schema_config() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator reset pgrst.db_schemas;\n  perform pg_notify('pgrst', 'reload config');\n  perform pg_notify('pgrst', 'reload schema');\nend $_$ volatile security definer language plpgsql ;\n\ncreate function invalid_role_claim_key_reload() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = 'test';\n  perform pg_notify('pgrst', 'reload config');\nend $_$ volatile security definer language plpgsql ;\n\ncreate function notify_do_nothing() returns void as $_$\n  notify pgrst, 'nothing';\n$_$ language sql;\n\ncreate function do_nothing() returns void as $_$\n$_$ language sql;\n\ncreate function reset_invalid_role_claim_key() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator reset pgrst.jwt_role_claim_key;\n  perform pg_notify('pgrst', 'reload config');\nend $_$ volatile security definer language plpgsql ;\n\ncreate function reload_pgrst_config() returns void as $_$\nbegin\n  perform pg_notify('pgrst', 'reload config');\nend $_$ language plpgsql ;\n\ncreate or replace function sleep(seconds double precision) returns void as $$\n  select pg_sleep(seconds);\n$$ language sql;\n\ncreate or replace function hello() returns text as $$\n  select 'hello'::text;\n$$ language sql;\n\ncreate function drop_change_cats() returns void\nlanguage sql security definer\nas $$\n  drop table cats;\n  create table cats(id bigint primary key, name text);\n  grant all on table cats to postgrest_test_anonymous;\n  notify pgrst, 'reload schema';\n$$;\n\ncreate function change_role_statement_timeout(timeout text) returns void as $_$\nbegin\n  execute format($$\n    alter role current_user set statement_timeout = %L;\n  $$, timeout);\nend $_$ volatile language plpgsql ;\n\ncreate view items_w_isolation_level as\nselect\n  id,\n  current_setting('transaction_isolation', true) as isolation_level\nfrom items;\n\ncreate function default_isolation_level()\nreturns text as $$\n  select current_setting('transaction_isolation', true);\n$$\nlanguage sql;\n\ncreate function serializable_isolation_level()\nreturns text as $$\n  select current_setting('transaction_isolation', true);\n$$\nlanguage sql set default_transaction_isolation = 'serializable';\n\ncreate function repeatable_read_isolation_level()\nreturns text as $$\n  select current_setting('transaction_isolation', true);\n$$\nlanguage sql set default_transaction_isolation = 'REPEATABLE READ';\n\ncreate or replace function create_function() returns void as $_$\n  drop function if exists mult_them(int, int);\n  create or replace function mult_them(a int, b int) returns int as $$\n    select a*b;\n  $$ language sql;\n  notify pgrst, 'reload schema';\n$_$ language sql security definer;\n\ncreate or replace function migrate_function() returns void as $_$\n  drop function if exists mult_them(int, int);\n  create or replace function mult_them(c int, d int) returns int as $$\n    select c*d;\n  $$ language sql;\n  notify pgrst, 'reload schema';\n$_$ language sql security definer;\n\ncreate or replace function get_pgrst_version() returns text\n  language sql\nas $$\nselect application_name\nfrom pg_stat_activity\nwhere application_name ilike 'postgrest%'\nlimit 1;\n$$;\n\ncreate function terminate_pgrst(appname text) returns setof record as $$\nselect pg_terminate_backend(pid) from pg_stat_activity where application_name iLIKE '%' || appname || '%';\n$$ language sql security definer;\n\ncreate or replace function one_sec_timeout() returns void as $$\n  select pg_sleep(3);\n$$ language sql set statement_timeout = '1s';\n\ncreate or replace function four_sec_timeout() returns void as $$\n  select pg_sleep(3);\n$$ language sql set statement_timeout = '4s';\n\ncreate function get_postgres_version() returns int as $$\n  select current_setting('server_version_num')::int;\n$$ language sql;\n\ncreate or replace function rpc_work_mem() returns items as $$\n  select 1\n$$ language sql\nset work_mem = '6000';\n\ncreate or replace function rpc_with_one_hoisted() returns items as $$\n  select 1\n$$ language sql\nset work_mem = '3000'\nset statement_timeout = '7s';\n\ncreate or replace function rpc_with_two_hoisted() returns items as $$\n  select 1\n$$ language sql\nset work_mem = '5000'\nset statement_timeout = '10s';\n\ncreate function get_work_mem(items) returns text as $$\n  select current_setting('work_mem', true) as work_mem\n$$ language sql;\n\ncreate function get_statement_timeout(items) returns text as $$\n  select current_setting('statement_timeout', true) as statement_timeout\n$$ language sql;\n\ncreate function change_db_schemas_config() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator set pgrst.db_schemas = 'test';\nend $_$ volatile security definer language plpgsql;\n\ncreate function reset_db_schemas_config() returns void as $_$\nbegin\n  alter role postgrest_test_authenticator reset pgrst.db_schemas;\nend $_$ volatile security definer language plpgsql ;\n\ncreate function test.get_current_schema() returns text as $$\n  select current_schema()::text;\n$$ language sql;\n\ncreate or replace function root() returns json as $_$\n  select '{\"swagger\": \"2.0\"}'::json;\n$_$ language sql;\n\ncreate view infinite_recursion as\nselect * from projects;\n\ncreate or replace view infinite_recursion as\nselect * from infinite_recursion;\n\ncreate or replace function \"true\"() returns boolean as $_$\n  select true;\n$_$ language sql;\n\ncreate or replace function notify_pgrst() returns void as $$\n  notify pgrst;\n$$ language sql;\n\n\ncreate or replace function custom_vary_hdr() returns void as $$\n  begin\n    perform set_config('response.headers', '[{\"Vary\": \"X-Test-Accept\"}]', false);\n  end\n$$ language plpgsql;\n"
  },
  {
    "path": "test/io/postgrest.py",
    "content": "\"Fixtures to run PostgREST as a server.\"\n\nimport contextlib\nimport dataclasses\nimport os\nimport pathlib\nimport socket\nimport subprocess\nimport tempfile\nimport time\nimport urllib.parse\n\nimport requests\nimport requests_unixsocket\n\nfrom config import POSTGREST_BIN, hpctixfile\n\n\ndef sleep_until_postgrest_scache_reload():\n    \"Sleep until schema cache reload\"\n    time.sleep(0.3)\n\n\ndef sleep_until_postgrest_config_reload():\n    \"Sleep until config reload\"\n    time.sleep(0.2)\n\n\ndef sleep_until_postgrest_full_reload():\n    \"Sleep until schema cache plus config reload\"\n    time.sleep(0.3)\n\n\nclass PostgrestTimedOut(Exception):\n    \"Connecting to PostgREST endpoint timed out.\"\n\n\nclass PostgrestSession(requests_unixsocket.Session):\n    \"HTTP client session directed at a PostgREST endpoint.\"\n\n    def __init__(self, baseurl, *args, **kwargs):\n        super(PostgrestSession, self).__init__(*args, **kwargs)\n        self.baseurl = baseurl\n\n    def request(self, method, url, *args, **kwargs):\n        # Not using urllib.parse.urljoin to compose the url, as it doesn't play\n        # well with our 'http+unix://' unix domain socket urls.\n        fullurl = self.baseurl + url\n        return super(PostgrestSession, self).request(method, fullurl, *args, **kwargs)\n\n\n@dataclasses.dataclass\nclass PostgrestProcess:\n    \"Running PostgREST process and its corresponding main and admin endpoints.\"\n\n    admin: object\n    process: object\n    session: object\n    config: object\n\n    def read_stdout(self, nlines=1):\n        \"Wait for line(s) on standard output.\"\n        output = []\n        for _ in range(10):\n            self.process.stdout.flush()\n            line = self.process.stdout.readline()\n            if line:\n                output.append(line.decode())\n                if len(output) >= nlines:\n                    break\n            time.sleep(0.1)\n        return output\n\n    def wait_until_scache_starts_loading(self, max_seconds=1):\n        \"Wait for the admin /ready return a status of 503\"\n\n        wait_until_status_code(\n            self.admin.baseurl + \"/ready\", max_seconds=max_seconds, status_code=503\n        )\n\n\n@contextlib.contextmanager\ndef run(\n    configpath=None,\n    stdin=None,\n    env=None,\n    port=None,\n    host=None,\n    wait_for_readiness=True,\n    wait_max_seconds=1,\n    no_pool_connection_available=False,\n    no_startup_stdout=True,\n):\n    \"Run PostgREST and yield an endpoint that is ready for connections.\"\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n\n        # with python requests, \"localhost\" doesn't automatically resolves\n        # to [::1], hence we use this explicitly when host is ipv6 special\n        # address\n        ipv6_special_addresses = [\"*6\", \"!6\"]\n\n        localhost = \"[::1]\" if host in ipv6_special_addresses else \"localhost\"\n\n        if port:\n            env[\"PGRST_SERVER_PORT\"] = str(port)\n            env[\"PGRST_SERVER_HOST\"] = host or localhost\n            # When constructing IPv6 address, host address should be bracketed like [host]\n            apihost = f\"[{host}]\" if host and is_ipv6(host) else localhost\n            baseurl = f\"http://{apihost}:{port}\"\n        else:\n            socketfile = pathlib.Path(tmpdir) / \"postgrest.sock\"\n            env[\"PGRST_SERVER_UNIX_SOCKET\"] = str(socketfile)\n            baseurl = \"http+unix://\" + urllib.parse.quote_plus(str(socketfile))\n\n        adminport = freeport(used_ports=[port])\n        env[\"PGRST_ADMIN_SERVER_PORT\"] = str(adminport)\n        adminhost = f\"[{host}]\" if host and is_ipv6(host) else localhost\n        adminurl = f\"http://{adminhost}:{adminport}\"\n\n        command = [POSTGREST_BIN]\n        env[\"HPCTIXFILE\"] = hpctixfile()\n\n        if configpath:\n            command.append(configpath)\n\n        process = subprocess.Popen(\n            command,\n            stdin=subprocess.PIPE,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            env=env,\n        )\n\n        os.set_blocking(process.stdout.fileno(), False)\n\n        try:\n            process.stdin.write(stdin or b\"\")\n            process.stdin.close()\n\n            if wait_for_readiness:\n                wait_until_status_code(adminurl + \"/ready\", wait_max_seconds, 200)\n\n            if no_startup_stdout:\n                process.stdout.read()\n\n            if no_pool_connection_available:\n                sleep_pool_connection(baseurl, 10)\n\n            yield PostgrestProcess(\n                process=process,\n                session=PostgrestSession(baseurl),\n                admin=PostgrestSession(adminurl),\n                config=env,\n            )\n        finally:\n            remaining_output = process.stdout.read()\n            if remaining_output:\n                print(remaining_output.decode())\n            process.terminate()\n            try:\n                process.wait(timeout=1)\n            except subprocess.TimeoutExpired:\n                process.kill()\n                process.wait()\n\n\ndef freeport(used_ports=None):\n    \"Find an unused free port on localhost.\"\n    while True:\n        with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:\n            s.bind((\"\", 0))\n            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            port = s.getsockname()[1]\n            if used_ports is None or port not in used_ports:\n                return port\n\n\ndef wait_until_exit(postgrest):\n    \"Wait for PostgREST to exit, or times out\"\n    try:\n        return postgrest.process.wait(timeout=1)\n    except subprocess.TimeoutExpired:\n        raise PostgrestTimedOut()\n\n\ndef wait_until_status_code(url, max_seconds, status_code):\n    \"Wait for the given HTTP endpoint to return a status code\"\n    session = requests_unixsocket.Session()\n\n    for _ in range(max_seconds * 10):\n        try:\n            response = session.get(url, timeout=1)\n            if response.status_code == status_code:\n                return\n        except (requests.ConnectionError, requests.ReadTimeout):\n            pass\n\n        time.sleep(0.1)\n\n    if response:\n        raise PostgrestTimedOut(f\"{response.status_code}: {response.text}\")\n    else:\n        raise PostgrestTimedOut()\n\n\ndef sleep_pool_connection(url, seconds):\n    \"Sleep a pool connection by calling an RPC that uses pg_sleep\"\n    session = requests_unixsocket.Session()\n\n    # The try/except is a hack for not waiting for the response,\n    # taken from https://stackoverflow.com/a/45601591/4692662\n    try:\n        session.get(url + f\"/rpc/sleep?seconds={seconds}\", timeout=0.1)\n    except requests.exceptions.ReadTimeout:\n        pass\n\n\ndef is_ipv6(addr):\n    try:\n        socket.inet_pton(socket.AF_INET6, addr)\n        return True\n    except OSError:\n        return False\n\n\ndef set_statement_timeout(postgrest, role, milliseconds):\n    \"\"\"Set the statement timeout for the given role.\n    For this to work reliably with low previous timeout settings,\n    use a postgrest instance that doesn't use the affected role.\"\"\"\n\n    response = postgrest.session.post(\n        \"/rpc/set_statement_timeout\", data={\"role\": role, \"milliseconds\": milliseconds}\n    )\n    assert response.text == \"\"\n    assert response.status_code == 204\n\n\ndef reset_statement_timeout(postgrest, role):\n    \"Reset the statement timeout for the given role to the default 0 (no timeout)\"\n    set_statement_timeout(postgrest, role, 0)\n"
  },
  {
    "path": "test/io/secrets/ascii.b64",
    "content": "QUJDCkVhc3kgYXMKMTIzCk9yIHNpbXBsZSBhcwpEbyByZSBtaQ==\n"
  },
  {
    "path": "test/io/secrets/ascii.jwt",
    "content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.p__cvqgrtteKUhxrJwTwqxeuFmlEm3hEj1mk7yO15M4"
  },
  {
    "path": "test/io/secrets/ascii.noeol",
    "content": "ABC\nEasy as\n123\nOr simple as\nDo re mi"
  },
  {
    "path": "test/io/secrets/ascii.txt",
    "content": "ABC\nEasy as\n123\nOr simple as\nDo re mi\n"
  },
  {
    "path": "test/io/secrets/binary.b64",
    "content": "RTwSHLM0/PWM2YCOyBiyChMQQamZLTZGrXdzGk61o5A=\n"
  },
  {
    "path": "test/io/secrets/binary.eol",
    "content": "E<\u0012\u001c4ـ\u0018\n\u0013\u0010A-6Fws\u001aN\n"
  },
  {
    "path": "test/io/secrets/binary.jwt",
    "content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.zyXYJKXgVYaWqMYgUXMk4zqXSU5cwA_vK2z9_5lRlqA"
  },
  {
    "path": "test/io/secrets/binary.noeol",
    "content": "E<\u0012\u001c4ـ\u0018\n\u0013\u0010A-6Fws\u001aN"
  },
  {
    "path": "test/io/secrets/utf8.b64",
    "content": "4pqg77iPIOKaoO+4jiBVbmljb2RlIOKYoO+4jyDimKA=\n"
  },
  {
    "path": "test/io/secrets/utf8.jwt",
    "content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.VFfaoJohnDeJMA-awM6Y7blbOjXDNwmtx48Z-0bIKrE"
  },
  {
    "path": "test/io/secrets/utf8.noeol",
    "content": "⚠️ ⚠︎ Unicode ☠️ ☠"
  },
  {
    "path": "test/io/secrets/utf8.txt",
    "content": "⚠️ ⚠︎ Unicode ☠️ ☠\n"
  },
  {
    "path": "test/io/secrets/word.b64",
    "content": "QUJDRWFzeUFzT25lVHdvVGhyZWVPclNpbXBsZUFzRG9SZU1p\n"
  },
  {
    "path": "test/io/secrets/word.jwt",
    "content": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.1oyYyosD3_z4YzBE1DB-Pxf7wC0SCuZ2u1zLzPYU_nQ"
  },
  {
    "path": "test/io/secrets/word.noeol",
    "content": "ABCEasyAsOneTwoThreeOrSimpleAsDoReMi"
  },
  {
    "path": "test/io/secrets/word.txt",
    "content": "ABCEasyAsOneTwoThreeOrSimpleAsDoReMi\n"
  },
  {
    "path": "test/io/test_auth.py",
    "content": "\"Auth related IO tests for PostgREST\"\n\nfrom datetime import datetime, timedelta, timezone\nfrom operator import attrgetter\nimport signal\nimport time\nimport pytest\n\nfrom config import BASEDIR, CONFIGSDIR, FIXTURES, SECRET\nfrom util import authheader, jwtauthheader, parse_server_timings_header\nfrom postgrest import (\n    run,\n    sleep_until_postgrest_config_reload,\n    sleep_until_postgrest_scache_reload,\n    wait_until_exit,\n)\n\n\n@pytest.mark.parametrize(\n    \"secretpath\",\n    [path for path in (BASEDIR / \"secrets\").iterdir() if path.suffix != \".jwt\"],\n    ids=attrgetter(\"name\"),\n)\ndef test_read_secret_from_file(secretpath, defaultenv):\n    \"Authorization should succeed when the secret is read from a file.\"\n\n    env = {**defaultenv, \"PGRST_JWT_SECRET\": f\"@{secretpath}\"}\n\n    if secretpath.suffix == \".b64\":\n        env[\"PGRST_JWT_SECRET_IS_BASE64\"] = \"true\"\n\n    secret = secretpath.read_bytes()\n    headers = authheader(secretpath.with_suffix(\".jwt\").read_text())\n\n    with run(stdin=secret, env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        print(response.text)\n        assert response.status_code == 200\n\n\ndef test_read_secret_from_stdin(defaultenv):\n    \"Authorization should succeed when the secret is read from stdin.\"\n\n    env = {**defaultenv, \"PGRST_DB_CONFIG\": \"false\", \"PGRST_JWT_SECRET\": \"@/dev/stdin\"}\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    with run(stdin=SECRET.encode(), env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        print(response.text)\n        assert response.status_code == 200\n\n\n# TODO: This test would fail right now, because of\n# https://github.com/PostgREST/postgrest/issues/2126\n@pytest.mark.skip\ndef test_read_secret_from_stdin_dbconfig(defaultenv):\n    \"Authorization should succeed when the secret is read from stdin with db-config=true.\"\n\n    env = {**defaultenv, \"PGRST_DB_CONFIG\": \"true\", \"PGRST_JWT_SECRET\": \"@/dev/stdin\"}\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    with run(stdin=SECRET.encode(), env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        print(response.text)\n        assert response.status_code == 200\n\n\ndef test_jwt_errors(defaultenv):\n    \"invalid JWT should throw error\"\n\n    env = {**defaultenv, \"PGRST_JWT_SECRET\": SECRET, \"PGRST_JWT_AUD\": \"io tests\"}\n\n    def relativeSeconds(sec):\n        return int((datetime.now(timezone.utc) + timedelta(seconds=sec)).timestamp())\n\n    with run(env=env) as postgrest:\n        headers = jwtauthheader({}, \"other secret\")\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"No suitable key or wrong key type\"\n        assert (\n            response.json()[\"details\"] == \"None of the keys was able to decode the JWT\"\n        )\n\n        headers = jwtauthheader({\"role\": \"not_existing\"}, SECRET)\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == 'role \"not_existing\" does not exist'\n\n        # -35 seconds, because we allow clock skew of 30 seconds\n        headers = jwtauthheader({\"exp\": relativeSeconds(-35)}, SECRET)\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"JWT expired\"\n\n        # 35 seconds, because we allow clock skew of 30 seconds\n        headers = jwtauthheader({\"nbf\": relativeSeconds(35)}, SECRET)\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"JWT not yet valid\"\n\n        # 35 seconds, because we allow clock skew of 35 seconds\n        headers = jwtauthheader({\"iat\": relativeSeconds(35)}, SECRET)\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"JWT issued at future\"\n\n        headers = jwtauthheader({\"aud\": \"not set\"}, SECRET)\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"JWT not in audience\"\n\n        # partial token, no signature\n        headers = authheader(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bm90IGFuIG9iamVjdA\")\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"Expected 3 parts in JWT; got 2\"\n\n        # complete token but random characters\n        headers = authheader(\"quifquirndsjagnrgniur.fonvoienqhhdj.iuqvnvhojah\")\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"JWT cryptographic operation failed\"\n\n        # token with algorithm \"none\"\n        headers = authheader(\n            \"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.e30.yOBhlOIqn56T-4NvyEXCjfi3UmyQZ-BzXtePMO2NgRI\"\n        )\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"Wrong or unsupported encoding algorithm\"\n        assert (\n            response.json()[\"details\"]\n            == \"JWT is unsecured but expected 'alg' was not 'none'\"\n        )\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"true\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n\n    # for code coverage with cache enabled and server-timing enabled\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\")\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"permission denied for table authors_only\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"false\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n\n    # for code coverage with cache enabled and server-timing disabled\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\")\n        assert response.status_code == 401\n        assert response.json()[\"message\"] == \"permission denied for table authors_only\"\n\n\ndef test_fail_with_invalid_password(defaultenv):\n    \"Connecting with an invalid password should fail without retries.\"\n    uri = f'postgresql://?dbname={defaultenv[\"PGDATABASE\"]}&host={defaultenv[\"PGHOST\"]}&user=some_protected_user&password=invalid_pass'\n    env = {**defaultenv, \"PGRST_DB_URI\": uri}\n    with run(env=env, wait_for_readiness=False) as postgrest:\n        exitCode = wait_until_exit(postgrest)\n        assert exitCode == 1\n\n\n@pytest.mark.parametrize(\n    \"roleclaim\", FIXTURES[\"roleclaims\"], ids=lambda claim: claim[\"key\"]\n)\ndef test_role_claim_key(roleclaim, defaultenv):\n    \"Authorization should depend on a correct role-claim-key and JWT claim.\"\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_ROLE_CLAIM_KEY\": roleclaim[\"key\"],\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n    headers = jwtauthheader(roleclaim[\"data\"], SECRET)\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == roleclaim[\"expected_status\"]\n\n\n@pytest.mark.parametrize(\n    \"jwtaudroleclaim\",\n    FIXTURES[\"jwtaudroleclaims\"],\n    ids=lambda claim: claim[\"key\"] + \"_\" + str(claim[\"expected_status\"]),\n)\ndef test_jwt_aud_in_role_claim_key(jwtaudroleclaim, defaultenv):\n    \"Allows authorization with JWT aud claim in role-claim-key\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_AUD\": \"postgrest_test_author\",\n        \"PGRST_JWT_ROLE_CLAIM_KEY\": jwtaudroleclaim[\"key\"],\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n\n    headers = jwtauthheader(jwtaudroleclaim[\"data\"], SECRET)\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == jwtaudroleclaim[\"expected_status\"]\n\n\ndef test_iat_claim(defaultenv):\n    \"\"\"\n    A claim with an 'iat' (issued at) attribute should be successful.\n\n    The PostgREST time cache leads to issues here, see:\n    https://github.com/PostgREST/postgrest/issues/1139\n\n    \"\"\"\n\n    env = {**defaultenv, \"PGRST_JWT_SECRET\": SECRET}\n\n    claim = {\"role\": \"postgrest_test_author\", \"iat\": datetime.now(timezone.utc)}\n    headers = jwtauthheader(claim, SECRET)\n\n    with run(env=env) as postgrest:\n        for _ in range(10):\n            response = postgrest.session.get(\"/authors_only\", headers=headers)\n            assert response.status_code == 200\n\n            time.sleep(0.1)\n\n\ndef test_jwt_secret_reload(tmp_path, defaultenv):\n    \"JWT secret should be reloaded from file when PostgREST is sent SIGUSR2.\"\n    config = (CONFIGSDIR / \"sigusr2-settings.config\").read_text()\n    configfile = tmp_path / \"test.config\"\n    configfile.write_text(config)\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    with run(configfile, env=defaultenv) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 401\n\n        # change setting\n        configfile.write_text(config.replace(\"invalid\" * 5, SECRET))\n\n        # reload config\n        postgrest.process.send_signal(signal.SIGUSR2)\n\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 200\n\n\ndef test_jwt_secret_external_file_reload(tmp_path, defaultenv):\n    \"JWT secret external file should be reloaded when PostgREST is sent a SIGUSR2 or a NOTIFY.\"\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    external_secret_file = tmp_path / \"jwt-secret-config\"\n    external_secret_file.write_text(\"invalid\" * 5)\n\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_SECRET\": f\"@{external_secret_file}\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n        \"PGRST_DB_CONFIG\": \"false\",\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",  # required for NOTIFY\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 401\n\n        # change external file\n        external_secret_file.write_text(SECRET)\n\n        # SIGUSR1 doesn't reload external files, at least when db-config=false\n        postgrest.process.send_signal(signal.SIGUSR1)\n        sleep_until_postgrest_scache_reload()\n\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 401\n\n        # reload config and external file with SIGUSR2\n        postgrest.process.send_signal(signal.SIGUSR2)\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 200\n\n        # change external file to wrong value again\n        external_secret_file.write_text(\"invalid\" * 5)\n\n        # reload config and external file with NOTIFY\n        response = postgrest.session.post(\"/rpc/reload_pgrst_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 401\n\n\n# TODO: This test is more related to observability than authentication.\n#       So, move it an appropriate test module.\ndef test_jwt_cache_server_timing(defaultenv):\n    \"server-timing duration is exposed for JWT with expiry\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"true\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",\n        \"PGRST_JWT_SECRET\": SECRET,\n        \"PGRST_DB_CONFIG\": \"false\",\n    }\n\n    headers = jwtauthheader(\n        {\n            \"role\": \"postgrest_test_author\",\n            \"exp\": int(\n                (datetime.now(timezone.utc) + timedelta(minutes=30)).timestamp()\n            ),\n        },\n        SECRET,\n    )\n\n    with run(env=env) as postgrest:\n        first = postgrest.session.get(\"/authors_only\", headers=headers)\n        second = postgrest.session.get(\"/authors_only\", headers=headers)\n\n        assert first.status_code == 200\n        assert second.status_code == 200\n\n        first_dur = parse_server_timings_header(first.headers[\"Server-Timing\"])[\"jwt\"]\n        second_dur = parse_server_timings_header(second.headers[\"Server-Timing\"])[\"jwt\"]\n\n        # with jwt caching the parse time of second request with the same token\n        # should be at least as fast as the first one\n        assert second_dur <= first_dur\n\n\ndef test_jwt_cache_without_server_timing(defaultenv):\n    \"JWT cache does not break requests with server-timing disabled\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"false\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",\n        \"PGRST_JWT_SECRET\": SECRET,\n        \"PGRST_DB_CONFIG\": \"false\",\n    }\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    with run(env=env) as postgrest:\n        first = postgrest.session.get(\"/authors_only\", headers=headers)\n        second = postgrest.session.get(\"/authors_only\", headers=headers)\n\n        assert first.status_code == 200\n        assert second.status_code == 200\n\n\ndef test_jwt_cache_without_exp_claim(defaultenv):\n    \"server-timing duration is exposed for JWT without expiry\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"true\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",\n        \"PGRST_JWT_SECRET\": SECRET,\n        \"PGRST_DB_CONFIG\": \"false\",\n    }\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)  # no exp\n\n    with run(env=env) as postgrest:\n        first = postgrest.session.get(\"/authors_only\", headers=headers)\n        second = postgrest.session.get(\"/authors_only\", headers=headers)\n\n        assert first.status_code == 200\n        assert second.status_code == 200\n\n        first_dur = parse_server_timings_header(first.headers[\"Server-Timing\"])[\"jwt\"]\n        second_dur = parse_server_timings_header(second.headers[\"Server-Timing\"])[\"jwt\"]\n\n        assert first_dur >= 0\n        assert second_dur >= 0\n\n\ndef test_invalidate_jwt_cache_when_secret_changes(tmp_path, defaultenv):\n    \"JWT cache should be emptied after jwt-secret is changed in a config reload\"\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    external_secret_file = tmp_path / \"jwt-secret-config\"\n    external_secret_file.write_text(SECRET)\n\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_SECRET\": f\"@{external_secret_file}\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n        \"PGRST_JWT_CACHE_MAX_ENTRIES\": \"86400\",  # enable cache\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",  # required for NOTIFY\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 200  # jwt gets cached\n\n        # change external file\n        external_secret_file.write_text(\"invalid\" * 5)\n\n        # reload config and external file with NOTIFY\n        # jwt-cache should get empty\n        response = postgrest.session.post(\"/rpc/reload_pgrst_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n        sleep_until_postgrest_config_reload()\n\n        # now the request should fail because the cached token is removed\n        response = postgrest.session.get(\"/authors_only\", headers=headers)\n        assert response.status_code == 401\n"
  },
  {
    "path": "test/io/test_big_schema.py",
    "content": "\"IO tests for PostgREST started on the big schema.\"\n\nimport re\n\nimport pytest\nimport requests\n\nfrom postgrest import run\n\n\ndef test_schema_cache_load_max_duration(defaultenv):\n    \"schema cache load should not surpass a max_duration of elapsed milliseconds\"\n\n    max_duration = 500.0\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_SCHEMAS\": \"apflora\",\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",\n    }\n\n    with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:\n        log_lines = postgrest.read_stdout(nlines=50)\n        schema_cache_lines = [\n            line for line in log_lines if \"Schema cache loaded in\" in line\n        ]\n\n        match = re.search(\n            r\"Schema cache loaded in ([0-9]+(?:\\.[0-9])?) milliseconds\",\n            schema_cache_lines[-1],\n        )\n\n        assert match, f\"unexpected log format: {schema_cache_lines[-1]}\"\n        duration_ms = float(match.group(1))\n\n        # check that loading takes long enough\n        # to make sure we measure the time correctly\n        assert 100 < duration_ms < max_duration\n\n\n# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122\n# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with \"stack overflow\"\n# A stack size of 200K seems to be enough for succeess\n@pytest.mark.skip\ndef test_openapi_in_big_schema(defaultenv):\n    \"Should get a successful response from openapi on a big schema\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_SCHEMAS\": \"apflora\",\n        \"PGRST_OPENAPI_MODE\": \"ignore-privileges\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/\")\n        assert response.status_code == 200\n\n\ndef test_stackoverflow_is_logged(defaultenv):\n    \"Stack overflow errors should be logged with the Warp error message\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_SCHEMAS\": \"apflora\",\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",\n    }\n\n    with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:\n        with pytest.raises(requests.exceptions.ConnectionError):\n            postgrest.session.get(\"/\")\n\n        output = postgrest.read_stdout(nlines=10)\n        output.extend(postgrest.read_stdout(nlines=10))\n\n        assert any(\"Warp server error: stack overflow\" in line for line in output)\n\n\n# See: https://github.com/PostgREST/postgrest/issues/3329\ndef test_should_not_fail_with_stack_overflow(defaultenv):\n    \"requesting a non-existent relationship should not fail with stack overflow due to fuzzy search of candidates\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_SCHEMAS\": \"apflora\",\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",\n    }\n\n    with run(env=env, wait_max_seconds=30) as postgrest:\n        response = postgrest.session.get(\"/unknown-table?select=unknown-rel(*)\")\n        assert response.status_code == 404\n        data = response.json()\n        assert data[\"code\"] == \"PGRST205\"\n\n\ndef test_second_request_for_non_existent_table_should_be_quick(defaultenv):\n    \"requesting a non-existent relationship should be quick after the fuzzy search index is loaded (2nd request)\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_SCHEMAS\": \"fuzzysearch\",\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_DB_ANON_ROLE\": \"postgrest_test_anonymous\",\n    }\n\n    with run(env=env, wait_max_seconds=30) as postgrest:\n        response = postgrest.session.get(\"/unknown-table\")\n        assert response.status_code == 404\n        data = response.json()\n        assert data[\"code\"] == \"PGRST205\"\n        first_duration = response.elapsed.total_seconds()\n        response = postgrest.session.get(\"/unknown-table\")\n        assert response.elapsed.total_seconds() < first_duration / 2\n"
  },
  {
    "path": "test/io/test_cli.py",
    "content": "\"Unit tests for Input/Ouput of PostgREST seen as a black box.\"\n\nfrom operator import attrgetter\nimport signal\nimport subprocess\nimport pytest\nimport yaml\n\nfrom config import (\n    CONFIGSDIR,\n    FIXTURES,\n    POSTGREST_BIN,\n    get_admin_host_and_port_from_config,\n    hpctixfile,\n)\nfrom postgrest import freeport, is_ipv6, run, set_statement_timeout\n\n\nclass ExtraNewLinesDumper(yaml.SafeDumper):\n    \"Dumper that inserts an extra newline after each top-level item.\"\n\n    def write_line_break(self, data=None):\n        super().write_line_break(data)\n        if len(self.indents) == 1:\n            super().write_line_break()\n\n\ndef itemgetter(*items):\n    \"operator.itemgetter with None as fallback when key does not exist\"\n    if len(items) == 1:\n        item = items[0]\n\n        def g(obj):\n            return obj.get(item)\n\n    else:\n\n        def g(obj):\n            return tuple(obj.get(item) for item in items)\n\n    return g\n\n\nclass PostgrestError(Exception):\n    \"Postgrest exited unexpectedly.\"\n\n\ndef cli(args, env=None, stdin=None, expect_error=False):\n    \"Run PostgREST and return stdout or stderr.\"\n    env = env or {}\n\n    command = [POSTGREST_BIN] + args\n    env[\"HPCTIXFILE\"] = hpctixfile()\n\n    process = subprocess.Popen(\n        command,\n        env=env,\n        stdin=subprocess.PIPE,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n    )\n\n    process.stdin.write(stdin or b\"\")\n    try:\n        (stdout_output, stderr_output) = process.communicate(timeout=5)\n        if expect_error:  # When expected to fail, return stderr, else stdout\n            if process.returncode == 0:\n                raise PostgrestError(\n                    \"PostgREST unexpectedly exited with return code zero.\"\n                )\n            return stderr_output.decode()\n        else:\n            if process.returncode != 0:\n                raise PostgrestError(\n                    \"PostgREST unexpectedly exited with non-zero return code.\"\n                )\n            return stdout_output.decode()\n    finally:\n        process.kill()\n        process.wait()\n\n\ndef dumpconfig(configpath=None, env=None, stdin=None):\n    \"Dump the config as parsed by PostgREST.\"\n    args = [\"--dump-config\"]\n\n    if configpath:\n        args.append(configpath)\n\n    return cli(args, env=env, stdin=stdin)\n\n\n@pytest.mark.parametrize(\n    \"args,env,use_defaultenv,expect\",\n    map(itemgetter(\"args\", \"env\", \"use_defaultenv\", \"expect\"), FIXTURES[\"cli\"]),\n    ids=map(itemgetter(\"name\"), FIXTURES[\"cli\"]),\n)\ndef test_cli(args, env, use_defaultenv, expect, defaultenv):\n    \"\"\"\n    When PostgREST is run with <args> arguments and <env>/<defaultenv> environment variabales\n    it should return. Exit code should be according to <expect_error>.\n    \"\"\"\n    # use --dump-config by default to make sure that the postgrest process will terminate for sure\n    args = args or [\"--dump-config\"]\n\n    env = env or {}\n    if use_defaultenv:\n        env = {**defaultenv, **env}\n\n    if expect == \"error\":\n        with pytest.raises(PostgrestError):\n            print(cli(args, env=env))\n    else:\n        dump = cli(args, env=env).split(\"\\n\")\n        if expect:\n            assert expect in dump\n\n\ndef test_server_port_and_admin_port_same_value(defaultenv):\n    \"PostgREST should exit with an error message in output if server-port and admin-server-port are the same.\"\n    env = {**defaultenv, \"PGRST_SERVER_PORT\": \"3000\", \"PGRST_ADMIN_SERVER_PORT\": \"3000\"}\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert \"admin-server-port cannot be the same as server-port\" in error\n\n\n@pytest.mark.parametrize(\n    \"expectedconfig\",\n    [\n        expectedconfig\n        for expectedconfig in (CONFIGSDIR / \"expected\").iterdir()\n        if (CONFIGSDIR / expectedconfig.name).exists()\n    ],\n    ids=attrgetter(\"name\"),\n)\ndef test_expected_config(expectedconfig):\n    \"\"\"\n    Configs as dumped by PostgREST should match an expected output.\n\n    Used to test default values, config aliases and environment variables. The\n    expected output for each file in 'configs', if available, is found in the\n    'configs/expected' directory.\n\n    \"\"\"\n    expected = expectedconfig.read_text()\n    config = CONFIGSDIR / expectedconfig.name\n\n    assert dumpconfig(config) == expected\n\n\ndef test_expected_config_from_environment():\n    \"Config should be read directly from environment without config file.\"\n\n    envfile = (CONFIGSDIR / \"no-defaults-env.yaml\").read_text()\n    env = {k: str(v) for k, v in yaml.load(envfile, Loader=yaml.Loader).items()}\n\n    expected = (CONFIGSDIR / \"expected\" / \"no-defaults.config\").read_text()\n    assert dumpconfig(env=env) == expected\n\n\n@pytest.mark.parametrize(\n    \"role, expectedconfig\",\n    [\n        (\"db_config_authenticator\", \"no-defaults-with-db.config\"),\n        (\"other_authenticator\", \"no-defaults-with-db-other-authenticator.config\"),\n    ],\n)\ndef test_expected_config_from_db_settings(baseenv, role, expectedconfig):\n    \"Config should be overriden from database settings\"\n\n    config = CONFIGSDIR / \"no-defaults.config\"\n\n    env = {\n        **baseenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_URI\": \"postgresql://\",\n        \"PGRST_DB_CONFIG\": \"true\",\n    }\n\n    expected = (CONFIGSDIR / \"expected\" / expectedconfig).read_text()\n    assert dumpconfig(configpath=config, env=env) == expected\n\n\n@pytest.mark.parametrize(\n    \"config\",\n    [conf for conf in CONFIGSDIR.iterdir() if conf.suffix == \".config\"],\n    ids=attrgetter(\"name\"),\n)\ndef test_stable_config(tmp_path, config, defaultenv):\n    \"\"\"\n    A dumped, re-read and re-dumped config should match the dumped config.\n\n    Note: only dump vs. re-dump must be equal, as the original config file might\n    be different because of default values, whitespace, and quoting.\n\n    \"\"\"\n\n    # Set environment variables that some of the configs expect. Using a\n    # complex ROLE_CLAIM_KEY to make sure quoting works.\n    env = {\n        **defaultenv,\n        \"ROLE_CLAIM_KEY\": '.\"https://www.example.com/roles\"[0].value',\n        \"POSTGREST_TEST_SOCKET\": \"/tmp/postgrest.sock\",\n        \"POSTGREST_TEST_PORT\": \"80\",\n        \"JWT_SECRET_FILE\": \"a_file\",\n    }\n\n    # Some configs expect input from stdin, at least on base64.\n    stdin = b\"Y29ubmVjdGlvbl9zdHJpbmc=\"\n\n    dumped = dumpconfig(config, env=env, stdin=stdin)\n\n    tmpconfigpath = tmp_path / \"config\"\n    tmpconfigpath.write_text(dumped)\n    redumped = dumpconfig(tmpconfigpath, env=env)\n\n    assert dumped == redumped\n\n\n@pytest.mark.parametrize(\"invalidroleclaimkey\", FIXTURES[\"invalidroleclaimkeys\"])\ndef test_invalid_role_claim_key(invalidroleclaimkey, defaultenv):\n    \"Given an invalid role-claim-key, Postgrest should exit with a non-zero exit code.\"\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_ROLE_CLAIM_KEY\": invalidroleclaimkey,\n    }\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert f\"failed to parse role-claim-key value ({invalidroleclaimkey})\" in error\n\n\n@pytest.mark.parametrize(\"invalidopenapimodes\", FIXTURES[\"invalidopenapimodes\"])\ndef test_invalid_openapi_mode(invalidopenapimodes, defaultenv):\n    \"Given an invalid openapi-mode, Postgrest should exit with a non-zero exit code.\"\n    env = {\n        **defaultenv,\n        \"PGRST_OPENAPI_MODE\": invalidopenapimodes,\n    }\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert \"Invalid openapi-mode. Check your configuration.\" in error\n\n\n# If this test is failing, run postgrest-test-io --snapshot-update -k test_schema_cache_snapshot\n@pytest.mark.parametrize(\n    \"key\",\n    [\n        \"dbMediaHandlers\",\n        \"dbRelationships\",\n        \"dbRepresentations\",\n        \"dbRoutines\",\n        \"dbTables\",\n        \"dbTimezones\",\n    ],\n)\ndef test_schema_cache_snapshot(baseenv, key, snapshot_yaml):\n    \"Dump of schema cache should match snapshot.\"\n\n    schema_cache = yaml.load(cli([\"--dump-schema\"], env=baseenv), Loader=yaml.Loader)\n    formatted = yaml.dump(\n        schema_cache[key],\n        encoding=\"utf8\",\n        allow_unicode=True,\n        Dumper=yaml.SafeDumper if key == \"dbTimezones\" else ExtraNewLinesDumper,\n    )\n    assert formatted == snapshot_yaml\n\n\ndef test_jwt_aud_config_set_to_invalid_uri(defaultenv):\n    \"PostgREST should exit with an error message in output if jwt-aud config is set to an invalid URI\"\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_AUD\": \"foo://%%$$^^.com\",\n    }\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert \"jwt-aud should be a string or a valid URI\" in error\n\n\ndef test_jwt_secret_min_length(defaultenv):\n    \"Should log error and not load the config when the secret is shorter than the minimum admitted length\"\n\n    env = {**defaultenv, \"PGRST_JWT_SECRET\": \"short_secret\"}\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert \"The JWT secret must be at least 32 characters long.\" in error\n\n\ndef test_invalid_client_error_verbosity(defaultenv):\n    \"Given an invalid value for client-error-verbosity, Postgrest should exit with a non-zero exit code.\"\n    env = {\n        **defaultenv,\n        \"PGRST_CLIENT_ERROR_VERBOSITY\": \"invalid\",\n    }\n\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert \"Invalid client-error-verbosity. Check your configuration.\" in error\n\n\n@pytest.mark.parametrize(\"restricted_schema\", FIXTURES[\"restrictedschemas\"])\ndef test_restricted_db_schemas(restricted_schema, defaultenv):\n    \"Should print error when db-schemas config contain pg_catalog or information_schema\"\n\n    # test when single schema is given in db-schemas\n    env = {**defaultenv, \"PGRST_DB_SCHEMAS\": restricted_schema}\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert f\"db-schemas does not allow schema: '{restricted_schema}'\" in error\n\n    # test when multiple schemas are given in db-schemas\n    env = {**defaultenv, \"PGRST_DB_SCHEMAS\": f\"public, {restricted_schema}\"}\n    error = cli([\"--dump-config\"], env=env, expect_error=True)\n    assert f\"db-schemas does not allow schema: '{restricted_schema}'\" in error\n\n\n# TODO: Improve readability of \"--ready\" healthcheck tests\n@pytest.mark.parametrize(\"host\", [\"127.0.0.1\", \"::1\"], ids=[\"IPv4\", \"IPv6\"])\ndef test_cli_ready_flag_success(host, defaultenv):\n    \"test PostgREST ready flag succeeds when ready\"\n\n    port = freeport()\n\n    with run(env=defaultenv, host=host, port=port) as postgrest:\n        output = cli([\"--ready\"], env=postgrest.config)\n\n        (admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)\n\n        if is_ipv6(host):\n            assert f\"OK: http://[{admin_host}]:{admin_port}/ready\" in output\n        else:\n            assert f\"OK: http://{admin_host}:{admin_port}/ready\" in output\n\n\ndef test_cli_ready_flag_fail_when_schema_cache_not_loaded(defaultenv, metapostgrest):\n    \"test PosgREST ready flag fail when schema cache not loaded\"\n\n    role = \"timeout_authenticator\"\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"500\",\n    }\n\n    port = freeport()\n\n    with run(env=env, port=port) as postgrest:\n        # The schema cache query takes at least 500ms, due to PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP above.\n        # Make it impossible to load the schema cache, by setting statement timeout to 400ms.\n        set_statement_timeout(metapostgrest, role, 400)\n\n        # force a reconnection so the new role setting is picked up\n        postgrest.process.send_signal(signal.SIGUSR1)\n\n        postgrest.wait_until_scache_starts_loading()\n\n        output = cli([\"--ready\"], env=postgrest.config, expect_error=True)\n        (admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)\n\n        assert f\"ERROR: http://{admin_host}:{admin_port}/ready\" in output\n\n\ndef test_cli_ready_flag_fail_with_http_exception(defaultenv):\n    \"test PostgREST ready flag fail when http exception occurs\"\n\n    port = freeport()\n\n    # when healthcheck process sends the request to a wrong endpoint\n    with run(env=defaultenv, port=port) as postgrest:\n        # we set it to some freeport where server and admin server is not running\n        admin_port = int(postgrest.config[\"PGRST_ADMIN_SERVER_PORT\"])\n        used_ports = [port, admin_port]\n\n        postgrest.config[\"PGRST_ADMIN_SERVER_PORT\"] = str(freeport(used_ports))\n        output = cli([\"--ready\"], env=postgrest.config, expect_error=True)\n        (admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)\n\n        assert (\n            f\"ERROR: connection refused to http://{admin_host}:{admin_port}/ready\"\n            in output\n        )\n\n    # When client sends the request to invalid URL\n    with run(env=defaultenv, port=port) as postgrest:\n        postgrest.config[\"PGRST_ADMIN_SERVER_PORT\"] = str(-1)\n        output = cli([\"--ready\"], env=postgrest.config, expect_error=True)\n        (admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)\n\n        assert f\"ERROR: invalid url - http://{admin_host}:{admin_port}/ready\" in output\n\n\ndef test_cli_ready_flag_fail_with_special_hostname(defaultenv):\n    \"test PostgREST ready flag fail when http exception occurs\"\n\n    port = freeport()\n    host = \"*4\"\n\n    with run(env=defaultenv, host=host, port=port) as postgrest:\n        output = cli([\"--ready\"], env=postgrest.config, expect_error=True)\n\n        assert (\n            f'ERROR: The `--ready` flag cannot be used when server-host is configured as \"{host}\". Please update your server-host config to \"localhost\".'\n            in output\n        )\n\n\ndef test_cli_ready_flag_fail_when_no_admin_server(defaultenv):\n    \"test PostgREST ready flag fail when admin server not running\"\n\n    with run(env=defaultenv) as postgrest:\n        # We set admin-server-port to <empty> to disable admin server\n        postgrest.config[\"PGRST_ADMIN_SERVER_PORT\"] = \"\"\n        output = cli([\"--ready\"], env=postgrest.config, expect_error=True)\n\n        assert (\n            \"ERROR: Admin server is not running. Please check admin-server-port config.\"\n            in output\n        )\n"
  },
  {
    "path": "test/io/test_io.py",
    "content": "\"Unit tests for Input/Ouput of PostgREST seen as a black box.\"\n\nimport os\nimport re\nimport signal\nimport time\nimport pytest\n\nfrom config import CONFIGSDIR, FIXTURES, SECRET\nfrom util import Thread, jwtauthheader, parse_server_timings_header\nfrom postgrest import (\n    freeport,\n    is_ipv6,\n    reset_statement_timeout,\n    run,\n    set_statement_timeout,\n    sleep_until_postgrest_config_reload,\n    sleep_until_postgrest_full_reload,\n    sleep_until_postgrest_scache_reload,\n    wait_until_exit,\n)\n\n\ndef test_connect_with_dburi(dburi, defaultenv):\n    \"Connecting with db-uri instead of LIPQ* environment variables should work.\"\n    defaultenv_without_libpq = {\n        key: value\n        for key, value in defaultenv.items()\n        if key not in [\"PGDATABASE\", \"PGHOST\", \"PGUSER\"]\n    }\n    env = {**defaultenv_without_libpq, \"PGRST_DB_URI\": dburi.decode()}\n    with run(env=env):\n        pass\n\n\ndef test_read_dburi_from_stdin_without_eol(dburi, defaultenv):\n    \"Reading the dburi from stdin with a single line should work.\"\n    defaultenv_without_libpq = {\n        key: value\n        for key, value in defaultenv.items()\n        if key not in [\"PGDATABASE\", \"PGHOST\", \"PGUSER\"]\n    }\n    env = {**defaultenv_without_libpq, \"PGRST_DB_URI\": \"@/dev/stdin\"}\n\n    with run(env=env, stdin=dburi):\n        pass\n\n\ndef test_read_dburi_from_stdin_with_eol(dburi, defaultenv):\n    \"Reading the dburi from stdin containing a newline should work.\"\n    defaultenv_without_libpq = {\n        key: value\n        for key, value in defaultenv.items()\n        if key not in [\"PGDATABASE\", \"PGHOST\", \"PGUSER\"]\n    }\n    env = {**defaultenv_without_libpq, \"PGRST_DB_URI\": \"@/dev/stdin\"}\n\n    with run(env=env, stdin=dburi + b\"\\n\"):\n        pass\n\n\ndef test_app_settings_flush_pool(defaultenv):\n    \"\"\"\n    App settings should not reset when the db pool is flushed.\n\n    See: https://github.com/PostgREST/postgrest/issues/1141\n\n    \"\"\"\n\n    env = {**defaultenv, \"PGRST_APP_SETTINGS_EXTERNAL_API_SECRET\": \"0123456789abcdef\"}\n\n    with run(env=env) as postgrest:\n        uri = \"/rpc/get_guc_value?name=app.settings.external_api_secret\"\n        response = postgrest.session.get(uri)\n        assert response.text == '\"0123456789abcdef\"'\n\n        # SIGUSR1 causes the postgres connection pool to be flushed\n        postgrest.process.send_signal(signal.SIGUSR1)\n        sleep_until_postgrest_scache_reload()\n\n        uri = \"/rpc/get_guc_value?name=app.settings.external_api_secret\"\n        response = postgrest.session.get(uri)\n        assert response.text == '\"0123456789abcdef\"'\n\n\ndef test_flush_pool_no_interrupt(defaultenv):\n    \"Flushing the pool via SIGUSR1 doesn't interrupt ongoing requests\"\n\n    with run(env=defaultenv) as postgrest:\n\n        def sleep():\n            response = postgrest.session.get(\"/rpc/sleep?seconds=0.5\")\n            assert response.text == \"\"\n            assert response.status_code == 204\n\n        t = Thread(target=sleep)\n        t.start()\n\n        # make sure the request has started\n        time.sleep(0.1)\n\n        # SIGUSR1 causes the postgres connection pool to be flushed\n        postgrest.process.send_signal(signal.SIGUSR1)\n\n        t.join()\n\n\n@pytest.mark.xfail(reason=\"Graceful shutdown is currently failing\", strict=True)\ndef test_graceful_shutdown_waits_for_in_flight_request(defaultenv):\n    \"SIGTERM should allow in-flight requests to finish before exiting\"\n\n    with run(env=defaultenv, wait_max_seconds=5) as postgrest:\n\n        def sleep():\n            response = postgrest.session.get(\"/rpc/sleep?seconds=3\", timeout=10)\n            assert response.text == \"\"\n            assert response.status_code == 204\n\n        t = Thread(target=sleep)\n        t.start()\n\n        # Wait for the request to be in-flight before shutting down.\n        time.sleep(1)\n\n        postgrest.process.terminate()\n\n        t.join()\n\n\ndef test_random_port_bound(defaultenv):\n    \"PostgREST should bind to a random port when PGRST_SERVER_PORT is 0.\"\n\n    with run(env=defaultenv, port=\"0\"):\n        assert True  # liveness check is done by run(), so we just need to check that it doesn't fail\n\n\ndef test_app_settings_reload(tmp_path, defaultenv):\n    \"App settings should be reloaded from file when PostgREST is sent SIGUSR2.\"\n    config = (CONFIGSDIR / \"sigusr2-settings.config\").read_text()\n    configfile = tmp_path / \"test.config\"\n    configfile.write_text(config)\n    uri = \"/rpc/get_guc_value?name=app.settings.name_var\"\n\n    with run(configfile, env=defaultenv) as postgrest:\n        response = postgrest.session.get(uri)\n        assert response.text == '\"John\"'\n\n        # change setting\n        configfile.write_text(config.replace(\"John\", \"Jane\"))\n        # reload\n        postgrest.process.send_signal(signal.SIGUSR2)\n\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.get(uri)\n        assert response.text == '\"Jane\"'\n\n\ndef test_db_schema_reload(tmp_path, defaultenv):\n    \"DB schema should be reloaded from file when PostgREST is sent SIGUSR2.\"\n    config = (CONFIGSDIR / \"sigusr2-settings.config\").read_text()\n    configfile = tmp_path / \"test.config\"\n    configfile.write_text(config)\n\n    with run(configfile, env=defaultenv) as postgrest:\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=search_path\")\n        assert response.text == '\"\\\\\"public\\\\\", \\\\\"public\\\\\"\"'\n\n        # change setting\n        configfile.write_text(\n            config.replace('db-schemas = \"public\"', 'db-schemas = \"v1\"')\n        )\n\n        # reload config\n        postgrest.process.send_signal(signal.SIGUSR2)\n        sleep_until_postgrest_config_reload()\n\n        # reload schema cache to verify that the config reload actually happened\n        postgrest.process.send_signal(signal.SIGUSR1)\n        sleep_until_postgrest_scache_reload()\n\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=search_path\")\n        assert response.text == '\"\\\\\"v1\\\\\", \\\\\"public\\\\\"\"'\n\n\ndef test_db_schema_notify_reload(defaultenv):\n    \"DB schema and config should be reloaded when PostgREST is sent a NOTIFY\"\n\n    env = {**defaultenv, \"PGRST_DB_CONFIG\": \"true\", \"PGRST_DB_CHANNEL_ENABLED\": \"true\"}\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=search_path\")\n        assert response.text == '\"\\\\\"public\\\\\", \\\\\"public\\\\\"\"'\n\n        # change db-schemas config on the db and reload config and cache with notify\n        postgrest.session.post(\n            \"/rpc/change_db_schema_and_full_reload\", data={\"schemas\": \"v1\"}\n        )\n\n        sleep_until_postgrest_full_reload()\n\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=search_path\")\n        assert response.text == '\"\\\\\"v1\\\\\", \\\\\"public\\\\\"\"'\n\n        # reset db-schemas config on the db\n        response = postgrest.session.post(\"/rpc/reset_db_schema_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n\ndef test_max_rows_reload(defaultenv):\n    \"max-rows should be reloaded from role settings when PostgREST receives a SIGUSR2.\"\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CONFIG\": \"true\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.head(\"/projects\")\n        assert response.status_code == 200\n        assert response.headers[\"Content-Range\"] == \"0-4/*\"\n\n        # change max-rows config on the db\n        postgrest.session.post(\"/rpc/change_max_rows_config\", data={\"val\": 1})\n\n        # reload config\n        postgrest.process.send_signal(signal.SIGUSR2)\n\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.head(\"/projects\")\n        assert response.status_code == 200\n        assert response.headers[\"Content-Range\"] == \"0-0/*\"\n\n        # reset max-rows config on the db\n        response = postgrest.session.post(\"/rpc/reset_max_rows_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n\ndef test_max_rows_notify_reload(defaultenv):\n    \"max-rows should be reloaded from role settings when PostgREST receives a NOTIFY\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CONFIG\": \"true\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.head(\"/projects\")\n        assert response.status_code == 200\n        assert response.headers[\"Content-Range\"] == \"0-4/*\"\n\n        # change max-rows config on the db and reload with notify\n        postgrest.session.post(\n            \"/rpc/change_max_rows_config\", data={\"val\": 1, \"notify\": True}\n        )\n\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.head(\"/projects\")\n        assert response.status_code == 200\n        assert response.headers[\"Content-Range\"] == \"0-0/*\"\n\n        # reset max-rows config on the db\n        response = postgrest.session.post(\"/rpc/reset_max_rows_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n\ndef test_invalid_role_claim_key_notify_reload(defaultenv):\n    \"NOTIFY reload config should show an error if role-claim-key is invalid\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CONFIG\": \"true\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n        \"PGRST_LOG_LEVEL\": \"crit\",\n    }\n\n    with run(env=env) as postgrest:\n        postgrest.session.post(\"/rpc/invalid_role_claim_key_reload\")\n\n        output = postgrest.read_stdout()\n        assert 'Received a config reload message on the \"pgrst\" channel' in output[0]\n        output = postgrest.read_stdout()\n        assert \"failed to parse role-claim-key value\" in output[0]\n\n        response = postgrest.session.post(\"/rpc/reset_invalid_role_claim_key\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n\ndef test_notify_do_nothing(defaultenv):\n    \"NOTIFY with unknown message should do nothing\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CONFIG\": \"true\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n        \"PGRST_LOG_LEVEL\": \"crit\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.post(\"/rpc/notify_do_nothing\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        output = postgrest.read_stdout()\n        assert output == []\n\n\ndef test_db_prepared_statements_enable(defaultenv):\n    \"Should use prepared statements when the setting is enabled.\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.post(\"/rpc/uses_prepared_statements\")\n        assert response.text == \"true\"\n\n\ndef test_db_prepared_statements_disable(defaultenv):\n    \"Should not use any prepared statements when the setting is disabled.\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_PREPARED_STATEMENTS\": \"false\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.post(\"/rpc/uses_prepared_statements\")\n        assert response.text == \"false\"\n\n\ndef test_statement_timeout(defaultenv, metapostgrest):\n    \"Statement timeout times out slow statements\"\n\n    role = \"timeout_authenticator\"\n    set_statement_timeout(metapostgrest, role, 1000)  # 1 second\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/sleep?seconds=0.5\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        response = postgrest.session.get(\"/rpc/sleep?seconds=2\")\n        assert response.status_code == 500\n        data = response.json()\n        assert data[\"message\"] == \"canceling statement due to statement timeout\"\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_change_statement_timeout(defaultenv, metapostgrest):\n    \"Statement timeout changes take effect immediately\"\n\n    role = \"timeout_authenticator\"\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n    }\n\n    with run(env=env) as postgrest:\n        # no limit initially\n        response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        set_statement_timeout(metapostgrest, role, 500)  # 0.5s\n\n        # trigger schema refresh\n        postgrest.process.send_signal(signal.SIGUSR1)\n        sleep_until_postgrest_scache_reload()\n\n        response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n        assert response.status_code == 500\n        data = response.json()\n        assert data[\"message\"] == \"canceling statement due to statement timeout\"\n\n        set_statement_timeout(metapostgrest, role, 2000)  # 2s\n\n        # trigger role setting refresh\n        postgrest.process.send_signal(signal.SIGUSR1)\n        sleep_until_postgrest_scache_reload()\n\n        response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_pool_size(defaultenv, metapostgrest):\n    \"Verify that PGRST_DB_POOL setting allows the correct number of parallel requests\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_POOL\": \"2\",\n    }\n\n    with run(env=env) as postgrest:\n        start = time.time()\n        threads = []\n        for i in range(4):\n\n            def sleep(i=i):\n                response = postgrest.session.get(\"/rpc/sleep?seconds=0.5\")\n                assert response.text == \"\"\n                assert response.status_code == 204, \"thread {}\".format(i)\n\n            t = Thread(target=sleep)\n            t.start()\n            threads.append(t)\n        for t in threads:\n            t.join()\n        end = time.time()\n        delta = end - start\n\n        # sleep 4 times for 0.5s each, with 2 requests in parallel\n        # => total time roughly 1s\n        assert delta > 1 and delta < 1.5\n\n\n@pytest.mark.parametrize(\"level\", [\"crit\", \"error\", \"warn\", \"info\", \"debug\"])\ndef test_pool_acquisition_timeout(level, defaultenv, metapostgrest):\n    \"Verify that PGRST_DB_POOL_ACQUISITION_TIMEOUT times out when the pool is empty\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_POOL\": \"1\",\n        \"PGRST_DB_POOL_ACQUISITION_TIMEOUT\": \"1\",  # 1 second\n        \"PGRST_LOG_LEVEL\": level,\n    }\n\n    with run(env=env, no_pool_connection_available=True) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 504\n        data = response.json()\n        assert data[\"message\"] == \"Timed out acquiring connection from connection pool.\"\n\n        # ensure the message appears on the logs as well\n        output = sorted(postgrest.read_stdout(nlines=10))\n\n        if level == \"crit\":\n            assert len(output) == 0\n        else:\n            assert any(\" 504 \" in line for line in output)\n            assert any(\n                \"Timed out acquiring connection from connection pool.\" in line\n                for line in output\n            )\n\n\ndef test_change_statement_timeout_held_connection(defaultenv, metapostgrest):\n    \"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration\"\n\n    role = \"timeout_authenticator\"\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_DB_POOL\": \"2\",\n    }\n\n    with run(env=env) as postgrest:\n        # start a slow request that holds a pool connection\n        def hold_connection():\n            response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n            assert response.text == \"\"\n            assert response.status_code == 204\n\n        hold = Thread(target=hold_connection)\n        hold.start()\n        # give the request time to start before SIGUSR1 flushes the pool\n        time.sleep(0.1)\n\n        set_statement_timeout(metapostgrest, role, 500)  # 0.5s\n        # trigger schema refresh; flushes pool and establishes a new connection\n        postgrest.process.send_signal(signal.SIGUSR1)\n\n        # wait for the slow request's connection to be returned to the pool\n        hold.join()\n\n        # subsequent requests should fail due to the lowered timeout; run several in parallel\n        # to ensure we use the full pool\n        threads = []\n        for i in range(2):\n\n            def sleep(i=i):\n                response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n                assert response.status_code == 500, \"thread {}\".format(i)\n                data = response.json()\n                assert data[\"message\"] == \"canceling statement due to statement timeout\"\n\n            thread = Thread(target=sleep)\n            thread.start()\n            threads.append(thread)\n\n        for t in threads:\n            t.join()\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_admin_schema_cache(defaultenv):\n    \"Should get a success response from the admin server containing current schema cache\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.admin.get(\"/schema_cache\")\n        assert response.status_code == 200\n        assert '\"dbTables\":[[{\"qiName\":\"authors_only\"' in response.text\n\n\ndef test_admin_ready_w_channel(defaultenv):\n    \"Should get a success response from the admin server ready endpoint when the LISTEN channel is enabled\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.admin.get(\"/ready\")\n        assert response.status_code == 200\n\n\ndef test_admin_ready_wo_channel(defaultenv):\n    \"Should get a success response from the admin server ready endpoint when the LISTEN channel is disabled\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CHANNEL_ENABLED\": \"false\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.admin.get(\"/ready\")\n        assert response.status_code == 200\n\n\ndef test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest):\n    \"Should get a failed response from the admin server ready endpoint when the schema cache is not loaded\"\n\n    role = \"timeout_authenticator\"\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"500\",\n    }\n\n    with run(env=env) as postgrest:\n        # The schema cache query takes at least 500ms, due to PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP above.\n        # Make it impossible to load the schema cache, by setting statement timeout to 400ms.\n        set_statement_timeout(metapostgrest, role, 400)\n\n        # force a reconnection so the new role setting is picked up\n        postgrest.process.send_signal(signal.SIGUSR1)\n\n        postgrest.wait_until_scache_starts_loading()\n\n        response = postgrest.admin.get(\"/ready\", timeout=1)\n        assert response.status_code == 503\n\n        response = postgrest.session.get(\"/projects\", timeout=1)\n        assert response.status_code == 503\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_metrics_include_schema_cache_fails(defaultenv, metapostgrest):\n    \"Should get shema cache fails from the metrics endpoint\"\n\n    role = \"timeout_authenticator\"\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"50\",\n    }\n\n    with run(env=env) as postgrest:\n        # The schema cache query takes at least 20ms, due to PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP above.\n        # Make it impossible to load the schema cache, by setting statement timeout to 100ms.\n        set_statement_timeout(metapostgrest, role, 20)\n\n        # force a reconnection so the new role setting is picked up\n        postgrest.process.send_signal(signal.SIGUSR1)\n\n        # wait for some schema cache retries\n        time.sleep(1)\n\n        response = postgrest.admin.get(\"/ready\", timeout=1)\n        assert response.status_code == 503\n\n        response = postgrest.admin.get(\"/metrics\", timeout=1)\n        assert response.status_code == 200\n\n        metrics = float(\n            re.search(\n                r'pgrst_schema_cache_loads_total{status=\"FAIL\"} (\\d+)', response.text\n            ).group(1)\n        )\n        assert metrics == 1.0\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_admin_not_found(defaultenv):\n    \"Should get a not found from a undefined endpoint on the admin server\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.admin.get(\"/notfound\")\n        assert response.status_code == 404\n\n\ndef test_admin_ready_dependent_on_main_app(defaultenv):\n    \"Should get a failure from the admin ready endpoint if the main app also fails\"\n\n    with run(env=defaultenv) as postgrest:\n        # delete the unix socket to make the main app fail\n        os.remove(defaultenv[\"PGRST_SERVER_UNIX_SOCKET\"])\n        response = postgrest.admin.get(\"/ready\")\n        assert response.status_code == 500\n\n\ndef test_admin_live_good(defaultenv):\n    \"Should get a success from the admin live endpoint if the main app is running\"\n\n    with run(env=defaultenv, port=freeport()) as postgrest:\n        response = postgrest.admin.get(\"/live\")\n        assert response.status_code == 200\n\n\ndef test_admin_live_dependent_on_main_app(defaultenv):\n    \"Should get a failure from the admin live endpoint if the main app also fails\"\n\n    with run(env=defaultenv) as postgrest:\n        # delete the unix socket to make the main app fail\n        os.remove(defaultenv[\"PGRST_SERVER_UNIX_SOCKET\"])\n        response = postgrest.admin.get(\"/live\")\n        assert response.status_code == 500\n\n\n@pytest.mark.parametrize(\"specialhostvalue\", FIXTURES[\"specialhostvalues\"])\ndef test_admin_works_with_host_special_values(specialhostvalue, defaultenv):\n    \"Should get a success from the admin live and ready endpoints when using special host values for the main app\"\n\n    with run(env=defaultenv, port=freeport(), host=specialhostvalue) as postgrest:\n        response = postgrest.admin.get(\"/live\")\n        assert response.status_code == 200\n\n        response = postgrest.admin.get(\"/ready\")\n        assert response.status_code == 200\n\n\n@pytest.mark.parametrize(\"level\", [\"crit\", \"error\", \"warn\", \"info\", \"debug\"])\ndef test_log_level(level, defaultenv):\n    \"log_level should filter request logging\"\n\n    env = {**defaultenv, \"PGRST_LOG_LEVEL\": level}\n\n    # any token to test 500 response for \"Server lacks JWT secret\"\n    claim = {\"role\": \"postgrest_test_author\"}\n    headers = jwtauthheader(claim, SECRET)\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/\", headers=headers)\n        assert response.status_code == 500\n\n        response = postgrest.session.get(\"/unknown\")\n        assert response.status_code == 404\n\n        response = postgrest.session.get(\"/\")\n        assert response.status_code == 200\n\n        output = sorted(postgrest.read_stdout(nlines=7))\n\n        if level == \"crit\":\n            assert len(output) == 0\n        elif level == \"error\":\n            assert re.match(\n                r'- - - \\[.+\\] \"GET / HTTP/1.1\" 500 \\d+ \"\" \"python-requests/.+\"',\n                output[0],\n            )\n            assert len(output) == 1\n        elif level == \"warn\":\n            assert re.match(\n                r'- - - \\[.+\\] \"GET / HTTP/1.1\" 500 \\d+ \"\" \"python-requests/.+\"',\n                output[0],\n            )\n            assert re.match(\n                r'- - postgrest_test_anonymous \\[.+\\] \"GET /unknown HTTP/1.1\" 404 \\d+ \"\" \"python-requests/.+\"',\n                output[1],\n            )\n            assert len(output) == 2\n        elif level == \"info\":\n            assert re.match(\n                r'- - - \\[.+\\] \"GET / HTTP/1.1\" 500 \\d+ \"\" \"python-requests/.+\"',\n                output[0],\n            )\n            assert re.match(\n                r'- - postgrest_test_anonymous \\[.+\\] \"GET / HTTP/1.1\" 200 \\d+ \"\" \"python-requests/.+\"',\n                output[1],\n            )\n            assert re.match(\n                r'- - postgrest_test_anonymous \\[.+\\] \"GET /unknown HTTP/1.1\" 404 \\d+ \"\" \"python-requests/.+\"',\n                output[2],\n            )\n            assert len(output) == 3\n        elif level == \"debug\":\n            assert re.match(\n                r'- - - \\[.+\\] \"GET / HTTP/1.1\" 500 \\d+ \"\" \"python-requests/.+\"',\n                output[0],\n            )\n            assert re.match(\n                r'- - postgrest_test_anonymous \\[.+\\] \"GET / HTTP/1.1\" 200 \\d+ \"\" \"python-requests/.+\"',\n                output[1],\n            )\n            assert re.match(\n                r'- - postgrest_test_anonymous \\[.+\\] \"GET /unknown HTTP/1.1\" 404 \\d+ \"\" \"python-requests/.+\"',\n                output[2],\n            )\n\n            assert len(output) == 7\n            assert any(\"Connection\" and \"is available\" in line for line in output)\n            assert any(\"Connection\" and \"is used\" in line for line in output)\n\n\n@pytest.mark.parametrize(\"level\", [\"crit\", \"error\", \"warn\", \"info\", \"debug\"])\ndef test_log_query(level, defaultenv):\n    \"log_query=true should log the SQL query according to the log_level\"\n\n    def drain_stdout(proc):\n        lines = []\n        while True:\n            chunk = proc.read_stdout(nlines=20)\n            if not chunk:\n                break\n            lines.extend(chunk)\n        return lines\n\n    env = {\n        **defaultenv,\n        \"PGRST_LOG_LEVEL\": level,\n        \"PGRST_LOG_QUERY\": \"true\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/\")\n        assert response.status_code == 200\n\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 200\n\n        response = postgrest.session.get(\n            \"/projects\", headers={\"Prefer\": \"count=estimated\"}\n        )\n        assert response.status_code == 206\n\n        response = postgrest.session.get(\n            \"/projects\", headers={\"Prefer\": \"count=planned\"}\n        )\n        assert response.status_code == 206\n\n        response = postgrest.session.get(\"/infinite_recursion\")\n        assert response.status_code == 500\n\n        get_2xx_regx = r'.+: WITH pgrst_source AS.+SELECT \"public\"\\.\"projects\"\\.\\* FROM \"public\"\\.\"projects\".+_postgrest_t'\n        get_2xx_count_regx = (\n            r'.+: EXPLAIN \\(FORMAT JSON\\) SELECT 1  FROM \"public\".\"projects\"'\n        )\n        infinite_recursion_5xx_regx = r'.+: WITH pgrst_source AS.+SELECT \"public\"\\.\"infinite_recursion\"\\.\\* FROM \"public\"\\.\"infinite_recursion\".+_postgrest_t'\n        root_tables_regx = r\".+: SELECT   n.nspname AS table_schema, .+ FROM pg_class c .+ ORDER BY table_schema, table_name\"\n        root_procs_regx = r\".+: WITH base_types AS \\(.+\\) SELECT   pn.nspname AS proc_schema, .+ FROM pg_proc p.+AND p.pronamespace = \\$1::regnamespace\"\n        root_descr_regx = r\".+: SELECT pg_catalog\\.obj_description\\(\\$1::regnamespace, 'pg_namespace'\\)\"\n        set_config_regx = (\n            r\".+: select set_config\\('search_path', \\$1, true\\), set_config\\(\"\n        )\n\n        output = drain_stdout(postgrest)\n\n        project_queries = [line for line in output if re.match(get_2xx_regx, line)]\n        project_counts = [line for line in output if re.match(get_2xx_count_regx, line)]\n        infinite_queries = [\n            line for line in output if re.match(infinite_recursion_5xx_regx, line)\n        ]\n        root_tables = [line for line in output if re.match(root_tables_regx, line)]\n        root_procs = [line for line in output if re.match(root_procs_regx, line)]\n        root_descr = [line for line in output if re.match(root_descr_regx, line)]\n        set_configs = [line for line in output if re.match(set_config_regx, line)]\n\n        if level == \"crit\":\n            assert not set_configs\n            assert not project_queries\n            assert not project_counts\n            assert not infinite_queries\n            assert not root_tables\n            assert not root_procs\n            assert not root_descr\n        elif level in {\"error\", \"warn\"}:\n            assert len(set_configs) == 1\n            assert len(infinite_queries) == 1\n            assert not project_queries\n            assert not project_counts\n            assert not root_tables\n            assert not root_procs\n            assert not root_descr\n        elif level == \"info\":\n            assert len(set_configs) == 5\n            assert len(project_queries) == 3\n            assert len(project_counts) == 2\n            assert len(infinite_queries) == 1\n            assert len(root_tables) == 1\n            assert len(root_procs) == 1\n            assert len(root_descr) == 1\n        elif level == \"debug\":\n            assert len(set_configs) == 5\n            assert len(project_queries) == 3\n            assert len(project_counts) == 2\n            assert len(infinite_queries) == 1\n            assert len(root_tables) == 1\n            assert len(root_procs) == 1\n            assert len(root_descr) == 1\n\n    pre_req_env = {\n        **env,\n        \"PGRST_DB_PRE_REQUEST\": \"do_nothing\",\n    }\n\n    with run(env=pre_req_env) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 200\n\n        output = drain_stdout(postgrest)\n\n        pre_request_regx = r'.+: select \"do_nothing\"()'\n        pre_reqs = [line for line in output if re.match(pre_request_regx, line)]\n\n        if level == \"crit\":\n            assert not pre_reqs\n        elif level in {\"error\", \"warn\"}:\n            assert not pre_reqs\n        elif level == \"info\":\n            assert len(pre_reqs) == 1\n        elif level == \"debug\":\n            assert len(pre_reqs) == 1\n\n\ndef test_no_pool_connection_required_on_bad_http_logic(defaultenv):\n    \"no pool connection should be consumed for failing on invalid http logic\"\n\n    with run(env=defaultenv, no_pool_connection_available=True) as postgrest:\n        # not found nested route shouldn't require opening a connection\n        response = postgrest.session.head(\"/path/notfound\")\n        assert response.status_code == 404\n\n        # an invalid http method on a resource shouldn't require opening a connection\n        response = postgrest.session.request(\"TRACE\", \"/projects\")\n        assert response.status_code == 405\n        response = postgrest.session.patch(\"/rpc/hello\")\n        assert response.status_code == 405\n\n\ndef test_no_pool_connection_required_on_options(defaultenv):\n    \"no pool connection should be consumed for OPTIONS requests\"\n\n    with run(env=defaultenv, no_pool_connection_available=True) as postgrest:\n        # OPTIONS on a table shouldn't require opening a connection\n        response = postgrest.session.options(\"/projects\")\n        assert response.status_code == 200\n\n        # OPTIONS on RPC shouldn't require opening a connection\n        response = postgrest.session.options(\"/rpc/hello\")\n        assert response.status_code == 200\n\n        # OPTIONS on root shouldn't require opening a connection\n        response = postgrest.session.options(\"/\")\n        assert response.status_code == 200\n\n\ndef test_no_pool_connection_required_on_bad_jwt_claim(defaultenv):\n    \"no pool connection should be consumed for failing on invalid jwt\"\n\n    env = {**defaultenv, \"PGRST_JWT_SECRET\": SECRET}\n\n    with run(env=env, no_pool_connection_available=True) as postgrest:\n        # A JWT with an invalid signature shouldn't open a connection\n        headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, \"Wrong Secret\")\n        response = postgrest.session.get(\"/projects\", headers=headers)\n        assert response.status_code == 401\n\n\ndef test_no_pool_connection_required_on_bad_embedding(defaultenv):\n    \"no pool connection should be consumed for failing to embed\"\n\n    with run(env=defaultenv, no_pool_connection_available=True) as postgrest:\n        # OPTIONS on a table shouldn't require opening a connection\n        response = postgrest.session.get(\"/projects?select=*,unexistent(*)\")\n        assert response.status_code == 400\n\n\n# https://github.com/PostgREST/postgrest/issues/2620\ndef test_notify_reloading_catalog_cache(defaultenv):\n    \"notify should reload the connection catalog cache\"\n\n    with run(env=defaultenv) as postgrest:\n        # first the id col is an uuid\n        response = postgrest.session.get(\n            \"/cats?id=eq.dea27321-f988-4a57-93e4-8eeb38f3cf1e\"\n        )\n        assert response.status_code == 200\n\n        # change it to a bigint\n        response = postgrest.session.post(\"/rpc/drop_change_cats\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n        sleep_until_postgrest_scache_reload()\n\n        # next request should succeed with a bigint value\n        response = postgrest.session.get(\"/cats?id=eq.1\")\n        assert response.status_code == 200\n\n\ndef test_role_settings(defaultenv):\n    \"statement_timeout should be set per role\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n\n    with run(env=env) as postgrest:\n        # statement_timeout for postgrest_test_anonymous\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=statement_timeout\")\n        assert response.text == '\"5s\"'\n\n        # reload statement_timeout with NOTIFY\n        response = postgrest.session.post(\n            \"/rpc/change_role_statement_timeout\", data={\"timeout\": \"8s\"}\n        )\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        response = postgrest.session.get(\"/rpc/reload_pgrst_config\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n        sleep_until_postgrest_config_reload()\n\n        response = postgrest.session.get(\"/rpc/get_guc_value?name=statement_timeout\")\n        assert response.text == '\"8s\"'\n\n        # statement_timeout for postgrest_test_author\n        headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n        response = postgrest.session.get(\n            \"/rpc/get_guc_value?name=statement_timeout\", headers=headers\n        )\n        assert response.text == '\"10s\"'\n\n        # reset statement timeout to original value\n        response = postgrest.session.post(\n            \"/rpc/change_role_statement_timeout\", data={\"timeout\": \"5s\"}\n        )\n        assert response.status_code == 204\n\n\ndef test_isolation_level(defaultenv):\n    \"isolation_level should be set per role and per function\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_JWT_SECRET\": SECRET,\n    }\n\n    with run(env=env) as postgrest:\n        # default isolation level for postgrest_test_anonymous\n        response = postgrest.session.get(\n            \"/items_w_isolation_level?select=isolation_level&limit=1\"\n        )\n        assert response.text == '[{\"isolation_level\":\"read committed\"}]'\n\n        # isolation level for postgrest_test_repeatable_read on GET\n        headers = jwtauthheader({\"role\": \"postgrest_test_repeatable_read\"}, SECRET)\n        response = postgrest.session.get(\n            \"/items_w_isolation_level?select=isolation_level&limit=1\", headers=headers\n        )\n        assert response.text == '[{\"isolation_level\":\"repeatable read\"}]'\n\n        # isolation level for postgrest_test_serializable on POST\n        headers = jwtauthheader({\"role\": \"postgrest_test_serializable\"}, SECRET)\n        headers[\"Prefer\"] = \"return=representation\"\n        response = postgrest.session.post(\n            \"/items_w_isolation_level?select=isolation_level\",\n            json={\"id\": \"666\"},\n            headers=headers,\n        )\n        assert response.text == '[{\"isolation_level\":\"serializable\"}]'\n\n        # isolation level for postgrest_test_serializable on PATCH\n        headers = jwtauthheader({\"role\": \"postgrest_test_serializable\"}, SECRET)\n        headers[\"Prefer\"] = \"return=representation\"\n        response = postgrest.session.patch(\n            \"/items_w_isolation_level?select=isolation_level&id=eq.666\",\n            json={\"id\": \"666\"},\n            headers=headers,\n        )\n        assert response.text == '[{\"isolation_level\":\"serializable\"}]'\n\n        # isolation level for postgrest_test_serializable on DELETE\n        headers = jwtauthheader({\"role\": \"postgrest_test_serializable\"}, SECRET)\n        headers[\"Prefer\"] = \"return=representation\"\n        response = postgrest.session.delete(\n            \"/items_w_isolation_level?select=isolation_level&id=eq.666\", headers=headers\n        )\n        assert response.text == '[{\"isolation_level\":\"serializable\"}]'\n\n        # default isolation level for function\n        response = postgrest.session.get(\"/rpc/default_isolation_level\")\n        assert response.text == '\"read committed\"'\n\n        # changes with role isolation level\n        headers = jwtauthheader({\"role\": \"postgrest_test_repeatable_read\"}, SECRET)\n        response = postgrest.session.get(\n            \"/rpc/default_isolation_level\", headers=headers\n        )\n        assert response.text == '\"repeatable read\"'\n\n        # isolation level can be set per function\n        response = postgrest.session.get(\"/rpc/serializable_isolation_level\")\n        assert response.text == '\"serializable\"'\n        response = postgrest.session.get(\"/rpc/repeatable_read_isolation_level\")\n        assert response.text == '\"repeatable read\"'\n\n        # isolation level for a function overrides the role isolation level\n        headers = jwtauthheader({\"role\": \"postgrest_test_repeatable_read\"}, SECRET)\n        response = postgrest.session.get(\"/rpc/serializable_isolation_level\")\n        assert response.text == '\"serializable\"'\n\n\ndef test_schema_cache_concurrent_notifications(slow_schema_cache_env):\n    \"schema cache should be up-to-date whenever a notification is sent while another reload is in progress, see https://github.com/PostgREST/postgrest/issues/2791\"\n\n    internal_sleep = (\n        int(slow_schema_cache_env[\"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\"]) / 1000\n    )\n\n    with run(env=slow_schema_cache_env, wait_for_readiness=False) as postgrest:\n        time.sleep(2 * internal_sleep + 0.1)  # wait for readiness manually\n\n        # first request, create a function and set a schema cache reload in progress\n        response = postgrest.session.post(\"/rpc/create_function\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        time.sleep(\n            internal_sleep / 2\n        )  # wait to be inside the schema cache reload process\n\n        # second request, change the same function and do another schema cache reload\n        response = postgrest.session.post(\"/rpc/migrate_function\")\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n        time.sleep(\n            2 * internal_sleep\n        )  # wait enough time to get the final schema cache state\n\n        # confirm the schema cache is up-to-date and the 2nd reload wasn't lost\n        response = postgrest.session.get(\"/rpc/mult_them?c=3&d=4\")\n        assert response.text == \"12\"\n        assert response.status_code == 200\n\n\ndef test_schema_cache_query_sleep_logs(defaultenv):\n    \"\"\"Schema cache sleep should be reflected in the logged query duration.\"\"\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"1000\",\n    }\n    log_pattern = re.compile(r\"Schema cache queried in ([\\d.]+) milliseconds\")\n\n    with run(env=env, wait_max_seconds=3, no_startup_stdout=False) as postgrest:\n        observed_ms = None\n        collected = []\n\n        lines = postgrest.read_stdout(nlines=10)\n        collected.extend(lines)\n        for line in lines:\n            match = log_pattern.search(line)\n            if match:\n                observed_ms = float(match.group(1))\n                break\n\n        assert observed_ms is not None\n        assert 1000 < observed_ms < 2000\n\n\ndef test_schema_cache_load_sleep_logs(defaultenv):\n    \"\"\"Schema cache load sleep should be reflected in the logged load duration.\"\"\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_LOAD_SLEEP\": \"1000\",\n    }\n    log_pattern = re.compile(r\"Schema cache loaded in ([\\d.]+) milliseconds\")\n\n    with run(env=env, wait_max_seconds=3, no_startup_stdout=False) as postgrest:\n        observed_ms = None\n        collected = []\n\n        lines = postgrest.read_stdout(nlines=10)\n        collected.extend(lines)\n        for line in lines:\n            match = log_pattern.search(line)\n            if match:\n                observed_ms = float(match.group(1))\n                break\n\n        assert observed_ms is not None\n        assert 1000 < observed_ms < 2000\n\n\n@pytest.mark.parametrize(\"dburi_type\", [\"no_params\", \"no_params_qmark\", \"with_params\"])\ndef test_get_pgrst_version_with_uri_connection_string(dburi_type, dburi, defaultenv):\n    \"The fallback_application_name should be added to the db-uri if it has a URI format\"\n    defaultenv_without_libpq = {\n        key: value\n        for key, value in defaultenv.items()\n        if key not in [\"PGDATABASE\", \"PGHOST\", \"PGUSER\"]\n    }\n\n    env = {\n        \"no_params\": {**defaultenv, \"PGRST_DB_URI\": \"postgresql://\"},\n        \"no_params_qmark\": {**defaultenv, \"PGRST_DB_URI\": \"postgresql://?\"},\n        \"with_params\": {**defaultenv_without_libpq, \"PGRST_DB_URI\": dburi.decode()},\n    }\n\n    with run(env=env[dburi_type]) as postgrest:\n        response = postgrest.session.post(\"/rpc/get_pgrst_version\")\n        version = '\"%s\"' % response.headers[\"Server\"].replace(\n            \"postgrest/\", \"PostgREST \"\n        )\n        assert response.text == version\n\n\ndef test_get_pgrst_version_with_keyval_connection_string(defaultenv):\n    \"The fallback_application_name should be added to the db-uri if it has a keyword/value format\"\n    uri = f'dbname={defaultenv[\"PGDATABASE\"]} host={defaultenv[\"PGHOST\"]} user={defaultenv[\"PGUSER\"]}'\n    defaultenv_without_libpq = {\n        key: value\n        for key, value in defaultenv.items()\n        if key not in [\"PGDATABASE\", \"PGHOST\", \"PGUSER\"]\n    }\n    env = {**defaultenv_without_libpq, \"PGRST_DB_URI\": uri}\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.post(\"/rpc/get_pgrst_version\")\n        version = '\"%s\"' % response.headers[\"Server\"].replace(\n            \"postgrest/\", \"PostgREST \"\n        )\n        assert response.text == version\n\n\ndef test_log_postgrest_version(defaultenv):\n    \"Should show the PostgREST version in the logs\"\n    with run(env=defaultenv, no_startup_stdout=False) as postgrest:\n        version = postgrest.session.head(\"/\").headers[\"Server\"].split(\"/\")[1]\n\n        output = postgrest.read_stdout(nlines=1)\n\n        assert \"Starting PostgREST %s...\" % version in output[0]\n\n\n@pytest.mark.parametrize(\n    \"host\", [\"127.0.0.1\", \"::1\", None], ids=[\"IPv4\", \"IPv6\", \"Unix\"]\n)\ndef test_log_postgrest_host_and_port(host, defaultenv):\n    \"PostgREST should output the host and port it is bound to.\"\n\n    # We run postgrest on unix socket when host and port are set to None\n    is_unix = host is None\n    port = None if is_unix else freeport()\n\n    with run(\n        env=defaultenv, host=host, port=port, no_startup_stdout=False\n    ) as postgrest:\n        output = postgrest.read_stdout(nlines=10)\n\n        if is_unix:\n            re.match(r'API server listening on \"/tmp/.*\\.sock\"', output[2])\n        elif is_ipv6(host):\n            assert f\"API server listening on [{host}]:{port}\" in output[2]\n        else:  # IPv4\n            assert f\"API server listening on {host}:{port}\" in output[2]\n\n\ndef test_succeed_w_role_having_superuser_settings(defaultenv):\n    \"Should succeed when having superuser settings on the impersonated role\"\n\n    env = {**defaultenv, \"PGRST_DB_CONFIG\": \"true\", \"PGRST_JWT_SECRET\": SECRET}\n\n    with run(stdin=SECRET.encode(), env=env) as postgrest:\n        headers = jwtauthheader({\"role\": \"postgrest_test_w_superuser_settings\"}, SECRET)\n        response = postgrest.session.get(\"/projects\", headers=headers)\n        print(response.text)\n        assert response.status_code == 200\n\n\ndef test_get_granted_superuser_setting(defaultenv):\n    \"Should succeed when the impersonated role has granted superuser settings\"\n\n    env = {**defaultenv, \"PGRST_DB_CONFIG\": \"true\", \"PGRST_JWT_SECRET\": SECRET}\n\n    with run(stdin=SECRET.encode(), env=env) as postgrest:\n        response_ver = postgrest.session.get(\"/rpc/get_postgres_version\")\n        pg_ver = eval(response_ver.text)\n        if pg_ver >= 150000:\n            headers = jwtauthheader(\n                {\"role\": \"postgrest_test_w_superuser_settings\"}, SECRET\n            )\n            response = postgrest.session.get(\n                \"/rpc/get_guc_value?name=log_min_duration_sample\", headers=headers\n            )\n            assert response.text == '\"12345ms\"'\n\n\ndef test_fail_with_invalid_dbname_and_automatic_recovery_disabled(defaultenv):\n    \"Should fail without retries when automatic recovery is disabled and dbname is invalid\"\n    dbname = \"INVALID\"\n    uri = f'postgresql://?dbname={dbname}&host={defaultenv[\"PGHOST\"]}&user={defaultenv[\"PGUSER\"]}'\n    env = {\n        **defaultenv,\n        \"PGRST_DB_URI\": uri,\n        \"PGRST_DB_POOL_AUTOMATIC_RECOVERY\": \"false\",\n    }\n\n    with run(env=env, wait_for_readiness=False) as postgrest:\n        exitCode = wait_until_exit(postgrest)\n        assert exitCode == 1\n\n\ndef test_fail_with_automatic_recovery_disabled_and_terminated_using_query(defaultenv):\n    \"Should fail without retries when automatic recovery is disabled and pg_terminate_backend(pid) is called\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_POOL_AUTOMATIC_RECOVERY\": \"false\",\n        \"PGAPPNAME\": \"target\",\n    }\n\n    app_name = \"'{}'\".format(env[\"PGAPPNAME\"])\n\n    with run(env=env) as postgrest:\n        os.system(\n            f'psql -d {env[\"PGDATABASE\"]} -U {env[\"PGUSER\"]} -h {env[\"PGHOST\"]} --set ON_ERROR_STOP=1 -a -c \"SELECT terminate_pgrst({app_name})\"'\n        )\n\n        exitCode = wait_until_exit(postgrest)\n        assert exitCode == 1\n\n\ndef test_preflight_request_with_cors_allowed_origin_config(defaultenv):\n    \"OPTIONS preflight request should return Access-Control-Allow-Origin equal to origin\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_CORS_ALLOWED_ORIGINS\": \"http://example.com, http://example2.com\",\n    }\n\n    headers = {\n        \"Accept\": \"*/*\",\n        \"Origin\": \"http://example.com\",\n        \"Access-Control-Request-Method\": \"POST\",\n        \"Access-Control-Request-Headers\": \"Content-Type\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.options(\"/items\", headers=headers)\n        assert (\n            response.headers[\"Access-Control-Allow-Origin\"] == \"http://example.com\"\n            and response.headers[\"Access-Control-Allow-Credentials\"] == \"true\"\n        )\n\n\ndef test_preflight_request_with_empty_cors_allowed_origin_config(defaultenv):\n    \"OPTIONS preflight request should allow all origins when config is present but empty\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_CORS_ALLOWED_ORIGINS\": \"\",\n    }\n\n    headers = {\n        \"Accept\": \"*/*\",\n        \"Origin\": \"http://anyorigin.com\",\n        \"Access-Control-Request-Method\": \"POST\",\n        \"Access-Control-Request-Headers\": \"Content-Type\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.options(\"/items\", headers=headers)\n        assert response.headers[\"Access-Control-Allow-Origin\"] == \"*\"\n        assert \"POST\" in response.headers[\"Access-Control-Allow-Methods\"]\n\n\ndef test_no_preflight_request_with_CORS_config_should_return_header(defaultenv):\n    \"GET no preflight request should return Access-Control-Allow-Origin equal to origin\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_CORS_ALLOWED_ORIGINS\": \"http://example.com, http://example2.com\",\n    }\n\n    headers = {\n        \"Accept\": \"*/*\",\n        \"Origin\": \"http://example.com\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/items\", headers=headers)\n        assert response.headers[\"Access-Control-Allow-Origin\"] == \"http://example.com\"\n\n\ndef test_no_preflight_request_with_CORS_config_should_not_return_header(defaultenv):\n    \"GET no preflight request should not return Access-Control-Allow-Origin\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_SERVER_CORS_ALLOWED_ORIGINS\": \"http://example.com, http://example2.com\",\n    }\n\n    headers = {\n        \"Accept\": \"*/*\",\n        \"Origin\": \"http://invalid.com\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/items\", headers=headers)\n        assert \"Access-Control-Allow-Origin\" not in response.headers\n\n\n@pytest.mark.parametrize(\"level\", [\"crit\", \"error\", \"warn\", \"info\", \"debug\"])\ndef test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):\n    \"verify that DB errors are logged to stderr\"\n\n    role = \"timeout_authenticator\"\n    set_statement_timeout(metapostgrest, role, 500)\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_LOG_LEVEL\": level,\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/sleep?seconds=1\")\n        assert response.status_code == 500\n\n        # ensure the message appears on the logs\n        output = sorted(postgrest.read_stdout(nlines=6))\n\n        if level == \"crit\":\n            assert len(output) == 0\n        elif level == \"debug\":\n            assert \" 500 \" in output[0]\n            assert \"canceling statement due to statement timeout\" in output[5]\n        else:\n            assert \" 500 \" in output[0]\n            assert \"canceling statement due to statement timeout\" in output[1]\n\n    reset_statement_timeout(metapostgrest, role)\n\n\ndef test_function_setting_statement_timeout_fails(defaultenv):\n    \"statement that takes three seconds to execute should fail with one second timeout\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.post(\"/rpc/one_sec_timeout\")\n\n        assert response.status_code == 500\n        assert (\n            response.text\n            == '{\"code\":\"57014\",\"details\":null,\"hint\":null,\"message\":\"canceling statement due to statement timeout\"}'\n        )\n\n\ndef test_function_setting_statement_timeout_passes(defaultenv):\n    \"statement that takes three seconds to execute should succeed with four second timeout\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.post(\"/rpc/four_sec_timeout\")\n\n        assert response.text == \"\"\n        assert response.status_code == 204\n\n\ndef test_function_setting_work_mem(defaultenv):\n    \"check function setting work_mem is applied\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_HOISTED_TX_SETTINGS\": \"work_mem\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/rpc_work_mem?select=get_work_mem\")\n\n        assert response.text == '{\"get_work_mem\":\"6000kB\"}'\n\n\ndef test_multiple_func_settings(defaultenv):\n    \"check multiple function settings are applied\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_HOISTED_TX_SETTINGS\": \"work_mem,statement_timeout\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\n            \"/rpc/rpc_with_two_hoisted?select=get_work_mem,get_statement_timeout\"\n        )\n\n        assert (\n            response.text == '{\"get_work_mem\":\"5000kB\",\"get_statement_timeout\":\"10s\"}'\n        )\n\n\ndef test_first_hoisted_setting_is_applied(defaultenv):\n    \"test that work_mem is applied and statement_timeout is not applied\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_HOISTED_TX_SETTINGS\": \"work_mem\",  # only work_mem is hoisted\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\n            \"/rpc/rpc_with_one_hoisted?select=get_work_mem,get_statement_timeout\"\n        )\n\n        assert response.text == '{\"get_work_mem\":\"3000kB\",\"get_statement_timeout\":\"5s\"}'\n\n\ndef test_second_hoisted_setting_is_applied(defaultenv):\n    \"test that statement_timeout is applied and work_mem is not applied\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_HOISTED_TX_SETTINGS\": \"statement_timeout\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\n            \"/rpc/rpc_with_one_hoisted?select=get_work_mem,get_statement_timeout\"\n        )\n\n        assert response.text == '{\"get_work_mem\":\"4MB\",\"get_statement_timeout\":\"7s\"}'\n\n\ndef test_admin_metrics(defaultenv):\n    \"Should get metrics from the admin endpoint\"\n\n    with run(env=defaultenv, port=freeport()) as postgrest:\n        response = postgrest.admin.get(\"/metrics\")\n        assert response.status_code == 200\n        assert response.headers[\"Content-Type\"] == \"text/plain; charset=utf-8\"\n        assert \"pgrst_schema_cache_query_time_seconds\" in response.text\n        assert 'pgrst_schema_cache_loads_total{status=\"SUCCESS\"}' in response.text\n        assert \"pgrst_db_pool_max\" in response.text\n        assert \"pgrst_db_pool_waiting\" in response.text\n        assert \"pgrst_db_pool_available\" in response.text\n        assert \"pgrst_db_pool_timeouts_total\" in response.text\n\n\ndef test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):\n    \"verify that the Schema Cache loads correctly at startup, using the in-db `pgrst.db_schemas` config\"\n\n    response = metapostgrest.session.post(\"/rpc/change_db_schemas_config\")\n    assert response.text == \"\"\n    assert response.status_code == 204\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.get(\"/rpc/get_current_schema\")\n        assert response.text == '\"test\"'\n        assert response.status_code == 200\n\n    response = metapostgrest.session.post(\"/rpc/reset_db_schemas_config\")\n    assert response.text == \"\"\n    assert response.status_code == 204\n\n\ndef test_pgrst_log_503_client_error_to_stderr(defaultenv):\n    \"PostgREST should log 503 errors to stderr\"\n\n    env = {\n        **defaultenv,\n        \"PGAPPNAME\": \"test-io\",\n    }\n\n    with run(env=env) as postgrest:\n\n        postgrest.session.get(\"/rpc/terminate_pgrst?appname=test-io\")\n\n        output = postgrest.read_stdout(nlines=6)\n\n        log_message = '{\"code\":\"PGRST001\",\"details\":\"no connection to the server\\\\n\",\"hint\":null,\"message\":\"Database client error. Retrying the connection.\"}\\n'\n\n        assert any(log_message in line for line in output)\n\n\ndef test_log_error_when_empty_schema_cache_on_startup_to_stderr(defaultenv):\n    \"Should log the 503 error message when there is an empty schema cache on startup\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"300\",\n    }\n\n    with run(env=env, wait_for_readiness=False) as postgrest:\n        postgrest.wait_until_scache_starts_loading()\n\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 503\n\n        output_start = postgrest.read_stdout(nlines=10)\n\n        log_err_message = '{\"code\":\"PGRST002\",\"details\":null,\"hint\":null,\"message\":\"Could not query the database for the schema cache. Retrying.\"}'\n\n        assert any(log_err_message in line for line in output_start)\n\n\ndef test_no_double_schema_cache_reload_on_empty_schema(defaultenv):\n    \"Should only load the schema cache once on a 503 error when there's an empty schema cache on startup\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP\": \"300\",\n    }\n\n    with run(env=env, port=freeport(), wait_for_readiness=False) as postgrest:\n        postgrest.wait_until_scache_starts_loading()\n\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 503\n\n        # Should wait enough time to load the schema cache twice to guarantee that the test is valid\n        time.sleep(1)\n\n        response = postgrest.admin.get(\"/metrics\")\n        assert response.status_code == 200\n        assert 'pgrst_schema_cache_loads_total{status=\"SUCCESS\"} 1.0' in response.text\n\n\n@pytest.mark.parametrize(\"level\", [\"crit\", \"error\", \"warn\", \"info\", \"debug\"])\ndef test_log_pool_req_observation(level, defaultenv):\n    \"PostgREST should log PoolRequest and PoolRequestFullfilled observation when log-level=debug\"\n\n    env = {**defaultenv, \"PGRST_LOG_LEVEL\": level, \"PGRST_JWT_SECRET\": SECRET}\n\n    headers = jwtauthheader({\"role\": \"postgrest_test_author\"}, SECRET)\n\n    pool_req = \"Trying to borrow a connection from pool\"\n    pool_req_fullfill = \"Borrowed a connection from the pool\"\n\n    with run(env=env) as postgrest:\n\n        postgrest.session.get(\"/authors_only\", headers=headers)\n\n        if level == \"debug\":\n            output = postgrest.read_stdout(nlines=5)\n            assert pool_req in output[1]\n            assert pool_req_fullfill in output[4]\n            assert len(output) == 5\n        elif level == \"info\":\n            output = postgrest.read_stdout(nlines=4)\n            assert len(output) == 1\n        else:\n            output = postgrest.read_stdout(nlines=4)\n            assert len(output) == 0\n\n\ndef test_proxy_status_header(defaultenv, metapostgrest):\n    \"Test Proxy-Status header in statement timeout error\"\n\n    role = \"timeout_authenticator\"\n    set_statement_timeout(metapostgrest, role, 1000)  # 1 second\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/sleep?seconds=2\")\n        assert response.status_code == 500\n        assert response.headers[\"Proxy-Status\"] == \"PostgREST; error=57014\"\n        data = response.json()\n        assert data[\"message\"] == \"canceling statement due to statement timeout\"\n\n\ndef test_allow_configs_to_be_set_to_empty(defaultenv):\n    'configs that are explicitly set to empty (= \"<empty>\") should not throw parse error'\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_EXTRA_SEARCH_PATH\": \"\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 200\n\n\ndef test_schema_cache_error_observation(defaultenv):\n    \"schema cache error observation should be logged with invalid db-schemas or db-extra-search-path\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_EXTRA_SEARCH_PATH\": \"x\",\n    }\n\n    with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:\n        # TODO: postgrest should exit here, instead it keeps retrying\n        # exitCode = wait_until_exit(postgrest)\n        # assert exitCode == 1\n\n        output = postgrest.read_stdout(nlines=9)\n        assert (\n            \"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x\"\n            in output[7]\n        )\n\n\ndef test_log_listener_connection_errors(defaultenv):\n    \"The logs should show the listener connection error message in a single line\"\n\n    env = {\n        **defaultenv,\n        \"PGHOST\": \"no_host\",\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n    }\n\n    with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:\n        output = postgrest.read_stdout(nlines=5)\n        assert any(\n            'Failed listening for database notifications on the \"pgrst\" channel. could not translate host name \"no_host\" to address:'\n            in line\n            for line in output\n        )\n\n\ndef test_log_listener_connection_start(defaultenv):\n    \"The logs should show the listener connection start message in a single line\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_CHANNEL_ENABLED\": \"true\",\n    }\n\n    with run(env=env, no_startup_stdout=False, wait_for_readiness=True) as postgrest:\n        output = postgrest.read_stdout(nlines=10)\n        # Check for the listener start message containing host and port\n        # Do not check if pg version is displayed properly as it is tricky to test it\n        assert any(\n            f'\"{defaultenv[\"PGHOST\"]}:5432\" and listening for database notifications on the \"pgrst\" channel'\n            in line\n            for line in output\n        )\n\n\ndef test_db_pre_config_with_pg_reserved_words(defaultenv):\n    \"The db-pre-config should not fail unexpectedly when function name is a postgres reserved word\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_PRE_CONFIG\": \"true\",  # call true function\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.post(\"/rpc/true\")\n        assert response.status_code == 200\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_PRE_CONFIG\": \"select\",  # no \"select\" function in our fixtures, fail gracefully at startup\n    }\n\n    with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:\n        output = postgrest.read_stdout(nlines=8)\n        assert any(\n            'Failed to query database settings for the config parameters.{\"code\":\"42883\",\"details\":null,\"hint\":\"No function matches the given name and argument types. You might need to add explicit type casts.\",\"message\":\"function select() does not exist\"}'\n            in line\n            for line in output\n        )\n\n\ndef test_requests_with_resource_embedding_wait_for_schema_cache_reload(defaultenv):\n    \"requests that use the schema cache with resource embedding wait long for the schema cache to reload\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_INTERNAL_SCHEMA_CACHE_RELATIONSHIP_LOAD_SLEEP\": \"5100\",\n    }\n\n    with run(env=env, wait_max_seconds=30) as postgrest:\n        # reload the schema cache\n        response = postgrest.session.get(\"/rpc/notify_pgrst\")\n        assert response.status_code == 204\n\n        postgrest.wait_until_scache_starts_loading()\n\n        response = postgrest.session.get(\"/directors?select=id,name,films(title)\")\n        assert response.status_code == 200\n\n        assert response.elapsed.total_seconds() > 5\n\n\ndef test_requests_without_resource_embedding_wait_for_schema_cache_reload(defaultenv):\n    \"requests that use the schema cache without resource embedding wait less for the schema cache to reload\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_DB_POOL\": \"2\",\n        \"PGRST_INTERNAL_SCHEMA_CACHE_LOAD_SLEEP\": \"1100\",\n        \"PGRST_INTERNAL_SCHEMA_CACHE_RELATIONSHIP_LOAD_SLEEP\": \"5000\",\n    }\n\n    with run(env=env, wait_max_seconds=30) as postgrest:\n        # reload the schema cache\n        response = postgrest.session.get(\"/rpc/notify_pgrst\")\n        assert response.status_code == 204\n\n        postgrest.wait_until_scache_starts_loading()\n\n        response = postgrest.session.get(\"/films\")\n        assert response.status_code == 200\n\n        assert (\n            response.elapsed.total_seconds() > 1\n            and response.elapsed.total_seconds() < 5\n        )\n\n\ndef test_server_timing_transaction_duration(defaultenv, metapostgrest):\n    \"server-timing transaction duration should be accurate\"\n\n    # just to ensure we don't timeout\n    role = \"timeout_authenticator\"\n    set_statement_timeout(metapostgrest, role, 3000)  # 3 seconds\n\n    env = {\n        **defaultenv,\n        \"PGUSER\": role,\n        \"PGRST_DB_ANON_ROLE\": role,\n        \"PGRST_SERVER_TIMING_ENABLED\": \"true\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/rpc/sleep?seconds=2\")\n\n        assert response.status_code == 204\n\n        response_dur = parse_server_timings_header(response.headers[\"Server-Timing\"])[\n            \"transaction\"\n        ]\n\n        assert 2000 <= response_dur < 3000\n\n\ndef test_client_error_verbosity_config(defaultenv):\n    \"Test PostgREST errors with different error verbosity settings\"\n\n    env = {\n        **defaultenv,\n        \"PGRST_CLIENT_ERROR_VERBOSITY\": \"minimal\",  # hide details and hint\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/itemsx\")\n        assert response.status_code == 404\n        assert response.json() == {\n            \"code\": \"PGRST205\",\n            \"message\": \"Could not find the table 'public.itemsx' in the schema cache\",\n        }\n\n    env = {\n        **defaultenv,\n        \"PGRST_CLIENT_ERROR_VERBOSITY\": \"verbose\",\n    }\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/itemsx\")\n        assert response.status_code == 404\n        assert response.json() == {\n            \"code\": \"PGRST205\",\n            \"message\": \"Could not find the table 'public.itemsx' in the schema cache\",\n            \"details\": None,\n            \"hint\": \"Perhaps you meant the table 'public.items'\",\n        }\n\n\ndef test_vary_custom_header_set(defaultenv):\n    \"Test default Vary header value is overridden in pre-request database function\"\n\n    env = {**defaultenv, \"PGRST_DB_PRE_REQUEST\": \"custom_vary_hdr\"}\n\n    with run(env=env) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n\n        assert response.headers[\"Vary\"] == \"X-Test-Accept\"\n\n\ndef test_vary_default_header_set(defaultenv):\n    \"Test default Vary header value matches default one\"\n\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n\n        assert response.headers[\"Vary\"] == \"Accept, Prefer, Range\"\n"
  },
  {
    "path": "test/io/test_replica.py",
    "content": "\"IO tests for PostgREST started on replicas\"\n\nfrom postgrest import run\n\n\ndef test_sanity_replica(replicaenv):\n    \"Test that primary and replica are working as intended\"\n\n    with run(env=replicaenv[\"primary\"]) as postgrest:\n        response = postgrest.session.get(\"/rpc/is_replica\")\n        assert response.text == \"false\"\n\n        response = postgrest.session.get(\"/rpc/get_replica_slot\")\n        assert response.text == '\"' + replicaenv[\"replica\"][\"PGREPLICASLOT\"] + '\"'\n\n        response = postgrest.session.get(\"/items?select=count\")\n        assert response.text == '[{\"count\":10}]'\n\n    with run(env=replicaenv[\"replica\"]) as postgrest:\n        response = postgrest.session.get(\"/rpc/is_replica\")\n        assert response.text == \"true\"\n\n        response = postgrest.session.get(\"/items?select=count\")\n        assert response.text == '[{\"count\":10}]'\n"
  },
  {
    "path": "test/io/test_sanity.py",
    "content": "\"Sanity checks for the PostgREST black box testing infrastructure.\"\n\nimport pytest\n\nfrom postgrest import freeport, run\n\n\ndef test_port_connection(defaultenv):\n    \"Connections via a port on localhost should work.\"\n    with run(env=defaultenv, port=freeport()):\n        pass\n\n\ndef test_plain_get(defaultenv):\n    \"run() should give a working PostgREST.\"\n    with run(env=defaultenv) as postgrest:\n        response = postgrest.session.get(\"/projects\")\n        assert response.status_code == 200\n\n\ndef test_no_pool_connection_available(defaultenv):\n    \"no_pool_connection_available option is functional\"\n    with run(env=defaultenv, no_pool_connection_available=True) as postgrest:\n        with pytest.raises(Exception):\n            postgrest.session.get(\"/projects\", timeout=1)\n"
  },
  {
    "path": "test/io/util.py",
    "content": "import threading\nimport jwt\n\n\nclass Thread(threading.Thread):\n    \"Variant of threading.Thread that re-raises any exceptions when joining the thread\"\n\n    def __init__(self, *args, **kwargs):\n        self._exception = None\n        super(Thread, self).__init__(*args, **kwargs)\n\n    def run(self):\n        try:\n            super(Thread, self).run()\n        except Exception as e:\n            self._exception = e\n\n    def join(self):\n        super(Thread, self).join()\n        if self._exception is not None:\n            raise self._exception\n\n\ndef authheader(token):\n    \"Bearer token HTTP authorization header.\"\n    return {\"Authorization\": f\"Bearer {token}\"}\n\n\ndef jwtauthheader(claim, secret):\n    \"Authorization header with signed JWT.\"\n    return authheader(jwt.encode(claim, secret))\n\n\ndef parse_server_timings_header(header):\n    \"\"\"Parse the Server-Timing header into a dict of metric names to values.\n\n    The header is a comma-separated list of metrics, each of which has a name\n    and a duration. The duration may be followed by a semicolon and a list of\n    parameters, but we ignore those.\n\n    See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing\n    \"\"\"\n    timings = {}\n    for timing in header.split(\",\"):\n        name, duration_text, *_ = timing.split(\";\")\n        _, duration = duration_text.split(\"=\")\n        timings[name.strip()] = float(duration)\n    return timings\n"
  },
  {
    "path": "test/load/bulk.json",
    "content": "[\n  {\n      \"id\": 1,\n      \"title\": \"Beetlejuice\",\n      \"year\": \"1988\",\n      \"runtime\": \"92\",\n      \"genres\": [\n          \"Comedy\",\n          \"Fantasy\"\n      ],\n      \"director\": \"Tim Burton\",\n      \"actors\": \"Alec Baldwin, Geena Davis, Annie McEnroe, Maurice Page\",\n      \"plot\": \"A couple of recently deceased ghosts contract the services of a \\\"bio-exorcist\\\" in order to remove the obnoxious new owners of their house.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTUwODE3MDE0MV5BMl5BanBnXkFtZTgwNTk1MjI4MzE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 2,\n      \"title\": \"The Cotton Club\",\n      \"year\": \"1984\",\n      \"runtime\": \"127\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Music\"\n      ],\n      \"director\": \"Francis Ford Coppola\",\n      \"actors\": \"Richard Gere, Gregory Hines, Diane Lane, Lonette McKee\",\n      \"plot\": \"The Cotton Club was a famous night club in Harlem. The story follows the people that visited the club, those that ran it, and is peppered with the Jazz music that made it so famous.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTU5ODAyNzA4OV5BMl5BanBnXkFtZTcwNzYwNTIzNA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 3,\n      \"title\": \"The Shawshank Redemption\",\n      \"year\": \"1994\",\n      \"runtime\": \"142\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Frank Darabont\",\n      \"actors\": \"Tim Robbins, Morgan Freeman, Bob Gunton, William Sadler\",\n      \"plot\": \"Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 4,\n      \"title\": \"Crocodile Dundee\",\n      \"year\": \"1986\",\n      \"runtime\": \"97\",\n      \"genres\": [\n          \"Adventure\",\n          \"Comedy\"\n      ],\n      \"director\": \"Peter Faiman\",\n      \"actors\": \"Paul Hogan, Linda Kozlowski, John Meillon, David Gulpilil\",\n      \"plot\": \"An American reporter goes to the Australian outback to meet an eccentric crocodile poacher and invites him to New York City.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTg0MTU1MTg4NF5BMl5BanBnXkFtZTgwMDgzNzYxMTE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 5,\n      \"title\": \"Valkyrie\",\n      \"year\": \"2008\",\n      \"runtime\": \"121\",\n      \"genres\": [\n          \"Drama\",\n          \"History\",\n          \"Thriller\"\n      ],\n      \"director\": \"Bryan Singer\",\n      \"actors\": \"Tom Cruise, Kenneth Branagh, Bill Nighy, Tom Wilkinson\",\n      \"plot\": \"A dramatization of the 20 July assassination and political coup plot by desperate renegade German Army officers against Hitler during World War II.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTg3Njc2ODEyN15BMl5BanBnXkFtZTcwNTAwMzc3NA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 6,\n      \"title\": \"Ratatouille\",\n      \"year\": \"2007\",\n      \"runtime\": \"111\",\n      \"genres\": [\n          \"Animation\",\n          \"Comedy\",\n          \"Family\"\n      ],\n      \"director\": \"Brad Bird, Jan Pinkava\",\n      \"actors\": \"Patton Oswalt, Ian Holm, Lou Romano, Brian Dennehy\",\n      \"plot\": \"A rat who can cook makes an unusual alliance with a young kitchen worker at a famous restaurant.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTMzODU0NTkxMF5BMl5BanBnXkFtZTcwMjQ4MzMzMw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 7,\n      \"title\": \"City of God\",\n      \"year\": \"2002\",\n      \"runtime\": \"130\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Fernando Meirelles, Kátia Lund\",\n      \"actors\": \"Alexandre Rodrigues, Leandro Firmino, Phellipe Haagensen, Douglas Silva\",\n      \"plot\": \"Two boys growing up in a violent neighborhood of Rio de Janeiro take different paths: one becomes a photographer, the other a drug dealer.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjA4ODQ3ODkzNV5BMl5BanBnXkFtZTYwOTc4NDI3._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 8,\n      \"title\": \"Memento\",\n      \"year\": \"2000\",\n      \"runtime\": \"113\",\n      \"genres\": [\n          \"Mystery\",\n          \"Thriller\"\n      ],\n      \"director\": \"Christopher Nolan\",\n      \"actors\": \"Guy Pearce, Carrie-Anne Moss, Joe Pantoliano, Mark Boone Junior\",\n      \"plot\": \"A man juggles searching for his wife's murderer and keeping his short-term memory loss from being an obstacle.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNThiYjM3MzktMDg3Yy00ZWQ3LTk3YWEtN2M0YmNmNWEwYTE3XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 9,\n      \"title\": \"The Intouchables\",\n      \"year\": \"2011\",\n      \"runtime\": \"112\",\n      \"genres\": [\n          \"Biography\",\n          \"Comedy\",\n          \"Drama\"\n      ],\n      \"director\": \"Olivier Nakache, Eric Toledano\",\n      \"actors\": \"François Cluzet, Omar Sy, Anne Le Ny, Audrey Fleurot\",\n      \"plot\": \"After he becomes a quadriplegic from a paragliding accident, an aristocrat hires a young man from the projects to be his caregiver.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTYxNDA3MDQwNl5BMl5BanBnXkFtZTcwNTU4Mzc1Nw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 10,\n      \"title\": \"Stardust\",\n      \"year\": \"2007\",\n      \"runtime\": \"127\",\n      \"genres\": [\n          \"Adventure\",\n          \"Family\",\n          \"Fantasy\"\n      ],\n      \"director\": \"Matthew Vaughn\",\n      \"actors\": \"Ian McKellen, Bimbo Hart, Alastair MacIntosh, David Kelly\",\n      \"plot\": \"In a countryside town bordering on a magical land, a young man makes a promise to his beloved that he'll retrieve a fallen star by venturing into the magical realm.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjkyMTE1OTYwNF5BMl5BanBnXkFtZTcwMDIxODYzMw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 11,\n      \"title\": \"Apocalypto\",\n      \"year\": \"2006\",\n      \"runtime\": \"139\",\n      \"genres\": [\n          \"Action\",\n          \"Adventure\",\n          \"Drama\"\n      ],\n      \"director\": \"Mel Gibson\",\n      \"actors\": \"Rudy Youngblood, Dalia Hernández, Jonathan Brewer, Morris Birdyellowhead\",\n      \"plot\": \"As the Mayan kingdom faces its decline, the rulers insist the key to prosperity is to build more temples and offer human sacrifices. Jaguar Paw, a young man captured for sacrifice, flees to avoid his fate.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNTM1NjYyNTY5OV5BMl5BanBnXkFtZTcwMjgwNTMzMQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 12,\n      \"title\": \"Taxi Driver\",\n      \"year\": \"1976\",\n      \"runtime\": \"113\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Martin Scorsese\",\n      \"actors\": \"Diahnne Abbott, Frank Adu, Victor Argo, Gino Ardito\",\n      \"plot\": \"A mentally unstable Vietnam War veteran works as a night-time taxi driver in New York City where the perceived decadence and sleaze feeds his urge for violent action, attempting to save a preadolescent prostitute in the process.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNGQxNDgzZWQtZTNjNi00M2RkLWExZmEtNmE1NjEyZDEwMzA5XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 13,\n      \"title\": \"No Country for Old Men\",\n      \"year\": \"2007\",\n      \"runtime\": \"122\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Ethan Coen, Joel Coen\",\n      \"actors\": \"Tommy Lee Jones, Javier Bardem, Josh Brolin, Woody Harrelson\",\n      \"plot\": \"Violence and mayhem ensue after a hunter stumbles upon a drug deal gone wrong and more than two million dollars in cash near the Rio Grande.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjA5Njk3MjM4OV5BMl5BanBnXkFtZTcwMTc5MTE1MQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 14,\n      \"title\": \"Planet 51\",\n      \"year\": \"2009\",\n      \"runtime\": \"91\",\n      \"genres\": [\n          \"Animation\",\n          \"Adventure\",\n          \"Comedy\"\n      ],\n      \"director\": \"Jorge Blanco, Javier Abad, Marcos Martínez\",\n      \"actors\": \"Jessica Biel, John Cleese, Gary Oldman, Dwayne Johnson\",\n      \"plot\": \"An alien civilization is invaded by Astronaut Chuck Baker, who believes that the planet was uninhabited. Wanted by the military, Baker must get back to his ship before it goes into orbit without him.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTUyOTAyNTA5Ml5BMl5BanBnXkFtZTcwODU2OTM0Mg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 15,\n      \"title\": \"Looper\",\n      \"year\": \"2012\",\n      \"runtime\": \"119\",\n      \"genres\": [\n          \"Action\",\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Rian Johnson\",\n      \"actors\": \"Joseph Gordon-Levitt, Bruce Willis, Emily Blunt, Paul Dano\",\n      \"plot\": \"In 2074, when the mob wants to get rid of someone, the target is sent into the past, where a hired gun awaits - someone like Joe - who one day learns the mob wants to 'close the loop' by sending back Joe's future self for assassination.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTY3NTY0MjEwNV5BMl5BanBnXkFtZTcwNTE3NDA1OA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 16,\n      \"title\": \"Corpse Bride\",\n      \"year\": \"2005\",\n      \"runtime\": \"77\",\n      \"genres\": [\n          \"Animation\",\n          \"Drama\",\n          \"Family\"\n      ],\n      \"director\": \"Tim Burton, Mike Johnson\",\n      \"actors\": \"Johnny Depp, Helena Bonham Carter, Emily Watson, Tracey Ullman\",\n      \"plot\": \"When a shy groom practices his wedding vows in the inadvertent presence of a deceased young woman, she rises from the grave assuming he has married her.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTk1MTY1NjU4MF5BMl5BanBnXkFtZTcwNjIzMTEzMw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 17,\n      \"title\": \"The Third Man\",\n      \"year\": \"1949\",\n      \"runtime\": \"93\",\n      \"genres\": [\n          \"Film-Noir\",\n          \"Mystery\",\n          \"Thriller\"\n      ],\n      \"director\": \"Carol Reed\",\n      \"actors\": \"Joseph Cotten, Alida Valli, Orson Welles, Trevor Howard\",\n      \"plot\": \"Pulp novelist Holly Martins travels to shadowy, postwar Vienna, only to find himself investigating the mysterious death of an old friend, Harry Lime.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjMwNzMzMTQ0Ml5BMl5BanBnXkFtZTgwNjExMzUwNjE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 18,\n      \"title\": \"The Beach\",\n      \"year\": \"2000\",\n      \"runtime\": \"119\",\n      \"genres\": [\n          \"Adventure\",\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Danny Boyle\",\n      \"actors\": \"Leonardo DiCaprio, Daniel York, Patcharawan Patarakijjanon, Virginie Ledoyen\",\n      \"plot\": \"Twenty-something Richard travels to Thailand and finds himself in possession of a strange map. Rumours state that it leads to a solitary beach paradise, a tropical bliss - excited and intrigued, he sets out to find it.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BN2ViYTFiZmUtOTIxZi00YzIxLWEyMzUtYjQwZGNjMjNhY2IwXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 19,\n      \"title\": \"Scarface\",\n      \"year\": \"1983\",\n      \"runtime\": \"170\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Brian De Palma\",\n      \"actors\": \"Al Pacino, Steven Bauer, Michelle Pfeiffer, Mary Elizabeth Mastrantonio\",\n      \"plot\": \"In Miami in 1980, a determined Cuban immigrant takes over a drug cartel and succumbs to greed.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjAzOTM4MzEwNl5BMl5BanBnXkFtZTgwMzU1OTc1MDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 20,\n      \"title\": \"Sid and Nancy\",\n      \"year\": \"1986\",\n      \"runtime\": \"112\",\n      \"genres\": [\n          \"Biography\",\n          \"Drama\",\n          \"Music\"\n      ],\n      \"director\": \"Alex Cox\",\n      \"actors\": \"Gary Oldman, Chloe Webb, David Hayman, Debby Bishop\",\n      \"plot\": \"Morbid biographical story of Sid Vicious, bassist with British punk group the Sex Pistols, and his girlfriend Nancy Spungen. When the Sex Pistols break up after their fateful US tour, ...\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjExNjA5NzY4M15BMl5BanBnXkFtZTcwNjQ2NzI5NA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 21,\n      \"title\": \"Black Swan\",\n      \"year\": \"2010\",\n      \"runtime\": \"108\",\n      \"genres\": [\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Darren Aronofsky\",\n      \"actors\": \"Natalie Portman, Mila Kunis, Vincent Cassel, Barbara Hershey\",\n      \"plot\": \"A committed dancer wins the lead role in a production of Tchaikovsky's \\\"Swan Lake\\\" only to find herself struggling to maintain her sanity.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNzY2NzI4OTE5MF5BMl5BanBnXkFtZTcwMjMyNDY4Mw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 22,\n      \"title\": \"Inception\",\n      \"year\": \"2010\",\n      \"runtime\": \"148\",\n      \"genres\": [\n          \"Action\",\n          \"Adventure\",\n          \"Sci-Fi\"\n      ],\n      \"director\": \"Christopher Nolan\",\n      \"actors\": \"Leonardo DiCaprio, Joseph Gordon-Levitt, Ellen Page, Tom Hardy\",\n      \"plot\": \"A thief, who steals corporate secrets through use of dream-sharing technology, is given the inverse task of planting an idea into the mind of a CEO.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjAxMzY3NjcxNF5BMl5BanBnXkFtZTcwNTI5OTM0Mw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 23,\n      \"title\": \"The Deer Hunter\",\n      \"year\": \"1978\",\n      \"runtime\": \"183\",\n      \"genres\": [\n          \"Drama\",\n          \"War\"\n      ],\n      \"director\": \"Michael Cimino\",\n      \"actors\": \"Robert De Niro, John Cazale, John Savage, Christopher Walken\",\n      \"plot\": \"An in-depth examination of the ways in which the U.S. Vietnam War impacts and disrupts the lives of people in a small industrial town in Pennsylvania.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTYzYmRmZTQtYjk2NS00MDdlLTkxMDAtMTE2YTM2ZmNlMTBkXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 24,\n      \"title\": \"Chasing Amy\",\n      \"year\": \"1997\",\n      \"runtime\": \"113\",\n      \"genres\": [\n          \"Comedy\",\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Kevin Smith\",\n      \"actors\": \"Ethan Suplee, Ben Affleck, Scott Mosier, Jason Lee\",\n      \"plot\": \"Holden and Banky are comic book artists. Everything's going good for them until they meet Alyssa, also a comic book artist. Holden falls for her, but his hopes are crushed when he finds out she's gay.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BZDM3MTg2MGUtZDM0MC00NzMwLWE5NjItOWFjNjA2M2I4YzgxXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 25,\n      \"title\": \"Django Unchained\",\n      \"year\": \"2012\",\n      \"runtime\": \"165\",\n      \"genres\": [\n          \"Drama\",\n          \"Western\"\n      ],\n      \"director\": \"Quentin Tarantino\",\n      \"actors\": \"Jamie Foxx, Christoph Waltz, Leonardo DiCaprio, Kerry Washington\",\n      \"plot\": \"With the help of a German bounty hunter, a freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMjIyNTQ5NjQ1OV5BMl5BanBnXkFtZTcwODg1MDU4OA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 26,\n      \"title\": \"The Silence of the Lambs\",\n      \"year\": \"1991\",\n      \"runtime\": \"118\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Jonathan Demme\",\n      \"actors\": \"Jodie Foster, Lawrence A. Bonney, Kasi Lemmons, Lawrence T. Wrentz\",\n      \"plot\": \"A young F.B.I. cadet must confide in an incarcerated and manipulative killer to receive his help on catching another serial killer who skins his victims.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ2NzkzMDI4OF5BMl5BanBnXkFtZTcwMDA0NzE1NA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 27,\n      \"title\": \"American Beauty\",\n      \"year\": \"1999\",\n      \"runtime\": \"122\",\n      \"genres\": [\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Sam Mendes\",\n      \"actors\": \"Kevin Spacey, Annette Bening, Thora Birch, Wes Bentley\",\n      \"plot\": \"A sexually frustrated suburban father has a mid-life crisis after becoming infatuated with his daughter's best friend.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjM4NTI5NzYyNV5BMl5BanBnXkFtZTgwNTkxNTYxMTE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 28,\n      \"title\": \"Snatch\",\n      \"year\": \"2000\",\n      \"runtime\": \"102\",\n      \"genres\": [\n          \"Comedy\",\n          \"Crime\"\n      ],\n      \"director\": \"Guy Ritchie\",\n      \"actors\": \"Benicio Del Toro, Dennis Farina, Vinnie Jones, Brad Pitt\",\n      \"plot\": \"Unscrupulous boxing promoters, violent bookmakers, a Russian gangster, incompetent amateur robbers, and supposedly Jewish jewelers fight to track down a priceless stolen diamond.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTA2NDYxOGYtYjU1Mi00Y2QzLTgxMTQtMWI1MGI0ZGQ5MmU4XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 29,\n      \"title\": \"Midnight Express\",\n      \"year\": \"1978\",\n      \"runtime\": \"121\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Alan Parker\",\n      \"actors\": \"Brad Davis, Irene Miracle, Bo Hopkins, Paolo Bonacelli\",\n      \"plot\": \"Billy Hayes, an American college student, is caught smuggling drugs out of Turkey and thrown into prison.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTQyMDA5MzkyOF5BMl5BanBnXkFtZTgwOTYwNTcxMTE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 30,\n      \"title\": \"Pulp Fiction\",\n      \"year\": \"1994\",\n      \"runtime\": \"154\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Quentin Tarantino\",\n      \"actors\": \"Tim Roth, Amanda Plummer, Laura Lovelace, John Travolta\",\n      \"plot\": \"The lives of two mob hit men, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTkxMTA5OTAzMl5BMl5BanBnXkFtZTgwNjA5MDc3NjE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 31,\n      \"title\": \"Lock, Stock and Two Smoking Barrels\",\n      \"year\": \"1998\",\n      \"runtime\": \"107\",\n      \"genres\": [\n          \"Comedy\",\n          \"Crime\"\n      ],\n      \"director\": \"Guy Ritchie\",\n      \"actors\": \"Jason Flemyng, Dexter Fletcher, Nick Moran, Jason Statham\",\n      \"plot\": \"A botched card game in London triggers four friends, thugs, weed-growers, hard gangsters, loan sharks and debt collectors to collide with each other in a series of unexpected events, all for the sake of weed, cash and two antique shotguns.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTAyN2JmZmEtNjAyMy00NzYwLThmY2MtYWQ3OGNhNjExMmM4XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 32,\n      \"title\": \"Lucky Number Slevin\",\n      \"year\": \"2006\",\n      \"runtime\": \"110\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Mystery\"\n      ],\n      \"director\": \"Paul McGuigan\",\n      \"actors\": \"Josh Hartnett, Bruce Willis, Lucy Liu, Morgan Freeman\",\n      \"plot\": \"A case of mistaken identity lands Slevin into the middle of a war being plotted by two of the city's most rival crime bosses: The Rabbi and The Boss. Slevin is under constant surveillance by relentless Detective Brikowski as well as the infamous assassin Goodkat and finds himself having to hatch his own ingenious plot to get them before they get him.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMzc1OTEwMTk4OF5BMl5BanBnXkFtZTcwMTEzMDQzMQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 33,\n      \"title\": \"Rear Window\",\n      \"year\": \"1954\",\n      \"runtime\": \"112\",\n      \"genres\": [\n          \"Mystery\",\n          \"Thriller\"\n      ],\n      \"director\": \"Alfred Hitchcock\",\n      \"actors\": \"James Stewart, Grace Kelly, Wendell Corey, Thelma Ritter\",\n      \"plot\": \"A wheelchair-bound photographer spies on his neighbours from his apartment window and becomes convinced one of them has committed murder.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNGUxYWM3M2MtMGM3Mi00ZmRiLWE0NGQtZjE5ODI2OTJhNTU0XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 34,\n      \"title\": \"Pan's Labyrinth\",\n      \"year\": \"2006\",\n      \"runtime\": \"118\",\n      \"genres\": [\n          \"Drama\",\n          \"Fantasy\",\n          \"War\"\n      ],\n      \"director\": \"Guillermo del Toro\",\n      \"actors\": \"Ivana Baquero, Sergi López, Maribel Verdú, Doug Jones\",\n      \"plot\": \"In the falangist Spain of 1944, the bookish young stepdaughter of a sadistic army officer escapes into an eerie but captivating fantasy world.\",\n      \"posterUrl\": \"\"\n  },\n  {\n      \"id\": 35,\n      \"title\": \"Shutter Island\",\n      \"year\": \"2010\",\n      \"runtime\": \"138\",\n      \"genres\": [\n          \"Mystery\",\n          \"Thriller\"\n      ],\n      \"director\": \"Martin Scorsese\",\n      \"actors\": \"Leonardo DiCaprio, Mark Ruffalo, Ben Kingsley, Max von Sydow\",\n      \"plot\": \"In 1954, a U.S. marshal investigates the disappearance of a murderess who escaped from a hospital for the criminally insane.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTMxMTIyNzMxMV5BMl5BanBnXkFtZTcwOTc4OTI3Mg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 36,\n      \"title\": \"Reservoir Dogs\",\n      \"year\": \"1992\",\n      \"runtime\": \"99\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Quentin Tarantino\",\n      \"actors\": \"Harvey Keitel, Tim Roth, Michael Madsen, Chris Penn\",\n      \"plot\": \"After a simple jewelry heist goes terribly wrong, the surviving criminals begin to suspect that one of them is a police informant.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNjE5ZDJiZTQtOGE2YS00ZTc5LTk0OGUtOTg2NjdjZmVlYzE2XkEyXkFqcGdeQXVyMzM4MjM0Nzg@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 37,\n      \"title\": \"The Shining\",\n      \"year\": \"1980\",\n      \"runtime\": \"146\",\n      \"genres\": [\n          \"Drama\",\n          \"Horror\"\n      ],\n      \"director\": \"Stanley Kubrick\",\n      \"actors\": \"Jack Nicholson, Shelley Duvall, Danny Lloyd, Scatman Crothers\",\n      \"plot\": \"A family heads to an isolated hotel for the winter where an evil and spiritual presence influences the father into violence, while his psychic son sees horrific forebodings from the past and of the future.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BODMxMjE3NTA4Ml5BMl5BanBnXkFtZTgwNDc0NTIxMDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 38,\n      \"title\": \"Midnight in Paris\",\n      \"year\": \"2011\",\n      \"runtime\": \"94\",\n      \"genres\": [\n          \"Comedy\",\n          \"Fantasy\",\n          \"Romance\"\n      ],\n      \"director\": \"Woody Allen\",\n      \"actors\": \"Owen Wilson, Rachel McAdams, Kurt Fuller, Mimi Kennedy\",\n      \"plot\": \"While on a trip to Paris with his fiancée's family, a nostalgic screenwriter finds himself mysteriously going back to the 1920s everyday at midnight.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTM4NjY1MDQwMl5BMl5BanBnXkFtZTcwNTI3Njg3NA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 39,\n      \"title\": \"Les Misérables\",\n      \"year\": \"2012\",\n      \"runtime\": \"158\",\n      \"genres\": [\n          \"Drama\",\n          \"Musical\",\n          \"Romance\"\n      ],\n      \"director\": \"Tom Hooper\",\n      \"actors\": \"Hugh Jackman, Russell Crowe, Anne Hathaway, Amanda Seyfried\",\n      \"plot\": \"In 19th-century France, Jean Valjean, who for decades has been hunted by the ruthless policeman Javert after breaking parole, agrees to care for a factory worker's daughter. The decision changes their lives forever.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTQ4NDI3NDg4M15BMl5BanBnXkFtZTcwMjY5OTI1OA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 40,\n      \"title\": \"L.A. Confidential\",\n      \"year\": \"1997\",\n      \"runtime\": \"138\",\n      \"genres\": [\n          \"Crime\",\n          \"Drama\",\n          \"Mystery\"\n      ],\n      \"director\": \"Curtis Hanson\",\n      \"actors\": \"Kevin Spacey, Russell Crowe, Guy Pearce, James Cromwell\",\n      \"plot\": \"As corruption grows in 1950s LA, three policemen - one strait-laced, one brutal, and one sleazy - investigate a series of murders with their own brand of justice.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BNWEwNDhhNWUtYWMzNi00ZTNhLWFiZDAtMjBjZmJhMTU0ZTY2XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 41,\n      \"title\": \"Moneyball\",\n      \"year\": \"2011\",\n      \"runtime\": \"133\",\n      \"genres\": [\n          \"Biography\",\n          \"Drama\",\n          \"Sport\"\n      ],\n      \"director\": \"Bennett Miller\",\n      \"actors\": \"Brad Pitt, Jonah Hill, Philip Seymour Hoffman, Robin Wright\",\n      \"plot\": \"Oakland A's general manager Billy Beane's successful attempt to assemble a baseball team on a lean budget by employing computer-generated analysis to acquire new players.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjAxOTU3Mzc1M15BMl5BanBnXkFtZTcwMzk1ODUzNg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 42,\n      \"title\": \"The Hangover\",\n      \"year\": \"2009\",\n      \"runtime\": \"100\",\n      \"genres\": [\n          \"Comedy\"\n      ],\n      \"director\": \"Todd Phillips\",\n      \"actors\": \"Bradley Cooper, Ed Helms, Zach Galifianakis, Justin Bartha\",\n      \"plot\": \"Three buddies wake up from a bachelor party in Las Vegas, with no memory of the previous night and the bachelor missing. They make their way around the city in order to find their friend before his wedding.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTU1MDA1MTYwMF5BMl5BanBnXkFtZTcwMDcxMzA1Mg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 43,\n      \"title\": \"The Great Beauty\",\n      \"year\": \"2013\",\n      \"runtime\": \"141\",\n      \"genres\": [\n          \"Drama\"\n      ],\n      \"director\": \"Paolo Sorrentino\",\n      \"actors\": \"Toni Servillo, Carlo Verdone, Sabrina Ferilli, Carlo Buccirosso\",\n      \"plot\": \"Jep Gambardella has seduced his way through the lavish nightlife of Rome for decades, but after his 65th birthday and a shock from the past, Jep looks past the nightclubs and parties to find a timeless landscape of absurd, exquisite beauty.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ0ODg1OTQ2Nl5BMl5BanBnXkFtZTgwNTc2MDY1MDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 44,\n      \"title\": \"Gran Torino\",\n      \"year\": \"2008\",\n      \"runtime\": \"116\",\n      \"genres\": [\n          \"Drama\"\n      ],\n      \"director\": \"Clint Eastwood\",\n      \"actors\": \"Clint Eastwood, Christopher Carley, Bee Vang, Ahney Her\",\n      \"plot\": \"Disgruntled Korean War veteran Walt Kowalski sets out to reform his neighbor, a Hmong teenager who tried to steal Kowalski's prized possession: a 1972 Gran Torino.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTQyMTczMTAxMl5BMl5BanBnXkFtZTcwOTc1ODE0Mg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 45,\n      \"title\": \"Mary and Max\",\n      \"year\": \"2009\",\n      \"runtime\": \"92\",\n      \"genres\": [\n          \"Animation\",\n          \"Comedy\",\n          \"Drama\"\n      ],\n      \"director\": \"Adam Elliot\",\n      \"actors\": \"Toni Collette, Philip Seymour Hoffman, Barry Humphries, Eric Bana\",\n      \"plot\": \"A tale of friendship between two unlikely pen pals: Mary, a lonely, eight-year-old girl living in the suburbs of Melbourne, and Max, a forty-four-year old, severely obese man living in New York.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ1NDIyNTA1Nl5BMl5BanBnXkFtZTcwMjc2Njk3OA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 46,\n      \"title\": \"Flight\",\n      \"year\": \"2012\",\n      \"runtime\": \"138\",\n      \"genres\": [\n          \"Drama\",\n          \"Thriller\"\n      ],\n      \"director\": \"Robert Zemeckis\",\n      \"actors\": \"Nadine Velazquez, Denzel Washington, Carter Cabassa, Adam C. Edwards\",\n      \"plot\": \"An airline pilot saves almost all his passengers on his malfunctioning airliner which eventually crashed, but an investigation into the accident reveals something troubling.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTUxMjI1OTMxNl5BMl5BanBnXkFtZTcwNjc3NTY1OA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 47,\n      \"title\": \"One Flew Over the Cuckoo's Nest\",\n      \"year\": \"1975\",\n      \"runtime\": \"133\",\n      \"genres\": [\n          \"Drama\"\n      ],\n      \"director\": \"Milos Forman\",\n      \"actors\": \"Michael Berryman, Peter Brocco, Dean R. Brooks, Alonzo Brown\",\n      \"plot\": \"A criminal pleads insanity after getting into trouble again and once in the mental institution rebels against the oppressive nurse and rallies up the scared patients.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BYmJkODkwOTItZThjZC00MTE0LWIxNzQtYTM3MmQwMGI1OWFiXkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 48,\n      \"title\": \"Requiem for a Dream\",\n      \"year\": \"2000\",\n      \"runtime\": \"102\",\n      \"genres\": [\n          \"Drama\"\n      ],\n      \"director\": \"Darren Aronofsky\",\n      \"actors\": \"Ellen Burstyn, Jared Leto, Jennifer Connelly, Marlon Wayans\",\n      \"plot\": \"The drug-induced utopias of four Coney Island people are shattered when their addictions run deep.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTkzODMzODYwOF5BMl5BanBnXkFtZTcwODM2NjA2NQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 49,\n      \"title\": \"The Truman Show\",\n      \"year\": \"1998\",\n      \"runtime\": \"103\",\n      \"genres\": [\n          \"Comedy\",\n          \"Drama\",\n          \"Sci-Fi\"\n      ],\n      \"director\": \"Peter Weir\",\n      \"actors\": \"Jim Carrey, Laura Linney, Noah Emmerich, Natascha McElhone\",\n      \"plot\": \"An insurance salesman/adjuster discovers his entire life is actually a television show.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMDIzODcyY2EtMmY2MC00ZWVlLTgwMzAtMjQwOWUyNmJjNTYyXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 50,\n      \"title\": \"The Artist\",\n      \"year\": \"2011\",\n      \"runtime\": \"100\",\n      \"genres\": [\n          \"Comedy\",\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Michel Hazanavicius\",\n      \"actors\": \"Jean Dujardin, Bérénice Bejo, John Goodman, James Cromwell\",\n      \"plot\": \"A silent movie star meets a young dancer, but the arrival of talking pictures sends their careers in opposite directions.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMzk0NzQxMTM0OV5BMl5BanBnXkFtZTcwMzU4MDYyNQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 51,\n      \"title\": \"Forrest Gump\",\n      \"year\": \"1994\",\n      \"runtime\": \"142\",\n      \"genres\": [\n          \"Comedy\",\n          \"Drama\"\n      ],\n      \"director\": \"Robert Zemeckis\",\n      \"actors\": \"Tom Hanks, Rebecca Williams, Sally Field, Michael Conner Humphreys\",\n      \"plot\": \"Forrest Gump, while not intelligent, has accidentally been present at many historic moments, but his true love, Jenny Curran, eludes him.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BYThjM2MwZGMtMzg3Ny00NGRkLWE4M2EtYTBiNWMzOTY0YTI4XkEyXkFqcGdeQXVyNDYyMDk5MTU@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 52,\n      \"title\": \"The Hobbit: The Desolation of Smaug\",\n      \"year\": \"2013\",\n      \"runtime\": \"161\",\n      \"genres\": [\n          \"Adventure\",\n          \"Fantasy\"\n      ],\n      \"director\": \"Peter Jackson\",\n      \"actors\": \"Ian McKellen, Martin Freeman, Richard Armitage, Ken Stott\",\n      \"plot\": \"The dwarves, along with Bilbo Baggins and Gandalf the Grey, continue their quest to reclaim Erebor, their homeland, from Smaug. Bilbo Baggins is in possession of a mysterious and magical ring.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMzU0NDY0NDEzNV5BMl5BanBnXkFtZTgwOTIxNDU1MDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 53,\n      \"title\": \"Vicky Cristina Barcelona\",\n      \"year\": \"2008\",\n      \"runtime\": \"96\",\n      \"genres\": [\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Woody Allen\",\n      \"actors\": \"Rebecca Hall, Scarlett Johansson, Christopher Evan Welch, Chris Messina\",\n      \"plot\": \"Two girlfriends on a summer holiday in Spain become enamored with the same painter, unaware that his ex-wife, with whom he has a tempestuous relationship, is about to re-enter the picture.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTU2NDQ4MTg2MV5BMl5BanBnXkFtZTcwNDUzNjU3MQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 54,\n      \"title\": \"Slumdog Millionaire\",\n      \"year\": \"2008\",\n      \"runtime\": \"120\",\n      \"genres\": [\n          \"Drama\",\n          \"Romance\"\n      ],\n      \"director\": \"Danny Boyle, Loveleen Tandan\",\n      \"actors\": \"Dev Patel, Saurabh Shukla, Anil Kapoor, Rajendranath Zutshi\",\n      \"plot\": \"A Mumbai teen reflects on his upbringing in the slums when he is accused of cheating on the Indian Version of \\\"Who Wants to be a Millionaire?\\\"\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTU2NTA5NzI0N15BMl5BanBnXkFtZTcwMjUxMjYxMg@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 55,\n      \"title\": \"Lost in Translation\",\n      \"year\": \"2003\",\n      \"runtime\": \"101\",\n      \"genres\": [\n          \"Drama\"\n      ],\n      \"director\": \"Sofia Coppola\",\n      \"actors\": \"Scarlett Johansson, Bill Murray, Akiko Takeshita, Kazuyoshi Minamimagoe\",\n      \"plot\": \"A faded movie star and a neglected young woman form an unlikely bond after crossing paths in Tokyo.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTI2NDI5ODk4N15BMl5BanBnXkFtZTYwMTI3NTE3._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 56,\n      \"title\": \"Match Point\",\n      \"year\": \"2005\",\n      \"runtime\": \"119\",\n      \"genres\": [\n          \"Drama\",\n          \"Romance\",\n          \"Thriller\"\n      ],\n      \"director\": \"Woody Allen\",\n      \"actors\": \"Jonathan Rhys Meyers, Alexander Armstrong, Paul Kaye, Matthew Goode\",\n      \"plot\": \"At a turning point in his life, a former tennis pro falls for an actress who happens to be dating his friend and soon-to-be brother-in-law.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTMzNzY4MzE5NF5BMl5BanBnXkFtZTcwMzQ1MDMzMQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 57,\n      \"title\": \"Psycho\",\n      \"year\": \"1960\",\n      \"runtime\": \"109\",\n      \"genres\": [\n          \"Horror\",\n          \"Mystery\",\n          \"Thriller\"\n      ],\n      \"director\": \"Alfred Hitchcock\",\n      \"actors\": \"Anthony Perkins, Vera Miles, John Gavin, Janet Leigh\",\n      \"plot\": \"A Phoenix secretary embezzles $40,000 from her employer's client, goes on the run, and checks into a remote motel run by a young man under the domination of his mother.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMDI3OWRmOTEtOWJhYi00N2JkLTgwNGItMjdkN2U0NjFiZTYwXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 58,\n      \"title\": \"North by Northwest\",\n      \"year\": \"1959\",\n      \"runtime\": \"136\",\n      \"genres\": [\n          \"Action\",\n          \"Adventure\",\n          \"Crime\"\n      ],\n      \"director\": \"Alfred Hitchcock\",\n      \"actors\": \"Cary Grant, Eva Marie Saint, James Mason, Jessie Royce Landis\",\n      \"plot\": \"A hapless New York advertising executive is mistaken for a government agent by a group of foreign spies, and is pursued across the country while he looks for a way to survive.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMjQwMTQ0MzgwNl5BMl5BanBnXkFtZTgwNjc4ODE4MzE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 59,\n      \"title\": \"Madagascar: Escape 2 Africa\",\n      \"year\": \"2008\",\n      \"runtime\": \"89\",\n      \"genres\": [\n          \"Animation\",\n          \"Action\",\n          \"Adventure\"\n      ],\n      \"director\": \"Eric Darnell, Tom McGrath\",\n      \"actors\": \"Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith\",\n      \"plot\": \"The animals try to fly back to New York City, but crash-land on an African wildlife refuge, where Alex is reunited with his parents.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjExMDA4NDcwMl5BMl5BanBnXkFtZTcwODAxNTQ3MQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 60,\n      \"title\": \"Despicable Me 2\",\n      \"year\": \"2013\",\n      \"runtime\": \"98\",\n      \"genres\": [\n          \"Animation\",\n          \"Adventure\",\n          \"Comedy\"\n      ],\n      \"director\": \"Pierre Coffin, Chris Renaud\",\n      \"actors\": \"Steve Carell, Kristen Wiig, Benjamin Bratt, Miranda Cosgrove\",\n      \"plot\": \"When Gru, the world's most super-bad turned super-dad has been recruited by a team of officials to stop lethal muscle and a host of Gru's own, He has to fight back with new gadgetry, cars, and more minion madness.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjExNjAyNTcyMF5BMl5BanBnXkFtZTgwODQzMjQ3MDE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 61,\n      \"title\": \"Downfall\",\n      \"year\": \"2004\",\n      \"runtime\": \"156\",\n      \"genres\": [\n          \"Biography\",\n          \"Drama\",\n          \"History\"\n      ],\n      \"director\": \"Oliver Hirschbiegel\",\n      \"actors\": \"Bruno Ganz, Alexandra Maria Lara, Corinna Harfouch, Ulrich Matthes\",\n      \"plot\": \"Traudl Junge, the final secretary for Adolf Hitler, tells of the Nazi dictator's final days in his Berlin bunker at the end of WWII.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTM1OTI1MjE2Nl5BMl5BanBnXkFtZTcwMTEwMzc4NA@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 62,\n      \"title\": \"Madagascar\",\n      \"year\": \"2005\",\n      \"runtime\": \"86\",\n      \"genres\": [\n          \"Animation\",\n          \"Adventure\",\n          \"Comedy\"\n      ],\n      \"director\": \"Eric Darnell, Tom McGrath\",\n      \"actors\": \"Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith\",\n      \"plot\": \"Spoiled by their upbringing with no idea what wild life is really like, four animals from New York Central Zoo escape, unwittingly assisted by four absconding penguins, and find themselves in Madagascar, among a bunch of merry lemurs\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTY4NDUwMzQxMF5BMl5BanBnXkFtZTcwMDgwNjgyMQ@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 63,\n      \"title\": \"Madagascar 3: Europe's Most Wanted\",\n      \"year\": \"2012\",\n      \"runtime\": \"93\",\n      \"genres\": [\n          \"Animation\",\n          \"Adventure\",\n          \"Comedy\"\n      ],\n      \"director\": \"Eric Darnell, Tom McGrath, Conrad Vernon\",\n      \"actors\": \"Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith\",\n      \"plot\": \"Alex, Marty, Gloria and Melman are still fighting to get home to their beloved Big Apple. Their journey takes them through Europe where they find the perfect cover: a traveling circus, which they reinvent - Madagascar style.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTM2MTIzNzk2MF5BMl5BanBnXkFtZTcwMDcwMzQxNw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 64,\n      \"title\": \"God Bless America\",\n      \"year\": \"2011\",\n      \"runtime\": \"105\",\n      \"genres\": [\n          \"Comedy\",\n          \"Crime\"\n      ],\n      \"director\": \"Bobcat Goldthwait\",\n      \"actors\": \"Joel Murray, Tara Lynne Barr, Melinda Page Hamilton, Mackenzie Brooke Smith\",\n      \"plot\": \"On a mission to rid society of its most repellent citizens, terminally ill Frank makes an unlikely accomplice in 16-year-old Roxy.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTQwMTc1MzA4NF5BMl5BanBnXkFtZTcwNzQwMTgzNw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 65,\n      \"title\": \"The Social Network\",\n      \"year\": \"2010\",\n      \"runtime\": \"120\",\n      \"genres\": [\n          \"Biography\",\n          \"Drama\"\n      ],\n      \"director\": \"David Fincher\",\n      \"actors\": \"Jesse Eisenberg, Rooney Mara, Bryan Barter, Dustin Fitzsimons\",\n      \"plot\": \"Harvard student Mark Zuckerberg creates the social networking site that would become known as Facebook, but is later sued by two brothers who claimed he stole their idea, and the co-founder who was later squeezed out of the business.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTM2ODk0NDAwMF5BMl5BanBnXkFtZTcwNTM1MDc2Mw@@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 66,\n      \"title\": \"The Pianist\",\n      \"year\": \"2002\",\n      \"runtime\": \"150\",\n      \"genres\": [\n          \"Biography\",\n          \"Drama\",\n          \"War\"\n      ],\n      \"director\": \"Roman Polanski\",\n      \"actors\": \"Adrien Brody, Emilia Fox, Michal Zebrowski, Ed Stoppard\",\n      \"plot\": \"A Polish Jewish musician struggles to survive the destruction of the Warsaw ghetto of World War II.\",\n      \"posterUrl\": \"http://ia.media-imdb.com/images/M/MV5BMTc4OTkyOTA3OF5BMl5BanBnXkFtZTYwMDIxNjk5._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 67,\n      \"title\": \"Alive\",\n      \"year\": \"1993\",\n      \"runtime\": \"120\",\n      \"genres\": [\n          \"Adventure\",\n          \"Biography\",\n          \"Drama\"\n      ],\n      \"director\": \"Frank Marshall\",\n      \"actors\": \"Ethan Hawke, Vincent Spano, Josh Hamilton, Bruce Ramsay\",\n      \"plot\": \"Uruguayan rugby team stranded in the snow swept Andes are forced to use desperate measures to survive after a plane crash.\",\n      \"posterUrl\": \"\"\n  },\n  {\n      \"id\": 68,\n      \"title\": \"Casablanca\",\n      \"year\": \"1942\",\n      \"runtime\": \"102\",\n      \"genres\": [\n          \"Drama\",\n          \"Romance\",\n          \"War\"\n      ],\n      \"director\": \"Michael Curtiz\",\n      \"actors\": \"Humphrey Bogart, Ingrid Bergman, Paul Henreid, Claude Rains\",\n      \"plot\": \"In Casablanca, Morocco in December 1941, a cynical American expatriate meets a former lover, with unforeseen complications.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMjQwNDYyNTk2N15BMl5BanBnXkFtZTgwMjQ0OTMyMjE@._V1_SX300.jpg\"\n  },\n  {\n      \"id\": 69,\n      \"title\": \"American Gangster\",\n      \"year\": \"2007\",\n      \"runtime\": \"157\",\n      \"genres\": [\n          \"Biography\",\n          \"Crime\",\n          \"Drama\"\n      ],\n      \"director\": \"Ridley Scott\",\n      \"actors\": \"Denzel Washington, Russell Crowe, Chiwetel Ejiofor, Josh Brolin\",\n      \"plot\": \"In 1970s America, a detective works to bring down the drug empire of Frank Lucas, a heroin kingpin from Manhattan, who is smuggling the drug into the country from the Far East.\",\n      \"posterUrl\": \"https://images-na.ssl-images-amazon.com/images/M/MV5BMTkyNzY5MDA5MV5BMl5BanBnXkFtZTcwMjg4MzI3MQ@@._V1_SX300.jpg\"\n  }\n]\n"
  },
  {
    "path": "test/load/errors.http",
    "content": "# Misspelled relations\nGET http://postgrest/actoxs?actor=eq.1\nPrefer: tx=commit\n\n# Misspelled relations on embeds\nGET http://postgrest/actors?select=*,rolws(*,films(*))\nPrefer: tx=commit\n\n# Misspelled function names\nGET http://postgrest/rpc/call_em_x?name=John\nPrefer: tx=commit\n\n# Permission denied errors\nGET http://postgrest/actors_1\nPrefer: tx=commit\n"
  },
  {
    "path": "test/load/errors.sql",
    "content": "\\ir fixtures.sql\n\nSELECT format('CREATE TABLE test.actors_%s ();', n)\nFROM generate_series(1, 20000) n\n\\gexec\n\n-- TODO add many function for fuzzy search (somehow this is making the loadtest start slow)\n"
  },
  {
    "path": "test/load/fixtures.sql",
    "content": "CREATE ROLE postgrest_test_anonymous;\nCREATE ROLE postgrest_test_author;\nGRANT postgrest_test_anonymous TO :PGUSER;\nGRANT postgrest_test_author TO :PGUSER;\nCREATE SCHEMA test;\n\n-- PUT+PATCH target needs one record and column to modify\nCREATE TABLE test.actors (\n  PRIMARY KEY (actor),\n  actor         INT,\n  name          TEXT,\n  last_modified TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n);\nINSERT INTO test.actors VALUES (1, 'John Doe');\n\n-- POST target needs generated PK\nCREATE TABLE test.films (\n  id INT PRIMARY KEY,\n  title TEXT,\n  year TEXT,\n  runtime TEXT,\n  genres TEXT[],\n  director TEXT,\n  actors TEXT,\n  plot TEXT,\n  \"posterUrl\" TEXT\n);\n\n-- DELETE target remains empty\nCREATE TABLE test.roles (\n  actor     INT REFERENCES test.actors,\n  film      INT REFERENCES test.films,\n  character TEXT\n);\n\n\nCREATE TABLE test.authors_only ();\n\nCREATE FUNCTION test.call_me (name TEXT) RETURNS TEXT\nSTABLE LANGUAGE SQL AS $$\n  SELECT 'Hello ' || name || ', how are you?';\n$$;\n\nGRANT USAGE ON SCHEMA test TO postgrest_test_anonymous, postgrest_test_author;\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test TO postgrest_test_anonymous;\n\nREVOKE ALL PRIVILEGES ON TABLE\n      authors_only\nFROM postgrest_test_anonymous;\n\nGRANT ALL ON TABLE authors_only TO postgrest_test_author;\n"
  },
  {
    "path": "test/load/patch.json",
    "content": "{\n  \"last_modified\": \"now\"\n}\n"
  },
  {
    "path": "test/load/post.json",
    "content": "{\n  \"id\": 0,\n  \"title\": \"Workers Leaving The Lumière Factory In Lyon\"\n}\n"
  },
  {
    "path": "test/load/put.json",
    "content": "{\n  \"actor\": 1,\n  \"name\": \"John Doe\",\n  \"last_modified\": \"now\"\n}\n"
  },
  {
    "path": "test/load/rpc.json",
    "content": "{\n  \"name\": \"John\"\n}\n"
  },
  {
    "path": "test/load/targets.http",
    "content": "GET http://postgrest/\nPrefer: tx=commit\n\nHEAD http://postgrest/actors?actor=eq.1\nPrefer: tx=commit\n\nGET http://postgrest/actors?select=*,roles(*,films(*))\nPrefer: tx=commit\n\nPOST http://postgrest/films?columns=id,title\nPrefer: tx=rollback\n@post.json\n\nPOST http://postgrest/films?columns=id,title,year,runtime,genres,director,actors,plot,posterUrl\nPrefer: tx=rollback\n# this bulk.json was obtained from https://github.com/erik-sytnyk/movies-list/blob/master/db.json\n@bulk.json\n\nPUT http://postgrest/actors?actor=eq.1&columns=name\nPrefer: tx=rollback\n@put.json\n\nPATCH http://postgrest/actors?actor=eq.1\nPrefer: tx=rollback\n@patch.json\n\nDELETE http://postgrest/roles\nPrefer: tx=rollback\n\nGET http://postgrest/rpc/call_me?name=John\n\nPOST http://postgrest/rpc/call_me\n@rpc.json\n\nOPTIONS http://postgrest/actors\n"
  },
  {
    "path": "test/memory/memory-tests.sh",
    "content": "#!/usr/bin/env bash\n\n# This test script expects that a `postgrest` executable with profiling enabled\n# is on the PATH.\n\nset -Eeuo pipefail\n\npgrPort=49421\n\nexport PGRST_DB_ANON_ROLE=\"postgrest_test_anonymous\"\nexport PGRST_DB_POOL=\"1\"\nexport PGRST_SERVER_HOST=\"127.0.0.1\"\nexport PGRST_SERVER_PORT=\"$pgrPort\"\nexport PGRST_JWT_SECRET=\"reallyreallyreallyreallyverysafe\"\nexport PGRST_DB_CONFIG=\"false\"\n\ntrap \"kill 0\" int term exit\n\ncurrentTest=1\nfailedTests=0\nresult(){ echo \"$1 $currentTest $2\"; currentTest=$(( currentTest + 1 )); }\nok(){ result 'ok' \"- $1\"; }\nko(){ result 'not ok' \"- $1\"; failedTests=$(( failedTests + 1 )); }\n\npgrStart(){ postgrest +RTS -p -h > /dev/null 2>&1 & pgrPID=\"$!\"; }\npgrStop(){ kill \"$pgrPID\" 2>/dev/null; }\n\ncheckPgrStarted(){\n  while pgrStarted && test \"$(rootStatus)\" -ne 200\n  do\n    sleep 1\n  done\n}\npgrStarted(){ kill -0 \"$pgrPID\" 2>/dev/null; }\nrootStatus(){\n  curl -s -o /dev/null -I -w '%{http_code}' \"http://localhost:$pgrPort/\"\n}\n\njsonKeyTest(){\n  pgrStart\n  checkPgrStarted\n  factor=$(( 3*$(numfmt --from=si \"$1\")/4 )) # 3/4 on $1 is need to maintain the specified size because of base64\n  payload=\"{\\\"blob\\\" : \\\"$(dd if=/dev/zero bs=$factor count=1 status=none | base64)\\\"}\"\n  httpStatus=$(echo \"$payload\" | curl -s -H \"Content-Type: application/json\" --request \"$2\" -d @- -w '%{http_code}' http://localhost:\"$pgrPort\"\"$3\" | tr -d '\"')\n  if test \"$httpStatus\" -ge 200 && test \"$httpStatus\" -lt 210\n  then\n    pgrStop\n    while [ ! -s postgrest.prof ]\n    do\n      sleep 1\n    done\n    BYTES_FMT=$(< postgrest.prof grep -o -P '(?<=alloc =).*(?=bytes)' | tr -d ' ')\n    BYTES=$(echo \"$BYTES_FMT\" | tr -d ',')\n    MAX_BYTES=$(numfmt --from=si \"$4\")\n    if test \"$BYTES\" -le \"$MAX_BYTES\"\n    then\n      ok \"$2 $3: with a json key of $1 the memory usage($BYTES_FMT bytes) is less than $4\"\n    else\n      ko \"$2 $3: with a json key of $1 the memory usage($BYTES_FMT bytes) is more than $4\"\n    fi\n  else\n    pgrStop\n    ko \"$2 $3: request failed with http $httpStatus\"\n  fi\n}\n\npostJsonArrayTest(){\n  pgrStart\n  checkPgrStarted\n  arr=()\n  arr+=('[')\n  for i in $(seq 1 $((\"$1\" - 1)))\n  do\n    arr+=(\"{\\\"id\\\": $i, \\\"body\\\": \\\"xxxxxxx\\\"},\")\n  done\n  arr+=(\"{\\\"id\\\": $1, \\\"body\\\": \\\"xxxxxxx\\\"}\")\n  arr+=(']')\n  payload=\"${arr[*]}\"\n  httpStatus=$(echo \"$payload\" | curl -s -H \"Content-Type: application/json\" -d @- -w '%{http_code}' http://localhost:\"$pgrPort\"\"$2\" | tr -d '\"')\n  if test \"$httpStatus\" -ge 200 && test \"$httpStatus\" -lt 210\n  then\n    pgrStop\n    while [ ! -s postgrest.prof ]\n    do\n      sleep 1\n    done\n    BYTES_FMT=$(< postgrest.prof grep -o -P '(?<=alloc =).*(?=bytes)' | tr -d ' ')\n    BYTES=$(echo \"$BYTES_FMT\" | tr -d ',')\n    MAX_BYTES=$(numfmt --from=si \"$3\")\n    PAYLOAD_SIZE=$(echo \"$payload\" | wc -c | numfmt --to=si)\n    if test \"$BYTES\" -le \"$MAX_BYTES\"\n    then\n      ok \"POST $2: with a json payload of $PAYLOAD_SIZE that has $1 array values the memory usage($BYTES_FMT bytes) is less than $3\"\n    else\n      ko \"POST $2: with a json payload of $PAYLOAD_SIZE that has $1 array values the memory usage($BYTES_FMT bytes) is more than $3\"\n    fi\n  else\n    pgrStop\n    ko \"POST $2: request failed with http $httpStatus\"\n  fi\n}\n\necho \"Running memory usage tests..\"\n\njsonKeyTest \"1M\" \"POST\" \"/rpc/leak?columns=blob\" \"22M\"\njsonKeyTest \"1M\" \"POST\" \"/leak?columns=blob\" \"22M\"\njsonKeyTest \"1M\" \"PATCH\" \"/leak?id=eq.1&columns=blob\" \"22M\"\n\njsonKeyTest \"10M\" \"POST\" \"/rpc/leak?columns=blob\" \"32M\"\njsonKeyTest \"10M\" \"POST\" \"/leak?columns=blob\" \"32M\"\njsonKeyTest \"10M\" \"PATCH\" \"/leak?id=eq.1&columns=blob\" \"50M\"\n\njsonKeyTest \"50M\" \"POST\" \"/rpc/leak?columns=blob\" \"73M\"\njsonKeyTest \"50M\" \"POST\" \"/leak?columns=blob\" \"73M\"\njsonKeyTest \"50M\" \"PATCH\" \"/leak?id=eq.1&columns=blob\" \"73M\"\n\npostJsonArrayTest \"1000\" \"/perf_articles?columns=id,body\" \"21M\"\npostJsonArrayTest \"10000\" \"/perf_articles?columns=id,body\" \"22M\"\npostJsonArrayTest \"100000\" \"/perf_articles?columns=id,body\" \"25M\"\n\ntrap - int term exit\n\nexit $failedTests\n"
  },
  {
    "path": "test/observability/Main.hs",
    "content": "module Main where\n\nimport qualified Hasql.Pool                 as P\nimport qualified Hasql.Pool.Config          as P\nimport qualified Hasql.Transaction.Sessions as HT\n\nimport Data.Function (id)\n\nimport           PostgREST.App             (postgrest)\nimport qualified PostgREST.AppState        as AppState\nimport           PostgREST.Config          (AppConfig (..))\nimport           PostgREST.Config.Database (queryPgVersion)\nimport qualified PostgREST.Logger          as Logger\nimport qualified PostgREST.Metrics         as Metrics\nimport           PostgREST.SchemaCache     (querySchemaCache)\n\nimport qualified Observation.JwtCache\n\nimport ObsHelper\nimport Protolude  hiding (toList, toS)\nimport Test.Hspec\n\nmain :: IO ()\nmain = do\n  pool <- P.acquire $ P.settings\n    [ P.size 3\n    , P.acquisitionTimeout 10\n    , P.agingTimeout 60\n    , P.idlenessTimeout 60\n    , P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)\n    ]\n\n  actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)\n\n  -- cached schema cache so most tests run fast\n  baseSchemaCache <- loadSCache pool testCfg\n  loggerState <- Logger.init\n  metricsState <- Metrics.init (configDbPoolSize testCfg)\n\n  let\n    initApp sCache st config = do\n      appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState)\n      AppState.putPgVersion appState actualPgVersion\n      AppState.putSchemaCache appState (Just sCache)\n      return (st, postgrest (configLogLevel config) appState (pure ()))\n\n  -- Run all test modules\n  hspec $ do\n    before (initApp baseSchemaCache metricsState testCfgJwtCache) $\n      describe \"Observation.JwtCacheObs\" Observation.JwtCache.spec\n\n  where\n    loadSCache pool conf =\n      either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)\n"
  },
  {
    "path": "test/observability/ObsHelper.hs",
    "content": "{-# LANGUAGE AllowAmbiguousTypes       #-}\n{-# LANGUAGE ExistentialQuantification #-}\n{-# LANGUAGE FlexibleContexts          #-}\n{-# LANGUAGE ScopedTypeVariables       #-}\n{-# LANGUAGE TupleSections             #-}\n{-# LANGUAGE TypeApplications          #-}\nmodule ObsHelper where\n\nimport qualified Data.ByteString.Base64 as B64 (decodeLenient)\nimport qualified Data.ByteString.Char8  as BS\nimport qualified Data.ByteString.Lazy   as BL\nimport qualified Jose.Jwa               as JWT\nimport qualified Jose.Jws               as JWT\nimport qualified Jose.Jwt               as JWT\n\nimport PostgREST.Config (AppConfig (..), JSPathExp (..),\n                         LogLevel (..), OpenAPIMode (..),\n                         Verbosity (..), parseSecret)\n\nimport Data.List.NonEmpty              (fromList)\nimport Data.String                     (String)\nimport Prometheus                      (Counter, getCounter)\nimport Test.Hspec.Expectations.Contrib (annotate)\n\nimport Network.HTTP.Types\nimport Protolude\nimport Test.Hspec\nimport Test.Hspec.Wai\n\n\nbaseCfg :: AppConfig\nbaseCfg = let secret = encodeUtf8 \"reallyreallyreallyreallyverysafe\" in\n  AppConfig {\n    configAppSettings               = []\n  , configClientErrorVerbosity      = Verbose\n  , configDbAggregates              = False\n  , configDbAnonRole                = Just \"postgrest_test_anonymous\"\n  , configDbChannel                 = mempty\n  , configDbChannelEnabled          = True\n  , configDbExtraSearchPath         = []\n  , configDbHoistedTxSettings       = [\"default_transaction_isolation\",\"plan_filter.statement_cost_limit\",\"statement_timeout\"]\n  , configDbMaxRows                 = Nothing\n  , configDbPlanEnabled             = False\n  , configDbPoolSize                = 10\n  , configDbPoolAcquisitionTimeout  = 10\n  , configDbPoolMaxLifetime         = 1800\n  , configDbPoolMaxIdletime         = 600\n  , configDbPoolAutomaticRecovery   = True\n  , configDbPreRequest              = Nothing\n  , configDbPreparedStatements      = True\n  , configDbRootSpec                = Nothing\n  , configDbSchemas                 = fromList [\"test\"]\n  , configDbConfig                  = False\n  , configDbPreConfig               = Nothing\n  , configDbUri                     = \"postgresql://\"\n  , configFilePath                  = Nothing\n  , configJWKS                      = rightToMaybe $ parseSecret secret\n  , configJwtAudience               = Nothing\n  , configJwtRoleClaimKey           = [JSPKey \"role\"]\n  , configJwtSecret                 = Just secret\n  , configJwtSecretIsBase64         = False\n  , configJwtCacheMaxEntries        = 10\n  , configLogLevel                  = LogCrit\n  , configLogQuery                  = False\n  , configOpenApiMode               = OAFollowPriv\n  , configOpenApiSecurityActive     = False\n  , configOpenApiServerProxyUri     = Nothing\n  , configServerCorsAllowedOrigins  = Nothing\n  , configServerHost                = \"localhost\"\n  , configServerPort                = 3000\n  , configServerTraceHeader         = Nothing\n  , configServerUnixSocket          = Nothing\n  , configServerUnixSocketMode      = 432\n  , configDbTxAllowOverride         = True\n  , configDbTxRollbackAll           = True\n  , configAdminServerHost           = \"localhost\"\n  , configAdminServerPort           = Nothing\n  , configRoleSettings              = mempty\n  , configRoleIsoLvl                = mempty\n  , configInternalSCQuerySleep      = Nothing\n  , configInternalSCLoadSleep       = Nothing\n  , configInternalSCRelLoadSleep    = Nothing\n  , configServerTimingEnabled       = True\n  }\n\ntestCfg :: AppConfig\ntestCfg = baseCfg\n\ntestCfgJwtCache :: AppConfig\ntestCfgJwtCache =\n  baseCfg {\n    configJwtSecret = Just generateSecret\n  , configJWKS = rightToMaybe $ parseSecret generateSecret\n  , configJwtCacheMaxEntries = 2\n  }\n\nauthHeader :: BS.ByteString -> BS.ByteString -> Header\nauthHeader typ creds =\n  (hAuthorization, typ <> \" \" <> creds)\n\nauthHeaderJWT :: BS.ByteString -> Header\nauthHeaderJWT = authHeader \"Bearer\"\n\ngenerateSecret :: ByteString\ngenerateSecret = B64.decodeLenient \"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=\"\n\ngenerateJWT :: BL.ByteString -> ByteString\ngenerateJWT claims =\n  either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)\n\n-- state check helpers\n\ndata StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)\n\nstateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m\nstateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)\n\nexpectField :: forall s st a c m. (KnownSymbol s, Show a, Eq a, HasField s st c) => (c -> m a) -> (a -> a) -> StateCheck st m\nexpectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)\n\ncheckState :: (Traversable t) => t (StateCheck st (WaiSession st)) -> WaiSession st b -> WaiSession st ()\ncheckState checks act = getState >>= flip (`checkState'` checks) act\n\ncheckState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()\ncheckState' initialState checks act = do\n  expectations <- traverse (\\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks\n  void act\n  sequenceA_ expectations\n  where\n    createExpectation msg metrics expect = pure $ metrics >>= liftIO . annotate msg . expect\n\nexpectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m) => (Int -> Int) -> StateCheck st m\nexpectCounter = expectField @s intCounter\n  where\n    intCounter = ((round @Double @Int) <$>) . getCounter\n"
  },
  {
    "path": "test/observability/Observation/JwtCache.hs",
    "content": "{-# LANGUAGE DataKinds        #-}\n{-# LANGUAGE TypeApplications #-}\nmodule Observation.JwtCache where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec         (SpecWith, describe, it)\nimport Test.Hspec.Wai\n\nimport ObsHelper\nimport PostgREST.Metrics   (MetricsState (..))\nimport Protolude\nimport Test.Hspec.Wai.JSON (json)\n\nspec :: SpecWith (MetricsState, Application)\nspec = describe \"Server started with JWT and metrics enabled\" $ do\n  it \"Should not have JWT in cache\" $ do\n    let auth = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe1\"}|]\n\n    expectCounters\n      [\n        requests (+ 1)\n      , hits     (+ 0)\n      ] $\n\n         request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 200\n\n  it \"Should have JWT in cache\" $ do\n    let auth = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe2\"}|]\n\n    expectCounters\n      [\n        requests (+ 2)\n      , hits     (+ 1)\n      ] $\n\n         request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 200\n      *> request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 200\n\n  it \"Should not cache invalid JWTs\" $ do\n    let auth = authHeaderJWT \"some random bytes\"\n\n    expectCounters\n      [\n        requests (+ 2)\n      , hits     (+ 0)\n      ] $\n\n         request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 401\n      *> request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 401\n\n  it \"Should cache expired JWTs\" $ do\n    let auth = genToken [json|{\"exp\": 1, \"role\": \"postgrest_test_author\", \"id\": \"jdoe2\"}|]\n\n    expectCounters\n      [\n        requests (+ 2)\n      , hits     (+ 1)\n      ] $\n\n         request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 401\n      *> request methodGet \"/authors_only\" [auth] \"\" `shouldRespondWith` 401\n\n  it \"Should evict entries from the JWT cache (jwt cache max is 2)\" $ do\n    let jwt1 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe3\"}|]\n        jwt2 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe4\"}|]\n        jwt3 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe5\"}|]\n\n    expectCounters\n      [\n        requests  (+ 6)\n      , hits      (+ 0)\n      , evictions (+ 4)\n      ] $\n\n         request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n      *> request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n\n  it \"Should not evict entries from the JWT cache in FIFO order\" $ do\n    let jwt1 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe6\"}|]\n        jwt2 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe7\"}|]\n        jwt3 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe8\"}|]\n\n    expectCounters\n      [\n        requests  (+ 6)\n      , hits      (+ 3)\n      , evictions (+ 1)\n      ] $\n\n         request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      -- this one should hit the cache\n      *> request methodGet \"/authors_only\" [jwt1] \"\"\n      -- this one should trigger eviction of jwt2 (not FIFO)\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n      -- these two should hit the cache\n      *> request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n\n  -- This one makes sure we test the scenario when finger\n  -- has to move through the whole list first and pass the head\n  -- The test case was added based on coverage report\n  -- showing this scenario was not covered by previous tests\n  it \"Should evict entries even though all were hit\" $ do\n    let jwt1 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe9\"}|]\n        jwt2 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe10\"}|]\n        jwt3 = genToken [json|{\"exp\": 9999999999, \"role\": \"postgrest_test_author\", \"id\": \"jdoe11\"}|]\n\n    expectCounters\n      [\n        requests  (+ 7)\n      , hits      (+ 4)\n      , evictions (+ 1)\n      ] $\n\n         request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      -- these two should hit the cache\n      *> request methodGet \"/authors_only\" [jwt1] \"\"\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      -- this one should trigger eviction of jwt1\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n      -- these two should hit the cache\n      *> request methodGet \"/authors_only\" [jwt2] \"\"\n      *> request methodGet \"/authors_only\" [jwt3] \"\"\n\n  where\n      genToken = authHeaderJWT . generateJWT\n      requests = expectCounter @\"jwtCacheRequests\"\n      hits = expectCounter @\"jwtCacheHits\"\n      evictions = expectCounter @\"jwtCacheEvictions\"\n      expectCounters = checkState\n"
  },
  {
    "path": "test/observability/fixtures/database.sql",
    "content": "-- Suppress NOTICE: ... messages\nSET client_min_messages TO warning;\n"
  },
  {
    "path": "test/observability/fixtures/load.sql",
    "content": "-- Loads all fixtures for the PostgREST observability tests\n\n\\set ON_ERROR_STOP on\n\n\\ir database.sql\n\\ir roles.sql\n\\ir schema.sql\n\\ir privileges.sql\n"
  },
  {
    "path": "test/observability/fixtures/privileges.sql",
    "content": "-- Schema test objects\nSET search_path = test, pg_catalog;\n\nGRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test TO postgrest_test_anonymous;\nREVOKE ALL PRIVILEGES ON TABLE authors_only FROM postgrest_test_anonymous;\n\nGRANT USAGE ON SCHEMA test TO postgrest_test_author;\nGRANT ALL ON TABLE authors_only TO postgrest_test_author;\n"
  },
  {
    "path": "test/observability/fixtures/roles.sql",
    "content": "DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_author;\nCREATE ROLE postgrest_test_anonymous;\nCREATE ROLE postgrest_test_author;\n\nGRANT postgrest_test_anonymous, postgrest_test_author TO :PGUSER;\n"
  },
  {
    "path": "test/observability/fixtures/schema.sql",
    "content": "DROP SCHEMA IF EXISTS test;\n\nCREATE SCHEMA test;\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: authors_only; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE authors_only (\n    owner character varying NOT NULL,\n    secret character varying NOT NULL\n);\n\n--\n-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY authors_only\n    ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);\n"
  },
  {
    "path": "test/pgbench/1567/new.sql",
    "content": "INSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\nSELECT pgrst_body.\"arr_data\", pgrst_body.\"field-with_sep\", pgrst_body.\"id\", pgrst_body.\"name\"\nFROM (\n  SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::jsonb as json_data\n) pgrst_payload,\nLATERAL (\n  SELECT CASE WHEN jsonb_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE jsonb_build_array(pgrst_payload.json_data) END AS val\n) pgrst_uniform_json,\nLATERAL (\n  SELECT jsonb_agg(jsonb_build_object('field-with_sep', 1) || elem) AS vals from jsonb_array_elements(pgrst_uniform_json.val) elem\n) pgrst_json_defs,\nLATERAL (\n  SELECT * FROM jsonb_to_recordset (pgrst_json_defs.vals) AS _ (\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text)\n) pgrst_body\nRETURNING \"test\".\"complex_items\".*;\n"
  },
  {
    "path": "test/pgbench/1567/old.sql",
    "content": "INSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\nSELECT pgrst_body.\"arr_data\", pgrst_body.\"field-with_sep\", pgrst_body.\"id\", pgrst_body.\"name\"\nFROM (\n  SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::jsonb as json_data\n) pgrst_payload,\nLATERAL (\n  SELECT CASE WHEN jsonb_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE jsonb_build_array(pgrst_payload.json_data) END AS val\n) pgrst_uniform_json,\nLATERAL (\n  SELECT * FROM jsonb_to_recordset (pgrst_uniform_json.val) AS _ (\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text)\n) pgrst_body\nRETURNING \"test\".\"complex_items\".*\n"
  },
  {
    "path": "test/pgbench/1652/new.sql",
    "content": "WITH pgrst_source AS (\n  SELECT pgrst_call.*\n  FROM (\n    SELECT '{\"id\": 4}'::json as json_data\n  ) pgrst_payload,\n  LATERAL (\n    SELECT CASE WHEN json_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE json_build_array(pgrst_payload.json_data) END AS val\n  ) pgrst_uniform_json,\n  LATERAL (\n    SELECT * FROM json_to_recordset(pgrst_uniform_json.val) AS _(\"id\" integer) LIMIT 1\n  ) pgrst_body,\n  LATERAL \"test\".\"get_projects_below\"(\"id\" := pgrst_body.id) pgrst_call\n)\nSELECT\n  null::bigint AS total_result_set,\n  pg_catalog.count(_postgrest_t) AS page_total,\n  coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,\n  nullif(current_setting('response.headers', true), '') AS response_headers,\n  nullif(current_setting('response.status', true), '') AS response_status\nFROM (SELECT \"projects\".* FROM \"pgrst_source\" AS \"projects\") _postgrest_t;\n"
  },
  {
    "path": "test/pgbench/1652/old.sql",
    "content": "WITH pgrst_source AS (\n  WITH\n  pgrst_payload AS (SELECT '{\"id\": 4}'::json AS json_data),\n  pgrst_body AS ( SELECT CASE WHEN json_typeof(json_data) = 'array' THEN json_data ELSE json_build_array(json_data) END AS val FROM pgrst_payload),\n  pgrst_args AS ( SELECT * FROM json_to_recordset((SELECT val FROM pgrst_body)) AS _(\"id\" integer) )\n  SELECT \"get_projects_below\".*\n  FROM \"test\".\"get_projects_below\"(\"id\" := (SELECT \"id\" FROM pgrst_args LIMIT 1))\n)\nSELECT\n  null::bigint AS total_result_set,\n  pg_catalog.count(_postgrest_t) AS page_total,\n  coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,\n  nullif(current_setting('response.headers', true), '') AS response_headers,\n  nullif(current_setting('response.status', true), '') AS response_status\nFROM (SELECT \"projects\".* FROM \"pgrst_source\" AS \"projects\") _postgrest_t;\n"
  },
  {
    "path": "test/pgbench/2676/new.sql",
    "content": "WITH pgrst_source AS (\n  INSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\n  SELECT \"pgrst_body\".\"arr_data\", \"pgrst_body\".\"field-with_sep\", \"pgrst_body\".\"id\", \"pgrst_body\".\"name\"\n  FROM (SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::json AS json_data) pgrst_payload,\n  LATERAL (SELECT \"arr_data\", \"field-with_sep\", \"id\", \"name\" FROM json_to_recordset(pgrst_payload.json_data) AS _(\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text) ) pgrst_body\n  RETURNING \"test\".\"complex_items\".*\n)\nSELECT\n  '' AS total_result_set,\n  pg_catalog.count(_postgrest_t) AS page_total,\n  array[]::text[] AS header,\n  coalesce(json_agg(_postgrest_t), '[]') AS body,\n  nullif(current_setting('response.headers', true), '') AS response_headers,\n  nullif(current_setting('response.status', true), '') AS response_status,\n  '' AS response_inserted\nFROM (SELECT \"complex_items\".* FROM \"pgrst_source\" AS \"complex_items\") _postgrest_t;\n"
  },
  {
    "path": "test/pgbench/2676/old.sql",
    "content": "WITH pgrst_source AS (\n  INSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\n  SELECT \"pgrst_body\".\"arr_data\", \"pgrst_body\".\"field-with_sep\", \"pgrst_body\".\"id\", \"pgrst_body\".\"name\"\n  FROM (SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::json AS json_data) pgrst_payload,\n  LATERAL (SELECT CASE WHEN json_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE json_build_array(pgrst_payload.json_data) END AS val) pgrst_uniform_json,\n  LATERAL (SELECT \"arr_data\", \"field-with_sep\", \"id\", \"name\" FROM json_to_recordset(pgrst_uniform_json.val) AS _(\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text) ) pgrst_body\n  RETURNING \"test\".\"complex_items\".*\n)\nSELECT\n  '' AS total_result_set,\n  pg_catalog.count(_postgrest_t) AS page_total,\n  array[]::text[] AS header,\n  coalesce(json_agg(_postgrest_t), '[]') AS body,\n  nullif(current_setting('response.headers', true), '') AS response_headers,\n  nullif(current_setting('response.status', true), '') AS response_status,\n  '' AS response_inserted\nFROM (SELECT \"complex_items\".* FROM \"pgrst_source\" AS \"complex_items\") _postgrest_t;\n"
  },
  {
    "path": "test/pgbench/2677/new.sql",
    "content": "INSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\nSELECT pgrst_body.\"arr_data\", pgrst_body.\"field-with_sep\", pgrst_body.\"id\", pgrst_body.\"name\"\nFROM (\n  SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::json as json_data\n) pgrst_payload,\nLATERAL (\n  SELECT CASE WHEN json_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE json_build_array(pgrst_payload.json_data) END AS val\n) pgrst_uniform_json,\nLATERAL (\n  SELECT * FROM json_to_recordset (pgrst_uniform_json.val) AS _ (\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text)\n) pgrst_body\nRETURNING \"test\".\"complex_items\".*\n"
  },
  {
    "path": "test/pgbench/2677/old.sql",
    "content": "WITH\npgrst_payload AS (SELECT '[{\"id\": 4, \"name\": \"Vier\"}, {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null}, {\"id\": 6, \"name\": \"Sechs\", \"arr_data\": [1, 2, 3], \"field-with_sep\": 6}]'::json AS json_data),\npgrst_body AS ( SELECT CASE WHEN json_typeof(json_data) = 'array' THEN json_data ELSE json_build_array(json_data) END AS val FROM pgrst_payload)\nINSERT INTO \"test\".\"complex_items\"(\"arr_data\", \"field-with_sep\", \"id\", \"name\")\nSELECT \"arr_data\", \"field-with_sep\", \"id\", \"name\"\nFROM json_to_recordset ((SELECT val FROM pgrst_body)) AS _ (\"arr_data\" integer[], \"field-with_sep\" integer, \"id\" bigint, \"name\" text)\nRETURNING \"test\".\"complex_items\".*\n"
  },
  {
    "path": "test/pgbench/README.md",
    "content": "## pgbench tests\n\nCan be used as:\n\n```\npostgrest-with-pg-15 -f test/pgbench/fixtures.sql pgbench -U postgres -n -T 10 -f test/pgbench/1567/old.sql\n\npostgrest-with-pg-15 -f test/pgbench/fixtures.sql pgbench -U postgres -n -T 10 -f test/pgbench/1567/new.sql\n```\n\n## Directory structure\n\nThe directory name is the issue number on github.\n"
  },
  {
    "path": "test/pgbench/fixtures.sql",
    "content": "\\ir ../spec/fixtures/load.sql\n\nALTER TABLE test.complex_items\n    DROP CONSTRAINT complex_items_pkey;\n\nALTER TABLE test.complex_items\n    ALTER COLUMN \"field-with_sep\" DROP NOT NULL;\n"
  },
  {
    "path": "test/spec/Feature/Auth/AsymmetricJwtSpec.hs",
    "content": "module Feature.Auth.AsymmetricJwtSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"server started with asymmetric JWK\" $\n\n  -- this test will stop working 9999999999s after the UNIX EPOCH\n  it \"succeeds with jwt token signed with an asymmetric key\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Auth/AudienceJwtSecretSpec.hs",
    "content": "module Feature.Auth.AudienceJwtSecretSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Protolude           hiding (get)\nimport SpecHelper\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nspec :: SpecWith ((), Application)\nspec = describe \"test handling of aud claims in JWT when the jwt-aud config is set\" $ do\n\n  context \"when the audience claim is a string\" $ do\n    -- this test will stop working 9999999999s after the UNIX EPOCH\n    it \"succeeds when the audience claim matches\" $ do\n      let jwtPayload =\n            [json|{\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": \"youraudience\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"fails when the audience claim does not match\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": \"notyouraudience\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith`\n          [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"JWT not in audience\"}|]\n          { matchStatus = 401 }\n\n    it \"fails when the audience claim is empty\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": \"\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith`\n          [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"JWT not in audience\"}|]\n          { matchStatus = 401 }\n\n  context \"when the audience claim is an array of strings\" $ do\n    it \"succeeds when the audience claim has 1 element and it matches\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"youraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"succeeds when the audience claim has more than 1 element and one matches\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"notyouraudience\", \"youraudience\", \"anotheraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"fails when the audience claim has 1 element and it doesn't match\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"notyouraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith`\n          [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"JWT not in audience\"}|]\n          { matchStatus = 401 }\n\n\n    it \"fails when the audience claim has more than 1 element and none matches\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"notyouraudience\", \"stillnotyouraudience\", \"anotheraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith`\n          [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"JWT not in audience\"}|]\n          { matchStatus = 401 }\n\n    it \"ignores the audience claim and succeeds when it's empty\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": []\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"ignores the audience claim and succeeds when it's null\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": null\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n  context \"when the audience claim is not present\" $ do\n    it \"succeeds with a JWT with no audience claim\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"succeeds without a JWT\" $\n      get \"/has_count_column\" `shouldRespondWith` 200\n\ndisabledSpec :: SpecWith ((), Application)\ndisabledSpec = describe \"test handling of aud claims in JWT when the jwt-aud config is not set\" $ do\n\n  context \"when the audience claim is a string\" $ do\n    it \"ignores the audience claim and suceeds\" $ do\n      let jwtPayload =\n            [json|{\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": \"youraudience\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"ignores the audience claim and suceeds when it's empty\" $ do\n      let jwtPayload =\n            [json|{\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": \"\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n  context \"when the audience is an array of strings\" $ do\n    it \"ignores the audience claim and suceeds when it has 1 element\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"youraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"ignores the audience claim and suceeds when it has more than 1 element\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": [\"notyouraudience\", \"youraudience\", \"anotheraudience\"]\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"ignores the audience claim and suceeds when it's empty\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": []\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"ignores the audience claim and succeeds when it's null\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\",\n              \"aud\": null\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n  context \"when the audience claim is not present\" $ do\n    it \"succeeds with a JWT with no audience claim\" $ do\n      let jwtPayload = [json|\n            {\n              \"exp\": 9999999999,\n              \"role\": \"postgrest_test_author\",\n              \"id\": \"jdoe\"\n            }|]\n          auth = authHeaderJWT $ generateJWT jwtPayload\n      request methodGet \"/authors_only\" [auth] \"\"\n        `shouldRespondWith` 200\n\n    it \"succeeds without a JWT\" $\n      get \"/has_count_column\" `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Auth/AuthSpec.hs",
    "content": "module Feature.Auth.AuthSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"authorization\" $ do\n  let single = (\"Accept\",\"application/vnd.pgrst.object+json\")\n\n  it \"denies access to tables that anonymous does not own\" $\n    get \"/authors_only\" `shouldRespondWith`\n      [json| {\n        \"hint\":null,\n        \"details\":null,\n        \"code\":\"42501\",\n        \"message\":\"permission denied for table authors_only\"} |]\n      { matchStatus = 401\n      , matchHeaders = [ \"WWW-Authenticate\" <:> \"Bearer\"\n                       , \"Content-Length\" <:> \"96\" ]\n      }\n\n  it \"denies access to tables that postgrest_test_author does not own\" $\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\" in\n    request methodGet \"/private_table\" [auth] \"\"\n      `shouldRespondWith`\n      [json| {\n        \"hint\":null,\n        \"details\":null,\n        \"code\":\"42501\",\n        \"message\":\"permission denied for table private_table\"} |]\n      { matchStatus = 403\n      , matchHeaders = [\"Content-Length\" <:> \"97\"]\n      }\n\n  it \"denies execution on functions that anonymous does not own\" $\n    post \"/rpc/privileged_hello\" [json|{\"name\": \"anonymous\"}|] `shouldRespondWith` 401\n\n  it \"allows execution on a function that postgrest_test_author owns\" $\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\" in\n    request methodPost \"/rpc/privileged_hello\" [auth] [json|{\"name\": \"jdoe\"}|]\n      `shouldRespondWith` [json|\"Privileged hello to jdoe\"|]\n      { matchStatus = 200\n      , matchHeaders = [matchContentTypeJson]\n      }\n\n  it \"returns jwt functions as jwt tokens\" $\n    request methodPost \"/rpc/login\" [single]\n      [json| { \"id\": \"jdoe\", \"pass\": \"1234\" } |]\n      `shouldRespondWith` [json| {\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs\"} |]\n        { matchStatus = 200\n        , matchHeaders = [matchContentTypeSingular]\n        }\n\n  it \"sql functions can encode custom and standard claims\" $\n    request methodPost  \"/rpc/jwt_test\" [single] \"{}\"\n      `shouldRespondWith` [json| {\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek\"} |]\n        { matchStatus = 200\n        , matchHeaders = [matchContentTypeSingular]\n        }\n\n  it \"sql functions can read custom and standard claims variables\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg\"\n    request methodPost \"/rpc/reveal_big_jwt\" [auth] \"{}\"\n      `shouldRespondWith` [json|[{\"iss\":\"joe\",\"sub\":\"fun\",\"exp\":9999999999,\"nbf\":1300819380,\"iat\":1300819380,\"jti\":\"foo\",\"http://postgrest.com/foo\":true}]|]\n\n  it \"allows users with permissions to see their tables\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n\n  it \"works with tokens which have extra fields\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n\n  -- this test will stop working 9999999999s after the UNIX EPOCH\n  it \"succeeds with an unexpired token\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n\n  it \"fails when auth header is sent empty\" $ do\n    let auth = authHeaderJWT \"\"\n    request methodGet \"/authors_only\" [auth] \"\"\n    `shouldRespondWith` [json| {\"message\":\"Empty JWT is sent in Authorization header\",\"code\":\"PGRST301\",\"hint\":null,\"details\":null} |]\n      { matchStatus = 401\n      , matchHeaders = [\n          \"WWW-Authenticate\" <:>\n          \"Bearer error=\\\"invalid_token\\\", error_description=\\\"Empty JWT is sent in Authorization header\\\"\",\n          \"Content-Length\" <:> \"100\"\n        ]\n      }\n\n  it \"fails with an expired token\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` [json| {\"message\":\"JWT expired\",\"code\":\"PGRST303\",\"hint\":null,\"details\":null} |]\n        { matchStatus = 401\n        , matchHeaders = [\n            \"WWW-Authenticate\" <:>\n            \"Bearer error=\\\"invalid_token\\\", error_description=\\\"JWT expired\\\"\"\n          ]\n        }\n\n  it \"hides tables from users with invalid JWT\" $ do\n    let auth = authHeaderJWT \"ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` [json| {\"message\":\"Expected 3 parts in JWT; got 2\",\"code\":\"PGRST301\",\"hint\":null,\"details\":null} |]\n        { matchStatus = 401\n        , matchHeaders = [\n            \"WWW-Authenticate\" <:>\n            \"Bearer error=\\\"invalid_token\\\", error_description=\\\"Expected 3 parts in JWT; got 2\\\"\"\n          ]\n        }\n\n  it \"should fail when jwt contains no claims\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 401\n\n  it \"hides tables from users with JWT that contain no claims about role\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 401\n\n  it \"recovers after 401 error with logged in user\" $ do\n    _ <- post \"/authors_only\" [json| { \"owner\": \"jdoe\", \"secret\": \"test content\" } |]\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0\"\n    _ <- request methodPost \"/rpc/problem\" [auth] \"\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n\n  it \"fails when the exp claim is not a number\" $ do\n    let jwtPayload = [json|\n          {\n            \"exp\": \"invalid\",\n            \"role\": \"postgrest_test_author\"\n          }|]\n        auth = authHeaderJWT $ generateJWT jwtPayload\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith`\n        [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"The JWT 'exp' claim must be a number\"}|]\n        { matchStatus = 401 }\n\n  it \"fails when the nbf claim is not a number\" $ do\n    let jwtPayload = [json|\n          {\n            \"nbf\": \"invalid\",\n            \"role\": \"postgrest_test_author\"\n          }|]\n        auth = authHeaderJWT $ generateJWT jwtPayload\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith`\n        [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"The JWT 'nbf' claim must be a number\"}|]\n        { matchStatus = 401 }\n\n  it \"fails when the iat claim is not a number\" $ do\n    let jwtPayload = [json|\n          {\n            \"iat\": \"invalid\",\n            \"role\": \"postgrest_test_author\"\n          }|]\n        auth = authHeaderJWT $ generateJWT jwtPayload\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith`\n        [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"The JWT 'iat' claim must be a number\"}|]\n        { matchStatus = 401 }\n\n  it \"fails when the aud claim has a single value and it's not a string\" $ do\n    let jwtPayload = [json|\n          {\n            \"aud\": {\"invalid\": \"value\"},\n            \"role\": \"postgrest_test_author\"\n          }|]\n        auth = authHeaderJWT $ generateJWT jwtPayload\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith`\n        [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"The JWT 'aud' claim must be a string or an array of strings\"}|]\n        { matchStatus = 401 }\n\n  it \"fails when the aud claim is an array but it has non-string elements\" $ do\n    let jwtPayload = [json|\n          {\n            \"aud\": [{\"invalid\": \"value\"}, \"test\"],\n            \"role\": \"postgrest_test_author\"\n          }|]\n        auth = authHeaderJWT $ generateJWT jwtPayload\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith`\n        [json|{\"code\":\"PGRST303\",\"details\":null,\"hint\":null,\"message\":\"The JWT 'aud' claim must be a string or an array of strings\"}|]\n        { matchStatus = 401 }\n\n  describe \"custom pre-request proc acting on id claim\" $ do\n\n    it \"able to switch to postgrest_test_author role (id=1)\" $\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE\" in\n      request methodPost \"/rpc/get_current_user\" [auth]\n        [json| {} |]\n         `shouldRespondWith` [json|\"postgrest_test_author\"|]\n          { matchStatus = 200\n          , matchHeaders = []\n          }\n\n    it \"able to switch to postgrest_test_default_role (id=2)\" $\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A\" in\n      request methodPost \"/rpc/get_current_user\" [auth]\n        [json| {} |]\n         `shouldRespondWith` [json|\"postgrest_test_default_role\"|]\n          { matchStatus = 200\n          , matchHeaders = []\n          }\n\n    it \"raises error (id=3)\" $\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM\" in\n      request methodPost \"/rpc/get_current_user\" [auth]\n        [json| {} |]\n         `shouldRespondWith` [json|{\"hint\":\"Please contact administrator\",\"details\":null,\"code\":\"P0001\",\"message\":\"Disabled ID --> 3\"}|]\n          { matchStatus = 400\n          , matchHeaders = []\n          }\n\n  it \"allows 'Bearer' and 'bearer' as authentication schemes\" $ do\n    let token = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0\"\n    request methodGet \"/authors_only\" [authHeader \"Bearer\" token] \"\"\n      `shouldRespondWith` 200\n    request methodGet \"/authors_only\" [authHeader \"bearer\" token] \"\"\n      `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Auth/BinaryJwtSecretSpec.hs",
    "content": "module Feature.Auth.BinaryJwtSecretSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"server started with binary JWT secret\" $\n\n  -- this test will stop working 9999999999s after the UNIX EPOCH\n  it \"succeeds with jwt token encoded with a binary secret\" $ do\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA\"\n    request methodGet \"/authors_only\" [auth] \"\"\n      `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Auth/NoAnonSpec.hs",
    "content": "module Feature.Auth.NoAnonSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"server started without anonymous role\" $ do\n  it \"behaves normally on attempted auth\" $ do\n    -- token body: { \"role\": \"postgrest_test_author\" }\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"\n    request methodGet \"/authors_only\"\n        [auth]\n        \"\"\n      `shouldRespondWith`\n        200\n\n  it \"responds with error when user does not attempt auth\" $\n    get \"/items\"\n      `shouldRespondWith`\n        [json|\n          {\"hint\": null,\n           \"details\": null,\n           \"code\": \"PGRST302\",\n           \"message\":\"Anonymous access is disabled\"}|]\n        { matchStatus  = 401\n        , matchHeaders = [\"WWW-Authenticate\" <:> \"Bearer\"]\n        }\n"
  },
  {
    "path": "test/spec/Feature/Auth/NoJwtSecretSpec.hs",
    "content": "module Feature.Auth.NoJwtSecretSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"server started without JWT secret\" $ do\n\n  it \"responds with error on attempted auth\" $ do\n    -- token body: { \"role\": \"postgrest_test_author\" }\n    let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"\n    request methodGet \"/authors_only\"\n        [auth]\n        \"\"\n      `shouldRespondWith`\n        [json|\n          {\"hint\": null,\n           \"details\": null,\n           \"code\": \"PGRST300\",\n           \"message\": \"Server lacks JWT secret\"}|]\n        { matchStatus  = 500 }\n\n  it \"behaves normally when user does not attempt auth\" $\n    get \"/items\" `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/ConcurrentSpec.hs",
    "content": "{-# LANGUAGE MultiParamTypeClasses #-}\n{-# LANGUAGE TypeFamilies          #-}\n{-# LANGUAGE UndecidableInstances  #-}\n{-# OPTIONS_GHC -fno-warn-orphans #-}\nmodule Feature.ConcurrentSpec where\n\nimport Control.Concurrent.Async (mapConcurrently)\nimport Network.Wai              (Application)\n\nimport Control.Monad.Base\nimport Control.Monad.Trans.Control\n\nimport Network.Wai.Test        (Session)\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.Internal\nimport Test.Hspec.Wai.JSON\n\nimport Protolude hiding (get)\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Querying in parallel\" $\n    it \"should not raise 'transaction in progress' error\" $\n      raceTest 10 $\n        get \"/fakefake\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.fakefake' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = []\n          }\n\nraceTest :: Int -> WaiExpectation st -> WaiExpectation st\nraceTest times = liftBaseDiscard go\n where\n  go test = void $ mapConcurrently (const test) [1..times]\n\ninstance MonadBaseControl IO (WaiSession st) where\n  type StM (WaiSession st) a = StM Session a\n  liftBaseWith f = WaiSession $\n    liftBaseWith $ \\runInBase ->\n      f $ \\k -> runInBase (unWaiSession k)\n  restoreM = WaiSession . restoreM\n  {-# INLINE liftBaseWith #-}\n  {-# INLINE restoreM #-}\n\ninstance MonadBase IO (WaiSession st) where\n  liftBase = liftIO\n"
  },
  {
    "path": "test/spec/Feature/CorsSpec.hs",
    "content": "module Feature.CorsSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"CORS\" $ do\n    it \"replies naively and permissively to preflight request\" $\n      request methodOptions \"/\"\n          [ (\"Accept\", \"*/*\")\n          , (\"Origin\", \"http://example.com\")\n          , (\"Access-Control-Request-Method\", \"POST\")\n          , (\"Access-Control-Request-Headers\", \"Foo,Bar\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"Access-Control-Allow-Origin\" <:> \"*\"\n                           , \"Access-Control-Allow-Methods\" <:> \"GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD\"\n                           , \"Access-Control-Allow-Headers\" <:> \"Authorization, Foo, Bar, Accept, Accept-Language, Content-Language\"\n                           , \"Access-Control-Max-Age\" <:> \"86400\" ]\n          }\n\n    it \"exposes necesssary response headers to regular request\" $\n      request methodGet \"/items\"\n          [(\"Origin\", \"http://example.com\")]\n          \"\"\n        `shouldRespondWith`\n          ResponseMatcher\n          { matchStatus = 200\n          , matchBody = MatchBody (\\_ _ -> Nothing) -- match any body\n          , matchHeaders = [ \"Access-Control-Expose-Headers\" <:>\n                             \"Content-Encoding, Content-Location, Content-Range, Content-Type, \\\n                             \\Date, Location, Server, Transfer-Encoding, Range-Unit\"]\n          }\n\n    it \"allows INFO body through even with CORS request headers present to postflight request\" $ do\n      request methodOptions \"/items\"\n          [ (\"Host\", \"localhost:3000\")\n          , (\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0\")\n          , (\"Origin\", \"http://localhost:8000\")\n          , (\"Accept\", \"text/csv, */*; q=0.01\")\n          , (\"Accept-Language\", \"en-US,en;q=0.5\")\n          , (\"Accept-Encoding\", \"gzip, deflate\")\n          , (\"Referer\", \"http://localhost:8000/\")\n          , (\"Connection\", \"keep-alive\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"Access-Control-Allow-Origin\" <:> \"*\" ] }\n\n      request methodOptions \"/items\"\n          [ (\"Accept\", \"application/json\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"Access-Control-Allow-Origin\" <:> \"*\" ] }\n\n      request methodOptions \"/shops\"\n          [ (\"Accept\", \"application/geo+json\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"Access-Control-Allow-Origin\" <:> \"*\" ] }\n"
  },
  {
    "path": "test/spec/Feature/ExtraSearchPathSpec.hs",
    "content": "module Feature.ExtraSearchPathSpec where\n\nimport Network.HTTP.Types\nimport Network.Wai         (Application)\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"extra search path\" $ do\n\n  it \"finds the ltree <@ operator on the public schema\" $\n    request methodGet \"/ltree_sample?path=cd.Top.Science.Astronomy\" [] \"\"\n      `shouldRespondWith` [json|[\n        {\"path\":\"Top.Science.Astronomy\"},\n        {\"path\":\"Top.Science.Astronomy.Astrophysics\"},\n        {\"path\":\"Top.Science.Astronomy.Cosmology\"}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"finds the ltree ~ operator on the public schema\" $ do\n    request methodGet \"/ltree_sample?path=match.*.Science\" [] \"\"\n      `shouldRespondWith` [json|[\n        {\"path\": \"Top.Science\"}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n    request methodGet \"/ltree_sample?path=match.*.Astronomy.*\" [] \"\"\n      `shouldRespondWith` [json|[\n        {\"path\": \"Top.Science.Astronomy\"},\n        {\"path\": \"Top.Science.Astronomy.Astrophysics\"},\n        {\"path\": \"Top.Science.Astronomy.Cosmology\"},\n        {\"path\": \"Top.Collections.Pictures.Astronomy\"},\n        {\"path\": \"Top.Collections.Pictures.Astronomy.Stars\"},\n        {\"path\": \"Top.Collections.Pictures.Astronomy.Galaxies\"},\n        {\"path\": \"Top.Collections.Pictures.Astronomy.Astronauts\"}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n    request methodGet \"/ltree_sample?path=match.*.Collections.*{1,2}\" [] \"\"\n      `shouldRespondWith` [json|[\n        {\"path\": \"Top.Collections.Pictures\"},\n        {\"path\": \"Top.Collections.Pictures.Astronomy\"}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"finds the ltree nlevel function on the public schema, used through a computed column\" $\n    request methodGet \"/ltree_sample?select=number_of_labels&path=eq.Top.Science\" [] \"\"\n      `shouldRespondWith` [json|[{\"number_of_labels\":2}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"finds the isn = operator on the extensions schema\" $\n    request methodGet \"/isn_sample?id=eq.978-0-393-04002-9&select=name\" [] \"\"\n      `shouldRespondWith` [json|[{\"name\":\"Mathematics: From the Birth of Numbers\"}]|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"finds the isn is_valid function on the extensions schema\" $\n    request methodGet \"/rpc/is_valid_isbn?input=978-0-393-04002-9\" [] \"\"\n      `shouldRespondWith` [json|true|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"finds a function on a schema with uppercase and special characters in its name\" $\n    request methodGet \"/rpc/special_extended_schema?val=value\" [] \"\"\n      `shouldRespondWith` [json|\"value\"|]\n      { matchHeaders = [matchContentTypeJson] }\n\n  it \"can detect fk relations through multiple views recursively when middle views are in extra search path\" $\n    get \"/consumers_extra_view?select=*,orders_view(*)\" `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/NoSuperuserSpec.hs",
    "content": "module Feature.NoSuperuserSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"No Superuser\" $ do\n    it \"proves that the authenticator role is not a superuser\" $ do\n      request methodGet \"/rpc/is_superuser\"\n          mempty\n          \"\"\n        `shouldRespondWith`\n          \"false\"\n          { matchStatus = 200 }\n"
  },
  {
    "path": "test/spec/Feature/ObservabilitySpec.hs",
    "content": "module Feature.ObservabilitySpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Observability\" $ do\n    it \"includes the server trace header on the response\" $ do\n      request methodHead \"/\"\n          [ (\"X-Request-Id\", \"1\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"X-Request-Id\" <:> \"1\"] }\n\n      request methodHead \"/projects\"\n          [ (\"X-Request-Id\", \"2\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"X-Request-Id\" <:> \"2\"] }\n\n      request methodHead \"/rpc/add_them?a=2&b=4\"\n          [ (\"X-Request-Id\", \"3\") ]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"X-Request-Id\" <:> \"3\"] }\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/DisabledOpenApiSpec.hs",
    "content": "module Feature.OpenApi.DisabledOpenApiSpec where\n\nimport Network.HTTP.Types\nimport Network.Wai        (Application)\n\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Disabled OpenApi\" $ do\n    it \"responds with 404\" $\n      request methodGet \"/\"\n        [(\"Accept\",\"application/openapi+json\")] \"\"\n        `shouldRespondWith`\n        [json| {\"code\":\"PGRST126\",\"details\":null,\"hint\":null,\"message\":\"Root endpoint metadata is disabled\"} |]\n        { matchStatus = 404\n        , matchHeaders = [\"Content-Length\" <:> \"93\"]}\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/IgnorePrivOpenApiSpec.hs",
    "content": "module Feature.OpenApi.IgnorePrivOpenApiSpec where\n\nimport Control.Lens ((^?))\n\nimport Data.Aeson.Lens\nimport Data.Aeson.QQ\n\nimport Network.HTTP.Types\nimport Network.Wai        (Application)\nimport Network.Wai.Test   (SResponse (..))\n\nimport Test.Hspec     hiding (pendingWith)\nimport Test.Hspec.Wai\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"OpenAPI Ignore Privileges\" $ do\n  it \"root path returns a valid openapi spec\" $ do\n    validateOpenApiResponse [(\"Accept\", \"application/openapi+json\")]\n    request methodHead \"/\"\n        (acceptHdrs \"application/openapi+json\")\n        \"\"\n      `shouldRespondWith`\n        \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/openapi+json; charset=utf-8\"\n                         , matchHeaderAbsent hContentLength]\n        }\n\n  describe \"table\" $ do\n\n    it \"includes privileged table even if user does not have permission\" $ do\n      r <- simpleBody <$> get \"/\"\n      let tableTag = r ^? key \"paths\" . key \"/authors_only\"\n                     . key \"post\" . key \"tags\"\n                     . nth 0\n\n      liftIO $ tableTag `shouldBe` Just [aesonQQ|\"authors_only\"|]\n\n    it \"only includes tables that belong to another schema if the Accept-Profile header is used\" $ do\n      r1 <- simpleBody <$> get \"/\"\n      let tableKey1 = r1 ^? key \"paths\" . key \"/children\"\n\n      liftIO $ tableKey1 `shouldBe` Nothing\n\n      r2 <- simpleBody <$> request methodGet \"/\" [(\"Accept-Profile\", \"v1\")] \"\"\n      let tableKey2 = r2 ^? key \"paths\" . key \"/children\"\n\n      liftIO $ tableKey2 `shouldNotBe` Nothing\n\n    it \"includes comments on tables\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let grandChildGet s = key \"paths\" . key \"/grandchild_entities\" . key \"get\" . key s\n          grandChildGetSummary = r ^? grandChildGet \"summary\"\n          grandChildGetDescription = r ^? grandChildGet \"description\"\n\n      liftIO $ do\n        grandChildGetSummary `shouldBe` Just \"grandchild_entities summary\"\n        grandChildGetDescription `shouldBe` Just \"grandchild_entities description\\nthat spans\\nmultiple lines\"\n\n  describe \"RPC\" $ do\n\n    it \"includes privileged function even if user does not have permission\" $ do\n      r <- simpleBody <$> get \"/\"\n      let funcTag = r ^? key \"paths\" . key \"/rpc/privileged_hello\"\n                    . key \"post\" . key \"tags\"\n                    . nth 0\n\n      liftIO $ funcTag `shouldBe` Just [aesonQQ|\"(rpc) privileged_hello\"|]\n\n    it \"only includes functions that belong to another schema if the Accept-Profile header is used\" $ do\n      r1 <- simpleBody <$> get \"/\"\n      let funcKey1 = r1 ^? key \"paths\" . key \"/rpc/get_parents_below\"\n\n      liftIO $ funcKey1 `shouldBe` Nothing\n\n      r2 <- simpleBody <$> request methodGet \"/\" [(\"Accept-Profile\", \"v1\")] \"\"\n      let funcKey2 = r2 ^? key \"paths\" . key \"/rpc/get_parents_below\"\n\n      liftIO $ funcKey2 `shouldNotBe` Nothing\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/OpenApiSpec.hs",
    "content": "module Feature.OpenApi.OpenApiSpec where\n\nimport Control.Lens     ((^?))\nimport Data.Aeson.Types (Value (..))\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (..))\n\nimport Data.Aeson.Lens\nimport Data.Aeson.QQ\nimport Network.HTTP.Types\nimport Test.Hspec         hiding (pendingWith)\nimport Test.Hspec.Wai\n\nimport PostgREST.Version (docsVersion)\nimport Protolude         hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"OpenAPI\" $ do\n  it \"root path returns a valid openapi spec\" $ do\n    validateOpenApiResponse [(\"Accept\", \"application/openapi+json\")]\n    request methodHead \"/\"\n        (acceptHdrs \"application/openapi+json\") \"\"\n      `shouldRespondWith`\n        \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/openapi+json; charset=utf-8\"\n                         , matchHeaderAbsent hContentLength ]\n        }\n\n  it \"should respond to openapi request on none root path with 406\" $\n    request methodGet \"/items\"\n            (acceptHdrs \"application/openapi+json\") \"\"\n      `shouldRespondWith` 406\n\n  it \"should respond to openapi request with unsupported media type with 406\" $\n    request methodGet \"/\"\n            (acceptHdrs \"text/csv\") \"\"\n      `shouldRespondWith` 406\n\n  it \"includes postgrest.org current version api docs\" $ do\n    r <- get \"/\"\n\n    let headers = simpleHeaders r\n        docsUrl = simpleBody r ^? key \"externalDocs\" . key \"url\"\n\n    liftIO $ do\n      headers `shouldSatisfy` notZeroContentLength\n      docsUrl `shouldBe` Just (String (\"https://postgrest.org/en/\" <> docsVersion <> \"/references/api.html\"))\n\n  describe \"schema\" $ do\n\n    it \"includes title and comments to schema\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let childGetTitle = r ^? key \"info\" . key \"title\"\n      let childGetDescription = r ^? key \"info\" . key \"description\"\n\n      liftIO $ do\n\n        childGetTitle `shouldBe` Just \"My API title\"\n\n        childGetDescription `shouldBe` Just \"My API description\\nthat spans\\nmultiple lines\"\n\n  describe \"table\" $ do\n\n    it \"includes paths to tables\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/child_entities\" . key s\n          childGetSummary = r ^? method \"get\" . key \"summary\"\n          childGetDescription = r ^? method \"get\" . key \"description\"\n          getParameters = r ^? method \"get\" . key \"parameters\"\n          postParameters = r ^? method \"post\" . key \"parameters\"\n          postResponse = r ^? method \"post\" . key \"responses\" . key \"201\" . key \"description\"\n          patchResponse = r ^? method \"patch\" . key \"responses\" . key \"204\" . key \"description\"\n          deleteResponse = r ^? method \"delete\" . key \"responses\" . key \"204\" . key \"description\"\n\n      let grandChildGet s = key \"paths\" . key \"/grandchild_entities\" . key \"get\" . key s\n          grandChildGetSummary = r ^? grandChildGet \"summary\"\n          grandChildGetDescription = r ^? grandChildGet \"description\"\n\n      liftIO $ do\n\n        childGetSummary `shouldBe` Just \"child_entities comment\"\n\n        childGetDescription `shouldBe` Nothing\n\n        grandChildGetSummary `shouldBe` Just \"grandchild_entities summary\"\n\n        grandChildGetDescription `shouldBe` Just \"grandchild_entities description\\nthat spans\\nmultiple lines\"\n\n        getParameters `shouldBe` Just\n          [aesonQQ|\n            [\n              { \"$ref\": \"#/parameters/rowFilter.child_entities.id\" },\n              { \"$ref\": \"#/parameters/rowFilter.child_entities.name\" },\n              { \"$ref\": \"#/parameters/rowFilter.child_entities.parent_id\" },\n              { \"$ref\": \"#/parameters/select\" },\n              { \"$ref\": \"#/parameters/order\" },\n              { \"$ref\": \"#/parameters/range\" },\n              { \"$ref\": \"#/parameters/rangeUnit\" },\n              { \"$ref\": \"#/parameters/offset\" },\n              { \"$ref\": \"#/parameters/limit\" },\n              { \"$ref\": \"#/parameters/preferCount\" }\n            ]\n          |]\n\n        postParameters `shouldBe` Just\n          [aesonQQ|\n            [\n              { \"$ref\": \"#/parameters/body.child_entities\" },\n              { \"$ref\": \"#/parameters/select\" },\n              { \"$ref\": \"#/parameters/preferPost\" }\n            ]\n          |]\n\n        postResponse `shouldBe` Just \"Created\"\n\n        patchResponse `shouldBe` Just \"No Content\"\n\n        deleteResponse `shouldBe` Just \"No Content\"\n\n    it \"includes an array type for GET responses\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let childGetSchema = r ^? key \"paths\"\n            . key \"/child_entities\"\n            . key \"get\"\n            . key \"responses\"\n            . key \"200\"\n            . key \"schema\"\n\n      liftIO $\n        childGetSchema `shouldBe` Just\n          [aesonQQ|\n            {\n              \"items\": {\n                \"$ref\": \"#/definitions/child_entities\"\n              },\n              \"type\": \"array\"\n            }\n          |]\n\n    it \"includes definitions to tables\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let def = r ^? key \"definitions\" . key \"child_entities\"\n\n      liftIO $\n\n        def `shouldBe` Just\n          [aesonQQ|\n            {\n              \"type\": \"object\",\n              \"description\": \"child_entities comment\",\n              \"properties\": {\n                \"id\": {\n                  \"description\": \"child_entities id comment\\n\\nNote:\\nThis is a Primary Key.<pk/>\",\n                  \"format\": \"integer\",\n                  \"type\": \"integer\"\n                },\n                \"name\": {\n                  \"description\": \"child_entities name comment. Can be longer than sixty-three characters long\",\n                  \"format\": \"text\",\n                  \"type\": \"string\"\n                },\n                \"parent_id\": {\n                  \"description\": \"Note:\\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>\",\n                  \"format\": \"integer\",\n                  \"type\": \"integer\"\n                }\n              },\n              \"required\": [\n                \"id\"\n              ]\n            }\n          |]\n\n    it \"includes definitions to views\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let def = r ^? key \"definitions\" . key \"child_entities_view\"\n\n      liftIO $\n\n        def `shouldBe` Just\n          [aesonQQ|\n            {\n              \"type\": \"object\",\n              \"description\": \"child_entities_view comment\",\n              \"properties\": {\n                \"id\": {\n                  \"description\": \"child_entities_view id comment\\n\\nNote:\\nThis is a Primary Key.<pk/>\",\n                  \"format\": \"integer\",\n                  \"type\": \"integer\"\n                },\n                \"name\": {\n                  \"description\": \"child_entities_view name comment. Can be longer than sixty-three characters long\",\n                  \"format\": \"text\",\n                  \"type\": \"string\"\n                },\n                \"parent_id\": {\n                  \"description\": \"Note:\\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>\",\n                  \"format\": \"integer\",\n                  \"type\": \"integer\"\n                }\n              }\n            }\n          |]\n\n    it \"doesn't include privileged table for anonymous\" $ do\n      r <- simpleBody <$> get \"/\"\n      let tablePath = r ^? key \"paths\" . key \"/authors_only\"\n\n      liftIO $ tablePath `shouldBe` Nothing\n\n    it \"includes table if user has permission\" $ do\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"\n      r <- simpleBody <$> request methodGet \"/\" [auth] \"\"\n      let tableTag = r ^? key \"paths\" . key \"/authors_only\"\n                    . key \"post\"  . key \"tags\"\n                    . nth 0\n      liftIO $ tableTag `shouldBe` Just [aesonQQ|\"authors_only\"|]\n\n    it \"includes a fk description for a O2O relationship\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let referralLink = r ^? key \"definitions\" . key \"first\" . key \"properties\" . key \"second_id_1\"\n\n      liftIO $\n        referralLink `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"integer\",\n              \"type\": \"integer\",\n              \"description\": \"Note:\\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>\"\n            }\n          |]\n\n  describe \"Foreign table\" $\n\n    it \"includes foreign table properties\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/projects_dump\" . key s\n          getSummary = r ^? method \"get\" . key \"summary\"\n          getDescription = r ^? method \"get\" . key \"description\"\n          getParameters = r ^? method \"get\" . key \"parameters\"\n\n      liftIO $ do\n\n        getSummary `shouldBe` Just \"A temporary projects dump\"\n\n        getDescription `shouldBe` Just \"Just a test for foreign tables\"\n\n        getParameters `shouldBe` Just\n          [aesonQQ|\n            [\n              { \"$ref\": \"#/parameters/rowFilter.projects_dump.id\" },\n              { \"$ref\": \"#/parameters/rowFilter.projects_dump.name\" },\n              { \"$ref\": \"#/parameters/rowFilter.projects_dump.client_id\" },\n              { \"$ref\": \"#/parameters/select\" },\n              { \"$ref\": \"#/parameters/order\" },\n              { \"$ref\": \"#/parameters/range\" },\n              { \"$ref\": \"#/parameters/rangeUnit\" },\n              { \"$ref\": \"#/parameters/offset\" },\n              { \"$ref\": \"#/parameters/limit\" },\n              { \"$ref\": \"#/parameters/preferCount\" }\n            ]\n          |]\n\n  describe \"Partitioned table\" $\n\n    it \"includes partitioned table properties\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/car_models\" . key s\n          getSummary = r ^? method \"get\" . key \"summary\"\n          getDescription = r ^? method \"get\" . key \"description\"\n          getParameterName = r ^? method \"get\" . key \"parameters\" . nth 0 . key \"$ref\"\n          getParameterYear = r ^? method \"get\" . key \"parameters\" . nth 1 . key \"$ref\"\n          getParameterRef = r ^? method \"get\" . key \"parameters\" . nth 2 . key \"$ref\"\n\n      liftIO $ do\n\n        getSummary `shouldBe` Just \"A partitioned table\"\n\n        getDescription `shouldBe` Just \"A test for partitioned tables\"\n\n        getParameterName `shouldBe` Just \"#/parameters/rowFilter.car_models.name\"\n\n        getParameterYear `shouldBe` Just \"#/parameters/rowFilter.car_models.year\"\n\n        getParameterRef `shouldBe` Just \"#/parameters/rowFilter.car_models.car_brand_name\"\n\n  describe \"Materialized view\" $\n\n    it \"includes materialized view properties\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/materialized_projects\" . key s\n          summary = r ^? method \"get\" . key \"summary\"\n          description = r ^? method \"get\" . key \"description\"\n          parameters = r ^? method \"get\" . key \"parameters\"\n\n      liftIO $ do\n\n        summary `shouldBe` Just \"A materialized view for projects\"\n\n        description `shouldBe` Just \"Just a test for materialized views\"\n\n        parameters `shouldBe` Just\n          [aesonQQ|\n            [\n              { \"$ref\": \"#/parameters/rowFilter.materialized_projects.id\" },\n              { \"$ref\": \"#/parameters/rowFilter.materialized_projects.name\" },\n              { \"$ref\": \"#/parameters/rowFilter.materialized_projects.client_id\" },\n              { \"$ref\": \"#/parameters/select\" },\n              { \"$ref\": \"#/parameters/order\" },\n              { \"$ref\": \"#/parameters/range\" },\n              { \"$ref\": \"#/parameters/rangeUnit\" },\n              { \"$ref\": \"#/parameters/offset\" },\n              { \"$ref\": \"#/parameters/limit\" },\n              { \"$ref\": \"#/parameters/preferCount\" }\n            ]\n          |]\n\n  describe \"VIEW that has a source FK based on a UNIQUE key\" $\n\n    it \"includes fk description\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let referralLink = r ^? key \"definitions\" . key \"referrals\" . key \"properties\" . key \"link\"\n\n      liftIO $\n        referralLink `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"integer\",\n              \"type\": \"integer\",\n              \"description\": \"Note:\\nThis is a Foreign Key to `pages.link`.<fk table='pages' column='link'/>\"\n            }\n          |]\n\n  describe \"VIEW created for a TABLE with a O2M relationship\" $ do\n\n    it \"fk points to destination TABLE instead of the VIEW\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let referralLink = r ^? key \"definitions\" . key \"projects\" . key \"properties\" . key \"client_id\"\n\n      liftIO $\n        referralLink `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"integer\",\n              \"type\": \"integer\",\n              \"description\": \"Note:\\nThis is a Foreign Key to `clients.id`.<fk table='clients' column='id'/>\"\n            }\n          |]\n\n  describe \"PostgreSQL to Swagger Type Mapping\" $ do\n\n    it \"character varying to string\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_character_varying\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"character varying\",\n              \"type\": \"string\"\n            }\n          |]\n    it \"character(1) to string\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_character\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"maxLength\": 1,\n              \"format\": \"character\",\n              \"type\": \"string\"\n            }\n          |]\n\n    it \"text to string\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_text\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"text\",\n              \"type\": \"string\"\n            }\n          |]\n\n    it \"boolean to boolean\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_boolean\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"boolean\",\n              \"type\": \"boolean\"\n            }\n          |]\n\n    it \"smallint to integer\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_smallint\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"smallint\",\n              \"type\": \"integer\"\n            }\n          |]\n\n    it \"integer to integer\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_integer\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"integer\",\n              \"type\": \"integer\"\n            }\n          |]\n\n    it \"bigint to integer\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_bigint\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"bigint\",\n              \"type\": \"integer\"\n            }\n          |]\n\n    it \"numeric to number\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_numeric\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"numeric\",\n              \"type\": \"number\"\n            }\n          |]\n\n    it \"real to number\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_real\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"real\",\n              \"type\": \"number\"\n            }\n          |]\n\n    it \"double_precision to number\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_double_precision\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"double precision\",\n              \"type\": \"number\"\n            }\n          |]\n\n    it \"json to any\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_json\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"json\"\n            }\n          |]\n\n    it \"jsonb to any\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_jsonb\"\n\n      liftIO $\n\n        types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"jsonb\"\n            }\n          |]\n\n    it \"array types to array\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let text_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_text_arr\"\n      let int_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_int_arr\"\n      let bool_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_bool_arr\"\n      let char_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_char_arr\"\n      let varchar_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_varchar_arr\"\n      let bigint_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_bigint_arr\"\n      let numeric_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_numeric_arr\"\n      let json_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_json_arr\"\n      let jsonb_arr_types = r ^? key \"definitions\" . key \"openapi_types\" . key \"properties\" . key \"a_jsonb_arr\"\n\n      liftIO $ do\n\n        text_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"text[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          |]\n\n        int_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"integer[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"integer\"\n              }\n            }\n          |]\n\n        bool_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"boolean[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"boolean\"\n              }\n            }\n          |]\n\n        char_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"character[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          |]\n\n        varchar_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"character varying[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          |]\n\n        bigint_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"bigint[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"integer\"\n              }\n            }\n          |]\n\n        numeric_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"numeric[]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"number\"\n              }\n            }\n          |]\n\n        json_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"json[]\",\n              \"type\": \"array\",\n              \"items\": {}\n            }\n          |]\n\n        jsonb_arr_types `shouldBe` Just\n          [aesonQQ|\n            {\n              \"format\": \"jsonb[]\",\n              \"type\": \"array\",\n              \"items\": {}\n            }\n          |]\n\n\n  describe \"Detects default values\" $ do\n\n    it \"text\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let defaultValue = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"text\" . key \"default\"\n\n      liftIO $\n\n        defaultValue `shouldBe` Just \"default\"\n\n    it \"boolean\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"boolean\" . key \"default\"\n\n      liftIO $\n\n        types `shouldBe` Just (Bool False)\n\n    it \"integer\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"integer\" . key \"default\"\n\n      liftIO $\n\n        types `shouldBe` Just (Number 42)\n\n    it \"numeric\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"numeric\" . key \"default\"\n\n      liftIO $\n\n        types `shouldBe` Just (Number 42.2)\n\n    it \"date\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"date\" . key \"default\"\n\n      liftIO $\n\n        types `shouldBe` Just \"1900-01-01\"\n\n    it \"time\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"openapi_defaults\" . key \"properties\" . key \"time\" . key \"default\"\n\n      liftIO $\n\n        types `shouldBe` Just \"13:00:00\"\n\n    it \"enum\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let types = r ^? key \"definitions\" . key \"menagerie\" . key \"properties\" . key \"enum\"\n\n      liftIO $\n\n        types `shouldBe` Just [aesonQQ|\n            {\n              \"format\": \"test.enum_menagerie_type\",\n              \"type\": \"string\",\n              \"enum\": [\n                \"foo\",\n                \"bar\"\n              ]\n            }\n          |]\n\n  describe \"RPC\" $ do\n\n    it \"includes function summary/description and query parameters for arguments in the get path item\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/rpc/varied_arguments_openapi\" . key s\n          args = r ^? method \"get\" . key \"parameters\"\n          summary = r ^? method \"get\" . key \"summary\"\n          description = r ^? method \"get\" . key \"description\"\n\n      liftIO $ do\n\n        summary `shouldBe` Just \"An RPC function\"\n\n        description `shouldBe` Just \"Just a test for RPC function arguments\"\n\n        args `shouldBe` Just\n          [aesonQQ|\n            [\n              {\n                \"format\": \"double precision\",\n                \"in\": \"query\",\n                \"name\": \"double\",\n                \"required\": true,\n                \"type\": \"number\"\n              },\n              {\n                \"format\": \"character varying\",\n                \"in\": \"query\",\n                \"name\": \"varchar\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"boolean\",\n                \"in\": \"query\",\n                \"name\": \"boolean\",\n                \"required\": true,\n                \"type\": \"boolean\"\n              },\n              {\n                \"format\": \"date\",\n                \"in\": \"query\",\n                \"name\": \"date\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"money\",\n                \"in\": \"query\",\n                \"name\": \"money\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"enum_menagerie_type\",\n                \"in\": \"query\",\n                \"name\": \"enum\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"text[]\",\n                \"in\": \"query\",\n                \"name\": \"text_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"integer[]\",\n                \"in\": \"query\",\n                \"name\": \"int_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"boolean[]\",\n                \"in\": \"query\",\n                \"name\": \"bool_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"character[]\",\n                \"in\": \"query\",\n                \"name\": \"char_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"character varying[]\",\n                \"in\": \"query\",\n                \"name\": \"varchar_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"bigint[]\",\n                \"in\": \"query\",\n                \"name\": \"bigint_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"numeric[]\",\n                \"in\": \"query\",\n                \"name\": \"numeric_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"json[]\",\n                \"in\": \"query\",\n                \"name\": \"json_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"jsonb[]\",\n                \"in\": \"query\",\n                \"name\": \"jsonb_arr\",\n                \"required\": true,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"integer\",\n                \"in\": \"query\",\n                \"name\": \"integer\",\n                \"required\": false,\n                \"type\": \"integer\"\n              },\n              {\n                \"format\": \"json\",\n                \"in\": \"query\",\n                \"name\": \"json\",\n                \"required\": false,\n                \"type\": \"string\"\n              },\n              {\n                \"format\": \"jsonb\",\n                \"in\": \"query\",\n                \"name\": \"jsonb\",\n                \"required\": false,\n                \"type\": \"string\"\n              }\n            ]\n          |]\n\n    it \"includes function summary/description and body schema for arguments in the post path item\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let method s = key \"paths\" . key \"/rpc/varied_arguments_openapi\" . key s\n          args = r ^? method \"post\" . key \"parameters\" . nth 0 . key \"schema\"\n          summary = r ^? method \"post\" . key \"summary\"\n          description = r ^? method \"post\" . key \"description\"\n\n      liftIO $ do\n\n        summary `shouldBe` Just \"An RPC function\"\n\n        description `shouldBe` Just \"Just a test for RPC function arguments\"\n\n        args `shouldBe` Just\n          [aesonQQ|\n            {\n              \"required\": [\n                \"double\",\n                \"varchar\",\n                \"boolean\",\n                \"date\",\n                \"money\",\n                \"enum\",\n                \"text_arr\",\n                \"int_arr\",\n                \"bool_arr\",\n                \"char_arr\",\n                \"varchar_arr\",\n                \"bigint_arr\",\n                \"numeric_arr\",\n                \"json_arr\",\n                \"jsonb_arr\"\n              ],\n              \"properties\": {\n                \"double\": {\n                  \"format\": \"double precision\",\n                  \"type\": \"number\"\n                },\n                \"varchar\": {\n                  \"format\": \"character varying\",\n                  \"type\": \"string\"\n                },\n                \"boolean\": {\n                  \"format\": \"boolean\",\n                  \"type\": \"boolean\"\n                },\n                \"date\": {\n                  \"format\": \"date\",\n                  \"type\": \"string\"\n                },\n                \"money\": {\n                  \"format\": \"money\",\n                  \"type\": \"string\"\n                },\n                \"enum\": {\n                  \"format\": \"enum_menagerie_type\",\n                  \"type\": \"string\"\n                },\n                \"text_arr\": {\n                  \"format\": \"text[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"int_arr\": {\n                  \"format\": \"integer[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"integer\"\n                  }\n                },\n                \"bool_arr\": {\n                  \"format\": \"boolean[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"boolean\"\n                  }\n                },\n                \"char_arr\": {\n                  \"format\": \"character[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"varchar_arr\": {\n                  \"format\": \"character varying[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"bigint_arr\": {\n                  \"format\": \"bigint[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"integer\"\n                  }\n                },\n                \"numeric_arr\": {\n                  \"format\": \"numeric[]\",\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"number\"\n                  }\n                },\n                \"json_arr\": {\n                  \"format\": \"json[]\",\n                  \"type\": \"array\",\n                  \"items\": {}\n                },\n                \"jsonb_arr\": {\n                  \"format\": \"jsonb[]\",\n                  \"type\": \"array\",\n                  \"items\": {}\n                },\n                \"integer\": {\n                  \"format\": \"integer\",\n                  \"type\": \"integer\"\n                },\n                \"json\": {\n                  \"format\": \"json\"\n                },\n                \"jsonb\": {\n                  \"format\": \"jsonb\"\n                }\n              },\n              \"type\": \"object\",\n              \"description\": \"An RPC function\\n\\nJust a test for RPC function arguments\"\n            }\n          |]\n\n    it \"doesn't include privileged function for anonymous\" $ do\n      r <- simpleBody <$> get \"/\"\n      let funcPath = r ^? key \"paths\" . key \"/rpc/privileged_hello\"\n\n      liftIO $ funcPath `shouldBe` Nothing\n\n    it \"includes function if user has permission\" $ do\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"\n      r <- simpleBody <$> request methodGet \"/\" [auth] \"\"\n      let funcTag = r ^? key \"paths\" . key \"/rpc/privileged_hello\"\n                    . key \"post\"  . key \"tags\"\n                    . nth 0\n\n      liftIO $ funcTag `shouldBe` Just [aesonQQ|\"(rpc) privileged_hello\"|]\n\n    it \"doesn't include OUT params of function as required parameters\" $ do\n      r <- simpleBody <$> get \"/\"\n      let params = r ^? key \"paths\" . key \"/rpc/many_out_params\"\n                      . key \"post\" . key \"parameters\" .  nth 0\n                      . key \"schema\". key \"required\"\n\n      liftIO $ params `shouldBe` Nothing\n\n    it \"includes INOUT params(with no DEFAULT) of function as required parameters\" $ do\n      r <- simpleBody <$> get \"/\"\n      let params = r ^? key \"paths\" . key \"/rpc/many_inout_params\"\n                      . key \"post\" . key \"parameters\" .  nth 0\n                      . key \"schema\". key \"required\"\n\n      liftIO $ params `shouldBe` Just [aesonQQ|[\"num\", \"str\"]|]\n\n    it \"uses a multi collection format when the function has a VARIADIC parameter\" $ do\n      r <- simpleBody <$> get \"/\"\n      let param = r ^? key \"paths\" . key \"/rpc/variadic_param\"\n                     . key \"get\" . key \"parameters\" .  nth 0\n\n      liftIO $ param `shouldBe` Just\n        [aesonQQ|\n          {\n            \"collectionFormat\": \"multi\",\n            \"in\": \"query\",\n            \"items\": {\n              \"format\": \"text\",\n              \"type\": \"string\"\n            },\n            \"name\": \"v\",\n            \"required\": false,\n            \"type\": \"array\"\n          }\n        |]\n\n    it \"only includes POST method for volatile functions\" $ do\n      r <- simpleBody <$> get \"/\"\n      let volatileGet = r ^? key \"paths\" . key \"/rpc/reset_table\" . key \"get\"\n          volatilePost = r ^? key \"paths\" . key \"/rpc/reset_table\" . key \"post\"\n\n      liftIO $ do\n        volatileGet `shouldBe` Nothing\n        volatilePost `shouldNotBe` Nothing\n\n    it \"includes GET and POST methods for stable functions\" $ do\n      r <- simpleBody <$> get \"/\"\n      let stableGet = r ^? key \"paths\" . key \"/rpc/getallusers\" . key \"get\"\n          stablePost = r ^? key \"paths\" . key \"/rpc/getallusers\" . key \"post\"\n\n      liftIO $ do\n        stableGet `shouldNotBe` Nothing\n        stablePost `shouldNotBe` Nothing\n\n    it \"includes GET and POST methods for immutable functions\" $ do\n      r <- simpleBody <$> get \"/\"\n      let immutableGet = r ^? key \"paths\" . key \"/rpc/jwt_test\" . key \"get\"\n          immutablePost = r ^? key \"paths\" . key \"/rpc/jwt_test\" . key \"post\"\n\n      liftIO $ do\n        immutableGet `shouldNotBe` Nothing\n        immutablePost `shouldNotBe` Nothing\n\n    it \"does not include empty enum in the preferParams parameter\" $ do\n      r <- simpleBody <$> get \"/\"\n      let preferParams = r ^? key \"parameters\" . key \"preferParams\" . key \"enum\"\n\n      liftIO $ do\n        preferParams `shouldBe` Nothing\n\n  describe \"Security\" $\n    it \"does not include security or security definitions by default\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let sec = r ^? key \"security\"\n          secDef = r ^? key \"securityDefinitions\"\n\n      liftIO $ do\n\n        sec `shouldBe` Nothing\n\n        secDef `shouldBe` Nothing\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/ProxySpec.hs",
    "content": "module Feature.OpenApi.ProxySpec where\n\nimport Network.Wai (Application)\nimport Test.Hspec  hiding (pendingWith)\n\nimport Protolude\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"GET / with proxy\" $\n    it \"returns a valid openapi spec with proxy\" $\n      validateOpenApiResponse [(\"Accept\", \"application/openapi+json\")]\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/RootSpec.hs",
    "content": "module Feature.OpenApi.RootSpec where\n\nimport Network.HTTP.Types\nimport Network.Wai        (Application)\n\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude hiding (get)\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"root spec function\" $ do\n    it \"accepts application/openapi+json\" $ do\n      request methodGet \"/\"\n        [(\"Accept\",\"application/openapi+json\")] \"\" `shouldRespondWith`\n        [json|{\n           \"swagger\": \"2.0\",\n           \"info\": {\"title\": \"PostgREST API\", \"description\": \"This is a dynamic API generated by PostgREST\"}\n          }|]\n        { matchHeaders = [\"Content-Type\" <:> \"application/openapi+json; charset=utf-8\"] }\n\n    it \"accepts application/json\" $ do\n      request methodGet \"/\"\n        [(\"Accept\",\"application/json\")] \"\" `shouldRespondWith`\n        [json|{\n           \"swagger\": \"2.0\",\n           \"info\": {\"title\": \"PostgREST API\", \"description\": \"This is a dynamic API generated by PostgREST\"}\n          }|]\n        { matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"] }\n"
  },
  {
    "path": "test/spec/Feature/OpenApi/SecurityOpenApiSpec.hs",
    "content": "module Feature.OpenApi.SecurityOpenApiSpec where\n\nimport Control.Lens ((^?))\n\nimport Data.Aeson.Lens\nimport Data.Aeson.QQ\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (..))\n\nimport Test.Hspec     hiding (pendingWith)\nimport Test.Hspec.Wai\n\nimport Protolude hiding (get)\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Security active\" $\n    it \"includes security and security definitions\" $ do\n      r <- simpleBody <$> get \"/\"\n\n      let sec = r ^? key \"security\"\n          secDef = r ^? key \"securityDefinitions\"\n\n      liftIO $ do\n\n        sec `shouldBe` Just\n          [aesonQQ|\n            [\n              { \"JWT\": [] }\n            ]\n          |]\n\n        secDef `shouldBe` Just\n          [aesonQQ|\n            {\n              \"JWT\": {\n                \"description\": \"Add the token prepending \\\"Bearer \\\" (without quotes) to it\",\n                \"in\": \"header\",\n                \"name\": \"Authorization\",\n                \"type\": \"apiKey\"\n              }\n            }\n          |]\n"
  },
  {
    "path": "test/spec/Feature/OptionsSpec.hs",
    "content": "module Feature.OptionsSpec where\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (..))\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\n\nimport Protolude\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"Allow header\" $ do\n  context \"a table\" $ do\n    it \"includes read/write methods for writeable table\" $ do\n      r <- request methodOptions \"/items\" [] \"\"\n      liftIO $ do\n        simpleHeaders r `shouldSatisfy` matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE\"\n        simpleHeaders r `shouldSatisfy` matchHeader \"Content-Length\" \"0\"\n\n    it \"fails with 404 for an unknown table\" $\n      request methodOptions \"/unknown\" [] \"\" `shouldRespondWith` 404\n\n  context \"a partitioned table\" $ do\n    it \"includes read/write methods for writeable partitioned tables\" $ do\n      r <- request methodOptions \"/car_models\" [] \"\"\n      liftIO $\n        simpleHeaders r `shouldSatisfy`\n          matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE\"\n\n  context \"a view\" $ do\n    context \"auto updatable\" $ do\n      it \"includes read/write methods for auto updatable views with pk\" $ do\n        r <- request methodOptions \"/projects_auto_updatable_view_with_pk\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE\"\n\n      it \"includes read/write methods for auto updatable views without pk\" $ do\n        r <- request methodOptions \"/projects_auto_updatable_view_without_pk\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PATCH,DELETE\"\n\n    context \"non auto updatable\" $ do\n      it \"includes read methods for non auto updatable views\" $ do\n        r <- request methodOptions \"/projects_view_without_triggers\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD\"\n\n      it \"includes read/write methods for insertable, updatable and deletable views with pk\" $ do\n        r <- request methodOptions \"/projects_view_with_all_triggers_with_pk\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE\"\n\n      it \"includes read/write methods for insertable, updatable and deletable views without pk\" $ do\n        r <- request methodOptions \"/projects_view_with_all_triggers_without_pk\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST,PATCH,DELETE\"\n\n      it \"includes read and insert methods for insertable views\" $ do\n        r <- request methodOptions \"/projects_view_with_insert_trigger\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST\"\n\n      it \"includes read and update methods for updatable views\" $ do\n        r <- request methodOptions \"/projects_view_with_update_trigger\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,PATCH\"\n\n      it \"includes read and delete methods for deletable views\" $ do\n        r <- request methodOptions \"/projects_view_with_delete_trigger\" [] \"\"\n        liftIO $\n          simpleHeaders r `shouldSatisfy`\n            matchHeader \"Allow\" \"OPTIONS,GET,HEAD,DELETE\"\n\n  context \"a function\" $ do\n    it \"includes the POST method for a volatile function\" $ do\n      r <- request methodOptions \"/rpc/reset_table\" [] \"\"\n      liftIO $\n        simpleHeaders r `shouldSatisfy`\n          matchHeader \"Allow\" \"OPTIONS,POST\"\n\n    it \"includes the GET/HEAD/POST method for a stable function\" $ do\n      r <- request methodOptions \"/rpc/getallusers\" [] \"\"\n      liftIO $\n        simpleHeaders r `shouldSatisfy`\n          matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST\"\n\n    it \"includes the GET/HEAD/POST method for a immutable function\" $ do\n      r <- request methodOptions \"/rpc/jwt_test\" [] \"\"\n      liftIO $\n        simpleHeaders r `shouldSatisfy`\n          matchHeader \"Allow\" \"OPTIONS,GET,HEAD,POST\"\n\n  context \"root endpoint\" $ do\n    it \"includes the GET/HEAD method \" $ do\n      r <- request methodOptions \"/\" [] \"\"\n      liftIO $\n        simpleHeaders r `shouldSatisfy`\n          matchHeader \"Allow\" \"OPTIONS,GET,HEAD\"\n"
  },
  {
    "path": "test/spec/Feature/Query/AggregateFunctionsSpec.hs",
    "content": "module Feature.Query.AggregateFunctionsSpec where\n\nimport Network.Wai (Application)\n\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nallowed :: SpecWith ((), Application)\nallowed =\n  describe \"aggregate functions\" $ do\n    context \"performing a count without specifying a field\" $ do\n      it \"returns the count of all rows when no other fields are selected\" $\n        get \"/entities?select=count()\" `shouldRespondWith`\n          [json|[{ \"count\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows you to specify an alias for the count\" $\n        get \"/entities?select=cnt:count()\" `shouldRespondWith`\n          [json|[{ \"cnt\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows you to cast the result of the count\" $\n        get \"/entities?select=count()::text\" `shouldRespondWith`\n          [json|[{ \"count\": \"4\" }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"returns the count grouped by all provided fields when other fields are selected\" $\n        get \"/projects?select=c:count(),client_id&order=client_id.desc\" `shouldRespondWith`\n          [json|[{ \"c\": 1, \"client_id\": null }, { \"c\": 2, \"client_id\": 2 }, { \"c\": 2, \"client_id\": 1}]|] { matchHeaders = [matchContentTypeJson] }\n\n    context \"performing a count by using it as a column (backwards compat)\" $ do\n      it \"returns the count of all rows when no other fields are selected\" $\n        get \"/entities?select=count\" `shouldRespondWith`\n          [json|[{ \"count\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"returns the embedded count of another resource\" $\n        get \"/clients?select=name,projects(count)'\" `shouldRespondWith`\n          [json|[{\"name\":\"Microsoft\",\"projects\":[{\"count\": 2}]}, {\"name\":\"Apple\",\"projects\":[{\"count\": 2}]}]|] { matchHeaders = [matchContentTypeJson] }\n\n    context \"performing an aggregation on one or more fields\" $ do\n      it \"supports sum()\" $\n        get \"/project_invoices?select=invoice_total.sum()\" `shouldRespondWith`\n          [json|[{\"sum\":8800}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"supports avg()\" $\n        get \"/project_invoices?select=invoice_total.avg()\" `shouldRespondWith`\n          [json|[{\"avg\":1100.0000000000000000}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"supports min()\" $\n        get \"/project_invoices?select=invoice_total.min()\" `shouldRespondWith`\n          [json|[{ \"min\": 100 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"supports max()\" $\n        get \"/project_invoices?select=invoice_total.max()\" `shouldRespondWith`\n          [json|[{ \"max\": 4000 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"supports count()\" $\n        get \"/project_invoices?select=invoice_total.count()\" `shouldRespondWith`\n          [json|[{ \"count\": 8 }]|] { matchHeaders = [matchContentTypeJson] }\n      it \"groups by any fields selected that do not have an aggregate applied\" $\n        get \"/project_invoices?select=invoice_total.sum(),invoice_total.max(),invoice_total.min(),project_id&order=project_id.desc\" `shouldRespondWith`\n          [json|[\n            {\"sum\":4100,\"max\":4000,\"min\":100,\"project_id\":4},\n            {\"sum\":3200,\"max\":2000,\"min\":1200,\"project_id\":3},\n            {\"sum\":1200,\"max\":700,\"min\":500,\"project_id\":2},\n            {\"sum\":300,\"max\":200,\"min\":100,\"project_id\":1} ]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"supports the use of aliases on fields that will be used in the group by\" $\n        get \"/project_invoices?select=invoice_total.sum(),invoice_total.max(),invoice_total.min(),pid:project_id&order=project_id.desc\" `shouldRespondWith`\n          [json|[\n            {\"sum\":4100,\"max\":4000,\"min\":100,\"pid\":4},\n            {\"sum\":3200,\"max\":2000,\"min\":1200,\"pid\":3},\n            {\"sum\":1200,\"max\":700,\"min\":500,\"pid\":2},\n            {\"sum\":300,\"max\":200,\"min\":100,\"pid\":1}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"allows you to specify an alias for the aggregate\" $\n        get \"/project_invoices?select=total_charged:invoice_total.sum(),project_id&order=project_id.desc\" `shouldRespondWith`\n          [json|[\n             {\"total_charged\":4100,\"project_id\":4},\n             {\"total_charged\":3200,\"project_id\":3},\n             {\"total_charged\":1200,\"project_id\":2},\n             {\"total_charged\":300,\"project_id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows you to cast the result of the aggregate\" $\n        get \"/project_invoices?select=total_charged:invoice_total.sum()::text,project_id&order=project_id.desc\" `shouldRespondWith`\n          [json|[\n             {\"total_charged\":\"4100\",\"project_id\":4},\n             {\"total_charged\":\"3200\",\"project_id\":3},\n             {\"total_charged\":\"1200\",\"project_id\":2},\n             {\"total_charged\":\"300\",\"project_id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows you to cast the input argument of the aggregate\" $\n        get \"/trash_details?select=jsonb_col->>key::integer.sum()\" `shouldRespondWith`\n          [json|[{\"sum\": 24}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows the combination of an alias, a before cast, and an after cast\" $\n        get \"/trash_details?select=s:jsonb_col->>key::integer.sum()::text\" `shouldRespondWith`\n          [json|[{\"s\": \"24\"}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"supports use of aggregates on RPC functions that return table values\" $\n        get \"/rpc/getallprojects?select=id.max()\" `shouldRespondWith`\n          [json|[{\"max\": 5}]|] { matchHeaders = [matchContentTypeJson] }\n      it \"allows the use of an JSON-embedded relationship column as part of the group by\" $\n        get \"/project_invoices?select=project_id,total:invoice_total.sum(),projects(name)&order=project_id\" `shouldRespondWith`\n          [json|[\n            {\"project_id\": 1, \"total\": 300,  \"projects\": {\"name\": \"Windows 7\"}},\n            {\"project_id\": 2, \"total\": 1200, \"projects\": {\"name\": \"Windows 10\"}},\n            {\"project_id\": 3, \"total\": 3200, \"projects\": {\"name\": \"IOS\"}},\n            {\"project_id\": 4, \"total\": 4100, \"projects\": {\"name\": \"OSX\"}}]|] { matchHeaders = [matchContentTypeJson] }\n    context \"performing aggregations that involve JSON-embedded relationships\" $ do\n      it \"supports sum()\" $\n        get \"/projects?select=name,project_invoices(invoice_total.sum())\" `shouldRespondWith`\n          [json|[\n            {\"name\":\"Windows 7\",\"project_invoices\":[{\"sum\": 300}]},\n            {\"name\":\"Windows 10\",\"project_invoices\":[{\"sum\": 1200}]},\n            {\"name\":\"IOS\",\"project_invoices\":[{\"sum\": 3200}]},\n            {\"name\":\"OSX\",\"project_invoices\":[{\"sum\": 4100}]},\n            {\"name\":\"Orphan\",\"project_invoices\":[{\"sum\": null}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"supports max()\" $\n        get \"/projects?select=name,project_invoices(invoice_total.max())\" `shouldRespondWith`\n          [json|[{\"name\":\"Windows 7\",\"project_invoices\":[{\"max\": 200}]},\n            {\"name\":\"Windows 10\",\"project_invoices\":[{\"max\": 700}]},\n            {\"name\":\"IOS\",\"project_invoices\":[{\"max\": 2000}]},\n            {\"name\":\"OSX\",\"project_invoices\":[{\"max\": 4000}]},\n            {\"name\":\"Orphan\",\"project_invoices\":[{\"max\": null}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"supports avg()\" $\n        get \"/projects?select=name,project_invoices(invoice_total.avg())\" `shouldRespondWith`\n          [json|[{\"name\":\"Windows 7\",\"project_invoices\":[{\"avg\": 150.0000000000000000}]},\n            {\"name\":\"Windows 10\",\"project_invoices\":[{\"avg\": 600.0000000000000000}]},\n            {\"name\":\"IOS\",\"project_invoices\":[{\"avg\": 1600.0000000000000000}]},\n            {\"name\":\"OSX\",\"project_invoices\":[{\"avg\": 2050.0000000000000000}]},\n            {\"name\":\"Orphan\",\"project_invoices\":[{\"avg\": null}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"supports min()\" $\n        get \"/projects?select=name,project_invoices(invoice_total.min())\" `shouldRespondWith`\n          [json|[{\"name\":\"Windows 7\",\"project_invoices\":[{\"min\": 100}]},\n            {\"name\":\"Windows 10\",\"project_invoices\":[{\"min\": 500}]},\n            {\"name\":\"IOS\",\"project_invoices\":[{\"min\": 1200}]},\n            {\"name\":\"OSX\",\"project_invoices\":[{\"min\": 100}]},\n            {\"name\":\"Orphan\",\"project_invoices\":[{\"min\": null}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"supports all at once\" $\n        get \"/projects?select=name,project_invoices(invoice_total.max(),invoice_total.min(),invoice_total.avg(),invoice_total.sum(),invoice_total.count())\" `shouldRespondWith`\n          [json|[\n            {\"name\":\"Windows 7\",\"project_invoices\":[{\"avg\": 150.0000000000000000, \"max\": 200, \"min\": 100, \"sum\": 300, \"count\": 2}]},\n            {\"name\":\"Windows 10\",\"project_invoices\":[{\"avg\": 600.0000000000000000, \"max\": 700, \"min\": 500, \"sum\": 1200, \"count\": 2}]},\n            {\"name\":\"IOS\",\"project_invoices\":[{\"avg\": 1600.0000000000000000, \"max\": 2000, \"min\": 1200, \"sum\": 3200, \"count\": 2}]},\n            {\"name\":\"OSX\",\"project_invoices\":[{\"avg\": 2050.0000000000000000, \"max\": 4000, \"min\": 100, \"sum\": 4100, \"count\": 2}]},\n            {\"name\":\"Orphan\",\"project_invoices\":[{\"avg\": null, \"max\": null, \"min\": null, \"sum\": null, \"count\": 0}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"performing aggregations on spreaded fields from an embedded resource\" $ do\n      context \"to-one spread relationships\" $ do\n        it \"supports the use of aggregates on spreaded fields\" $ do\n          get \"/budget_expenses?select=total_expenses:expense_amount.sum(),...budget_categories(budget_owner,total_budget:budget_amount.sum())&order=budget_categories(budget_owner)\" `shouldRespondWith`\n            [json|[\n              {\"total_expenses\": 600.52,\"budget_owner\": \"Brian Smith\",  \"total_budget\": 2000.42},\n              {\"total_expenses\": 100.22, \"budget_owner\": \"Jane Clarkson\",\"total_budget\": 7000.41},\n              {\"total_expenses\": 900.27, \"budget_owner\": \"Sally Hughes\", \"total_budget\": 500.23}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports the use of aggregates on spreaded fields when only aggregates are supplied\" $ do\n          get \"/budget_expenses?select=...budget_categories(total_budget:budget_amount.sum())\" `shouldRespondWith`\n            [json|[{\"total_budget\": 9501.06}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports aggregates from a spread relationships grouped by spreaded fields from other relationships\" $ do\n          get \"/processes?select=...process_costs(cost.sum()),...process_categories(name)\" `shouldRespondWith`\n            [json|[\n              {\"sum\": 400.00, \"name\": \"Batch\"},\n              {\"sum\": 350.00, \"name\": \"Mass\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/processes?select=...process_costs(cost_sum:cost.sum()),...process_categories(category:name)\" `shouldRespondWith`\n            [json|[\n              {\"cost_sum\": 400.00, \"category\": \"Batch\"},\n              {\"cost_sum\": 350.00, \"category\": \"Mass\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports aggregates on spreaded fields from nested relationships\" $ do\n          get \"/process_supervisor?select=...processes(factory_id,...process_costs(cost.sum()))\" `shouldRespondWith`\n            [json|[\n              {\"factory_id\": 3, \"sum\": 110.00},\n              {\"factory_id\": 2, \"sum\": 500.00},\n              {\"factory_id\": 1, \"sum\": 350.00}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/process_supervisor?select=...processes(factory_id,...process_costs(cost_sum:cost.sum()))\" `shouldRespondWith`\n            [json|[\n              {\"factory_id\": 3, \"cost_sum\": 110.00},\n              {\"factory_id\": 2, \"cost_sum\": 500.00},\n              {\"factory_id\": 1, \"cost_sum\": 350.00}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports aggregates on spreaded fields from nested relationships, grouped by a regular nested relationship\" $ do\n          get \"/process_supervisor?select=...processes(factories(name),...process_costs(cost.sum()))\" `shouldRespondWith`\n            [json|[\n              {\"factories\": {\"name\": \"Factory A\"}, \"sum\": 350.00},\n              {\"factories\": {\"name\": \"Factory B\"}, \"sum\": 500.00},\n              {\"factories\": {\"name\": \"Factory C\"}, \"sum\": 110.00}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/process_supervisor?select=...processes(factory:factories(name),...process_costs(cost_sum:cost.sum()))\" `shouldRespondWith`\n            [json|[\n              {\"factory\": {\"name\": \"Factory A\"}, \"cost_sum\": 350.00},\n              {\"factory\": {\"name\": \"Factory B\"}, \"cost_sum\": 500.00},\n              {\"factory\": {\"name\": \"Factory C\"}, \"cost_sum\": 110.00}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships\" $ do\n          get \"/process_supervisor?select=supervisor_id,...processes(...process_costs(cost.sum()),...process_categories(name))&order=supervisor_id\" `shouldRespondWith`\n            [json|[\n              {\"supervisor_id\": 1, \"sum\": 220.00, \"name\": \"Batch\"},\n              {\"supervisor_id\": 2, \"sum\": 70.00, \"name\": \"Batch\"},\n              {\"supervisor_id\": 2, \"sum\": 200.00, \"name\": \"Mass\"},\n              {\"supervisor_id\": 3, \"sum\": 180.00, \"name\": \"Batch\"},\n              {\"supervisor_id\": 3, \"sum\": 110.00, \"name\": \"Mass\"},\n              {\"supervisor_id\": 4, \"sum\": 180.00, \"name\": \"Batch\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/process_supervisor?select=supervisor_id,...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name))&order=supervisor_id\" `shouldRespondWith`\n            [json|[\n              {\"supervisor_id\": 1, \"cost_sum\": 220.00, \"category\": \"Batch\"},\n              {\"supervisor_id\": 2, \"cost_sum\": 70.00, \"category\": \"Batch\"},\n              {\"supervisor_id\": 2, \"cost_sum\": 200.00, \"category\": \"Mass\"},\n              {\"supervisor_id\": 3, \"cost_sum\": 180.00, \"category\": \"Batch\"},\n              {\"supervisor_id\": 3, \"cost_sum\": 110.00, \"category\": \"Mass\"},\n              {\"supervisor_id\": 4, \"cost_sum\": 180.00, \"category\": \"Batch\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n        it \"supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships, using a nested relationship as top parent\" $ do\n          get \"/supervisors?select=name,process_supervisor(...processes(...process_costs(cost.sum()),...process_categories(name)))\" `shouldRespondWith`\n            [json|[\n              {\"name\": \"Mary\", \"process_supervisor\": [{\"name\": \"Batch\", \"sum\": 220.00}]},\n              {\"name\": \"John\", \"process_supervisor\": [{\"name\": \"Batch\", \"sum\": 70.00}, {\"name\": \"Mass\", \"sum\": 200.00}]},\n              {\"name\": \"Peter\", \"process_supervisor\": [{\"name\": \"Batch\", \"sum\": 180.00}, {\"name\": \"Mass\", \"sum\": 110.00}]},\n              {\"name\": \"Sarah\", \"process_supervisor\": [{\"name\": \"Batch\", \"sum\": 180.00}]},\n              {\"name\": \"Jane\", \"process_supervisor\": []}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/supervisors?select=name,process_supervisor(...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name)))\" `shouldRespondWith`\n            [json|[\n              {\"name\": \"Mary\", \"process_supervisor\": [{\"category\": \"Batch\", \"cost_sum\": 220.00}]},\n              {\"name\": \"John\", \"process_supervisor\": [{\"category\": \"Batch\", \"cost_sum\": 70.00}, {\"category\": \"Mass\", \"cost_sum\": 200.00}]},\n              {\"name\": \"Peter\", \"process_supervisor\": [{\"category\": \"Batch\", \"cost_sum\": 180.00}, {\"category\": \"Mass\", \"cost_sum\": 110.00}]},\n              {\"name\": \"Sarah\", \"process_supervisor\": [{\"category\": \"Batch\", \"cost_sum\": 180.00}]},\n              {\"name\": \"Jane\", \"process_supervisor\": []}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        context \"supports count() aggregate without specifying a field\" $ do\n          it \"works by itself in the embedded resource\" $ do\n            get \"/process_supervisor?select=supervisor_id,...processes(count())&order=supervisor_id\" `shouldRespondWith`\n              [json|[\n                {\"supervisor_id\": 1, \"count\": 2},\n                {\"supervisor_id\": 2, \"count\": 2},\n                {\"supervisor_id\": 3, \"count\": 3},\n                {\"supervisor_id\": 4, \"count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/process_supervisor?select=supervisor_id,...processes(processes_count:count())&order=supervisor_id\" `shouldRespondWith`\n              [json|[\n                {\"supervisor_id\": 1, \"processes_count\": 2},\n                {\"supervisor_id\": 2, \"processes_count\": 2},\n                {\"supervisor_id\": 3, \"processes_count\": 3},\n                {\"supervisor_id\": 4, \"processes_count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n          it \"works alongside other columns in the embedded resource\" $ do\n            get \"/process_supervisor?select=...supervisors(id,count())&order=supervisors(id)\" `shouldRespondWith`\n              [json|[\n                {\"id\": 1, \"count\": 2},\n                {\"id\": 2, \"count\": 2},\n                {\"id\": 3, \"count\": 3},\n                {\"id\": 4, \"count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/process_supervisor?select=...supervisors(supervisor:id,supervisor_count:count())&order=supervisors(supervisor)\" `shouldRespondWith`\n              [json|[\n                {\"supervisor\": 1, \"supervisor_count\": 2},\n                {\"supervisor\": 2, \"supervisor_count\": 2},\n                {\"supervisor\": 3, \"supervisor_count\": 3},\n                {\"supervisor\": 4, \"supervisor_count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n          it \"works on nested resources\" $ do\n            get \"/process_supervisor?select=supervisor_id,...processes(...process_costs(count()))&order=supervisor_id\" `shouldRespondWith`\n              [json|[\n                {\"supervisor_id\": 1, \"count\": 2},\n                {\"supervisor_id\": 2, \"count\": 2},\n                {\"supervisor_id\": 3, \"count\": 3},\n                {\"supervisor_id\": 4, \"count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/process_supervisor?select=supervisor:supervisor_id,...processes(...process_costs(process_costs_count:count()))&order=supervisor_id\" `shouldRespondWith`\n              [json|[\n                {\"supervisor\": 1, \"process_costs_count\": 2},\n                {\"supervisor\": 2, \"process_costs_count\": 2},\n                {\"supervisor\": 3, \"process_costs_count\": 3},\n                {\"supervisor\": 4, \"process_costs_count\": 1}]|]\n              { matchHeaders = [matchContentTypeJson] }\n          it \"works on nested resources grouped by spreaded fields\" $ do\n            get \"/process_supervisor?select=...processes(factory_id,...process_costs(count()))&order=processes(factory_id)\" `shouldRespondWith`\n              [json|[\n                {\"factory_id\": 1, \"count\": 2},\n                {\"factory_id\": 2, \"count\": 4},\n                {\"factory_id\": 3, \"count\": 2}]|]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/process_supervisor?select=...processes(factory:factory_id,...process_costs(process_costs_count:count()))&order=processes(factory)\" `shouldRespondWith`\n              [json|[\n                {\"factory\": 1, \"process_costs_count\": 2},\n                {\"factory\": 2, \"process_costs_count\": 4},\n                {\"factory\": 3, \"process_costs_count\": 2}]|]\n              { matchHeaders = [matchContentTypeJson] }\n          it \"works on different levels of the nested resources at the same time\" $\n            get \"/process_supervisor?select=...processes(factory:factory_id,processes_count:count(),...process_costs(process_costs_count:count()))&order=processes(factory)\" `shouldRespondWith`\n              [json|[\n                {\"factory\": 1, \"processes_count\": 2, \"process_costs_count\": 2},\n                {\"factory\": 2, \"processes_count\": 4, \"process_costs_count\": 4},\n                {\"factory\": 3, \"processes_count\": 2, \"process_costs_count\": 2}]|]\n              { matchHeaders = [matchContentTypeJson] }\n\n      context \"to-many spread relationships\" $ do\n        it \"does not support the use of aggregates\" $ do\n          get \"/factories?select=name,...factory_buildings(type,size.sum())\" `shouldRespondWith`\n            [json|{\n              \"code\": \"PGRST127\",\n              \"message\":\"Feature not implemented\",\n              \"details\":\"Aggregates are not implemented for one-to-many or many-to-many spreads.\",\n              \"hint\":null\n            }|]\n            { matchStatus = 400\n            , matchHeaders = [matchContentTypeJson] }\n\ndisallowed :: SpecWith ((), Application)\ndisallowed =\n  describe \"attempting to use an aggregate when aggregate functions are disallowed\" $ do\n    it \"prevents the use of aggregates\" $\n      get \"/project_invoices?select=invoice_total.sum()\" `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":null,\n          \"code\":\"PGRST123\",\n          \"message\":\"Use of aggregate functions is not allowed\"\n        }|]\n        { matchStatus = 400\n        , matchHeaders = [matchContentTypeJson] }\n\n    it \"prevents the use of aggregates on embedded relationships\" $\n      get \"/projects?select=name,project_invoices(invoice_total.sum())\" `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":null,\n          \"code\":\"PGRST123\",\n          \"message\":\"Use of aggregate functions is not allowed\"\n        }|]\n        { matchStatus = 400\n        , matchHeaders = [matchContentTypeJson] }\n\n    it \"prevents the use of aggregates on to-one spread embeds\" $\n      get \"/project_invoices?select=...projects(id.count())\" `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":null,\n          \"code\":\"PGRST123\",\n          \"message\":\"Use of aggregate functions is not allowed\"\n        }|]\n        { matchStatus = 400\n        , matchHeaders = [matchContentTypeJson] }\n\n    it \"prevents the use of aggregates on to-many spread embeds\" $\n      get \"/factories?select=...processes(id.count())\" `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":null,\n          \"code\":\"PGRST123\",\n          \"message\":\"Use of aggregate functions is not allowed\"\n        }|]\n        { matchStatus = 400\n        , matchHeaders = [matchContentTypeJson] }\n"
  },
  {
    "path": "test/spec/Feature/Query/AndOrParamsSpec.hs",
    "content": "module Feature.Query.AndOrParamsSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"and/or params used for complex boolean logic\" $ do\n    context \"used with GET\" $ do\n      context \"or param\" $ do\n        it \"can do simple logic\" $\n          get \"/entities?or=(id.eq.1,id.eq.2)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can negate simple logic\" $\n          get \"/entities?not.or=(id.eq.1,id.eq.2)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can be combined with traditional filters\" $\n          get \"/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n\n      context \"embedded levels\" $ do\n        it \"can do logic on the second level\" $\n          get \"/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)\" `shouldRespondWith`\n            [json|[\n              {\"id\": 1, \"child_entities\": [ { \"id\": 1 }, { \"id\": 2 } ] }, { \"id\": 2, \"child_entities\": []},\n              {\"id\": 3, \"child_entities\": []}, {\"id\": 4, \"child_entities\": []}\n            ]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"can do logic on the third level\" $\n          get \"/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))\"\n            `shouldRespondWith`\n              [json|[\n                {\"id\": 1, \"child_entities\": [\n                  { \"id\": 1, \"grandchild_entities\": [ { \"id\": 1 }, { \"id\": 2 } ]},\n                  { \"id\": 2, \"grandchild_entities\": []},\n                  { \"id\": 4, \"grandchild_entities\": []},\n                  { \"id\": 5, \"grandchild_entities\": []}\n                ]},\n                {\"id\": 2, \"child_entities\": [\n                  { \"id\": 3, \"grandchild_entities\": []},\n                  { \"id\": 6, \"grandchild_entities\": []}\n                ]},\n                {\"id\": 3, \"child_entities\": []},\n                {\"id\": 4, \"child_entities\": []}\n              ]|]\n\n      context \"and/or params combined\" $ do\n        it \"can be nested inside the same expression\" $\n          get \"/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can be negated while nested\" $\n          get \"/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can be combined unnested\" $\n          get \"/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n\n      context \"operators inside and/or\" $ do\n        it \"can handle eq and neq\" $\n          get \"/entities?and=(id.eq.1,id.neq.2))&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle lt and gt\" $\n          get \"/entities?or=(id.lt.2,id.gt.3)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle lte and gte\" $\n          get \"/entities?or=(id.lte.2,id.gte.3)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle like and ilike\" $\n          get \"/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle in\" $\n          get \"/entities?or=(id.in.(1,2),id.in.(3,4))&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle is\" $\n          get \"/entities?and=(name.is.null,arr.is.null)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle fts on tsvector columns\" $ do\n          get \"/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)\" `shouldRespondWith`\n            [json|[\n              {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\" },\n              {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" },\n              {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}\n            ]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle fts on text and json columns\" $ do\n          get \"/grandchild_entities?or=(jsonb_col.fts.bar,jsonb_col.fts.foo)&select=jsonb_col\" `shouldRespondWith`\n            [json|[\n              { \"jsonb_col\": {\"a\": {\"b\":\"foo\"}} },\n              { \"jsonb_col\": {\"b\":\"bar\"} }]\n            |] { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?and=(text_search.not.plfts(german).Art%20Spass, text_search.not.plfts(french).amusant%20impossible, text_search.not.fts(english).impossible)&select=text_search\" `shouldRespondWith`\n            [json|[\n              { \"text_search\": \"But also fun to do what is possible\" },\n              { \"text_search\": \"Fat cats ate rats\" }]\n            |] { matchHeaders = [matchContentTypeJson] }\n        it \"can handle isdistinct\" $\n          get \"/entities?and=(id.gte.2,arr.isdistinct.{1,2})&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"can handle wfts (websearch_to_tsquery)\" $\n          get \"/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)\"\n          `shouldRespondWith`\n            [json|[\n                   {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\" },\n                   {\"text_search_vector\": \"'ate':3 'cat':2 'fat':1 'rat':4\" },\n                   {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" },\n                   {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\" }\n            ]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can handle cs and cd\" $\n          get \"/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 },{ \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"can handle range operators\" $ do\n          get \"/ranges?range=eq.[1,3]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=neq.[1,3]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=lt.[1,10]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=gt.[8,11]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=lte.[1,3]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=gte.[2,3]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=cs.[1,2]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=cd.[1,6]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=ov.[0,4]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=sl.[9,10]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=sr.[3,4]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=nxr.[4,7]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=nxl.[4,7]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=adj.(3,10]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/ranges?range=isdistinct.[1,3]&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }, {\"id\": 5}]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"can handle array operators\" $ do\n          get \"/entities?arr=eq.{1,2,3}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=neq.{1,2}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=lt.{2,3}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=lt.{2,0}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=gt.{1,1}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=gt.{3}&select=id\" `shouldRespondWith`\n            [json|[]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=lte.{2,1}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=lte.{1,2,3}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=lte.{1,2}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=cs.{1,2}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=cd.{1,2,6}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=ov.{3}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=ov.{2,3}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 2 }, { \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?arr=isdistinct.{1,2}&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n\n        context \"operators with not\" $ do\n          it \"eq, cs, like can be negated\" $\n            get \"/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id\" `shouldRespondWith`\n              [json|[{ \"id\": 1}]|] { matchHeaders = [matchContentTypeJson] }\n          it \"in, is, fts can be negated\" $\n            get \"/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id\" `shouldRespondWith`\n              [json|[{ \"id\": 2}]|] { matchHeaders = [matchContentTypeJson] }\n          it \"lt, gte, cd can be negated\" $\n            get \"/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id\" `shouldRespondWith`\n              [json|[{\"id\": 2}, {\"id\": 3}]|] { matchHeaders = [matchContentTypeJson] }\n          it \"gt, lte, ilike can be negated\" $\n            get \"/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id\" `shouldRespondWith`\n              [json|[{\"id\": 1}, {\"id\": 2}, {\"id\": 3}]|] { matchHeaders = [matchContentTypeJson] }\n          it \"isdistinct can be negated\" $\n            get \"/entities?and=(id.not.eq.2,arr.not.isdistinct.{1,2,3})&select=id\" `shouldRespondWith`\n              [json|[{\"id\": 3}]|] { matchHeaders = [matchContentTypeJson] }\n\n      context \"and/or params with quotes\" $ do\n        it \"eq can have quotes\" $\n          get \"/grandchild_entities?or=(name.eq.\\\"(grandchild,entity,4)\\\",name.eq.\\\"(grandchild,entity,5)\\\")&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 4 }, { \"id\": 5 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"like and ilike can have quotes\" $\n          get \"/grandchild_entities?or=(name.like.\\\"*ity,4*\\\",name.ilike.\\\"*ITY,5)\\\")&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 4 }, { \"id\": 5 }]|] { matchHeaders = [matchContentTypeJson] }\n        it \"in can have quotes\" $\n          get \"/grandchild_entities?or=(id.in.(\\\"1\\\",\\\"2\\\"),id.in.(\\\"3\\\",\\\"4\\\"))&select=id\" `shouldRespondWith`\n            [json|[{ \"id\": 1 }, { \"id\": 2 }, { \"id\": 3 }, { \"id\": 4 }]|] { matchHeaders = [matchContentTypeJson] }\n\n      it \"allows whitespace\" $\n        get \"/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id\" `shouldRespondWith`\n          [json|[{ \"id\": 3 }]|] { matchHeaders = [matchContentTypeJson] }\n\n      context \"multiple and/or conditions\" $ do\n        it \"cannot have zero conditions\" $\n          get \"/entities?or=()\" `shouldRespondWith`\n            [json|{\n              \"details\": \"unexpected \\\")\\\" expecting field name (* or [a..z0..9_$]), negation operator (not) or logic operator (and, or)\",\n              \"message\": \"\\\"failed to parse logic tree (())\\\" (line 1, column 4)\",\n              \"code\": \"PGRST100\",\n              \"hint\": null\n            }|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }\n        it \"can have a single condition\" $ do\n          get \"/entities?or=(id.eq.1)&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/entities?and=(id.eq.1)&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can have three conditions\" $ do\n          get \"/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}, {\"id\":2}, {\"id\":3}]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n        it \"can have four conditions combining and/or\" $ do\n          get \"/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}, {\"id\":2}, {\"id\":3}, {\"id\":4}]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id\" `shouldRespondWith`\n            [json|[{\"id\":1}]|] { matchHeaders = [matchContentTypeJson] }\n\n    context \"used with POST\" $\n      it \"includes related data with filters\" $\n        request methodPost \"/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|[\n              {\"id\":7,\"name\":\"entity 4\",\"parent_id\":1},\n              {\"id\":8,\"name\":\"entity 5\",\"parent_id\":2},\n              {\"id\":9,\"name\":\"entity 6\",\"parent_id\":3}\n            ]|]\n          `shouldRespondWith`\n            [json|[{\"id\": 7, \"entities\":null}, {\"id\": 8, \"entities\": {\"id\": 2}}, {\"id\": 9, \"entities\": {\"id\": 3}}]|]\n            { matchStatus = 201 }\n\n    context \"used with PATCH\" $ do\n      it \"succeeds when using and/or params\" $\n        request methodPatch \"/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|{ name : \"updated grandchild entity\"}|] `shouldRespondWith`\n          [json|[{ \"id\": 1, \"name\" : \"updated grandchild entity\"},{ \"id\": 2, \"name\" : \"updated grandchild entity\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"succeeds when the filtered column is modified\" $\n        request methodPatch \"/entities?select=id,name&or=(name.is.null,name.like.*test*)\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|{ \"name\" : \"updated entity\" }|] `shouldRespondWith`\n          [json|[{ \"id\": 4, \"name\": \"updated entity\" }]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"succeeds when the filtered column is not selected in the returned representation\" $\n        request methodPatch \"/entities?select=id&or=(name.is.null,name.like.*test*)\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|{ \"name\" : \"updated entity\" }|] `shouldRespondWith`\n          [json|[{ \"id\": 4 }]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"used with DELETE\" $ do\n      it \"succeeds when using and/or params\" $\n        request methodDelete \"/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name\"\n            [(\"Prefer\", \"return=representation\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{ \"id\": 1, \"name\" : \"grandchild entity 1\" },{ \"id\": 2, \"name\" : \"grandchild entity 2\" }]|]\n      it \"succeeds when the filtered column is not selected in the returned representation\" $\n        request methodDelete \"/entities?select=id&or=(name.is.null,name.like.*test*)\"\n            [(\"Prefer\", \"return=representation\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{ \"id\": 4 }]|]\n\n    it \"can query columns that begin with and/or reserved words\" $\n      get \"/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)\" `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Query/ComputedRelsSpec.hs",
    "content": "module Feature.Query.ComputedRelsSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"computed relationships\" $ do\n  it \"can define a many-to-one relationship with SETOF and ROWS 1 and embed\" $\n    get \"/videogames?select=name,designer:computed_designers(name)\"\n    `shouldRespondWith`\n      [json|[\n        {\"name\":\"Civilization I\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Civilization II\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}},\n        {\"name\":\"Final Fantasy II\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"can define a many-to-one relationship without SETOF and embed\" $\n    get \"/videogames?select=name,designer:computed_designers_noset(name)\"\n    `shouldRespondWith`\n      [json|[\n        {\"name\":\"Civilization I\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Civilization II\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}},\n        {\"name\":\"Final Fantasy II\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"can define a one-to-many relationship and embed\" $\n    get \"/designers?select=name,videogames:computed_videogames(name)\"\n    `shouldRespondWith`\n      [json|[\n        {\"name\":\"Sid Meier\",\"videogames\":[{\"name\":\"Civilization I\"}, {\"name\":\"Civilization II\"}]},\n        {\"name\":\"Hironobu Sakaguchi\",\"videogames\":[{\"name\":\"Final Fantasy I\"}, {\"name\":\"Final Fantasy II\"}]}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"works with !inner and count=exact\" $ do\n    request methodGet \"/designers?select=name,videogames:computed_videogames!inner(name)&videogames.name=eq.Civilization%20I\"\n      [(\"Prefer\", \"count=exact\")] \"\"\n      `shouldRespondWith`\n        [json|[{\"name\":\"Sid Meier\",\"videogames\":[{\"name\":\"Civilization I\"}]}]|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-0/1\"]\n        }\n    request methodGet \"/videogames?select=name,designer:computed_designers!inner(name)&designer.name=like.*Hironobu*\"\n      [(\"Prefer\", \"count=exact\")] \"\"\n      `shouldRespondWith`\n        [json|[\n          {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}},\n          {\"name\":\"Final Fantasy II\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-1/2\"]\n        }\n    request methodGet \"/videogames?select=name,designer:computed_designers_noset!inner(name)&designer.name=like.*Hironobu*\"\n      [(\"Prefer\", \"count=exact\")] \"\"\n      `shouldRespondWith`\n        [json|[\n          {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}},\n          {\"name\":\"Final Fantasy II\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-1/2\"]\n        }\n\n  it \"works with rpc\" $ do\n    get \"/rpc/getallvideogames?select=name,designer:computed_designers(name)\"\n      `shouldRespondWith`\n      [json|[\n        {\"name\":\"Civilization I\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Civilization II\",\"designer\":{\"name\":\"Sid Meier\"}},\n        {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}},\n        {\"name\":\"Final Fantasy II\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n    get \"/rpc/getalldesigners?select=name,videogames:computed_videogames(name)\"\n      `shouldRespondWith`\n      [json|[\n        {\"name\":\"Sid Meier\",\"videogames\":[{\"name\":\"Civilization I\"}, {\"name\":\"Civilization II\"}]},\n        {\"name\":\"Hironobu Sakaguchi\",\"videogames\":[{\"name\":\"Final Fantasy I\"}, {\"name\":\"Final Fantasy II\"}]}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"works with mutations\" $ do\n    request methodPost \"/videogames?select=name,designer:computed_designers(name)\"\n      [(\"Prefer\", \"return=representation\")]\n      [json| {\"id\": 5, \"name\": \"Chrono Trigger\", \"designer_id\": 2} |]\n      `shouldRespondWith`\n        [json|[ {\"name\":\"Chrono Trigger\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}} ]|]\n        { matchStatus  = 201 }\n    request methodPatch \"/designers?select=name,videogames:computed_videogames(name)&id=eq.1\"\n      [(\"Prefer\", \"return=representation\")]\n      [json| {\"name\": \"Sidney K. Meier\"} |]\n      `shouldRespondWith`\n        [json|[ { \"name\": \"Sidney K. Meier\", \"videogames\": [{\"name\":\"Civilization I\"}, {\"name\":\"Civilization II\"}] } ]|]\n        { matchStatus  = 200 }\n    request methodDelete \"/videogames?select=name,designer:computed_designers(name)&id=eq.3\"\n      [(\"Prefer\", \"return=representation\")] \"\"\n      `shouldRespondWith`\n        [json|[ {\"name\":\"Final Fantasy I\",\"designer\":{\"name\":\"Hironobu Sakaguchi\"}} ]|]\n        { matchStatus  = 200 }\n\n  it \"applies data representations to response\" $ do\n    -- A smoke test for data reps in the presence of computed relations.\n\n    -- The data rep here title cases the designer name before presentation. So here the lowercase version will be saved,\n    -- but the title case version returned. Pulling in a computed relation should not confuse this.\n    request methodPatch \"/designers?select=name,videogames:computed_videogames(name)&id=eq.1\"\n      [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"tx=commit\")]\n      [json| {\"name\": \"sidney k. meier\"} |]\n      `shouldRespondWith`\n      [json|[{\"name\":\"Sidney K. Meier\",\"videogames\":[{\"name\":\"Civilization I\"}, {\"name\":\"Civilization II\"}]}]|]\n      { matchStatus = 200 }\n\n    -- Verify it was saved the way we requested (there's no text data rep for this column, so if we select with the wrong casing, it should fail.)\n    get \"/designers?select=id&name=eq.Sidney%20K.%20Meier\"\n      `shouldRespondWith`\n      [json|[]|]\n      { matchStatus = 200, matchHeaders = [matchContentTypeJson] }\n    -- But with the right casing it works.\n    get \"/designers?select=id,name&name=eq.sidney%20k.%20meier\"\n      `shouldRespondWith`\n      [json|[{\"id\": 1, \"name\":\"Sidney K. Meier\"}]|]\n      { matchStatus = 200, matchHeaders = [matchContentTypeJson] }\n\n    -- Most importantly, if you read it back even via a computed relation, the data rep should be applied.\n    get \"/videogames?select=name,designer:computed_designers(*)&id=eq.1\"\n      `shouldRespondWith`\n      [json|[\n        {\"name\":\"Civilization I\",\"designer\":{\"id\": 1, \"name\":\"Sidney K. Meier\"}}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n    -- reset the test fixture\n    request methodPatch \"/designers?id=eq.1\"\n      [(\"Prefer\", \"tx=commit\")]\n      [json| {\"name\": \"Sid Meier\"} |]\n      `shouldRespondWith` 204\n    -- need to poke the second one too to prevent inherent ordering from changing\n    request methodPatch \"/designers?id=eq.2\"\n      [(\"Prefer\", \"tx=commit\")]\n      [json| {\"name\": \"Hironobu Sakaguchi\"} |]\n      `shouldRespondWith` 204\n\n  it \"works with self joins\" $\n    get \"/web_content?select=name,child_web_content(name),parent_web_content(name)&id=in.(0,1)\"\n    `shouldRespondWith`\n      [json|[\n        {\"name\":\"tardis\",\"child_web_content\":[{\"name\":\"fezz\"}, {\"name\":\"foo\"}, {\"name\":\"bar\"}],\"parent_web_content\":{\"name\":\"wat\"}},\n        {\"name\":\"fezz\",\"child_web_content\":[{\"name\":\"wut\"}],\"parent_web_content\":{\"name\":\"tardis\"}}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"can override many-to-one and one-to-many relationships\" $ do\n    get \"/videogames?select=*,designers!inner(*)\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n    get \"/designers?select=*,videogames!inner(*)\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"can override one-to-one relationships(would give disambiguation errors otherwise)\" $ do\n    get \"/first_1?select=*,second_1(*)\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n    get \"/second_1?select=*,first_1(*)\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n\n  -- https://github.com/PostgREST/postgrest/issues/2455\n  it \"creates queries with the right aliasing\" $ do\n    get \"/fee?select=*,jsbaz(*,janedoe(*))\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n    get \"/fee?select=*,jsbaz(*,johnsmith(*, fee(*)))\"\n      `shouldRespondWith`\n      [json|[]|] { matchHeaders = [matchContentTypeJson] }\n\n  it \"creates queries with the right aliasing when following a normal embed\" $ do\n    get \"/projects?select=name,clients(name,computed_projects(name))&limit=1\"\n      `shouldRespondWith`\n      [json|\n        [{\"name\":\"Windows 7\",\"clients\":{\"name\":\"Microsoft\",\"computed_projects\":{\"name\":\"Windows 7\"}}}]\n      |] { matchHeaders = [matchContentTypeJson] }\n    get \"/clients?select=name,projects(name,computed_clients(name))&limit=1\"\n      `shouldRespondWith`\n      [json|[\n        {\"name\":\"Microsoft\",\"projects\":[\n          {\"name\":\"Windows 7\",\"computed_clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"Windows 10\",\"computed_clients\":{\"name\":\"Microsoft\"}}\n        ]}\n      ]|] { matchHeaders = [matchContentTypeJson] }\n\n  -- https://github.com/PostgREST/postgrest/issues/2963\n  context \"can be defined using overloaded functions\" $ do\n    it \"tables\" $ do\n      get \"/items?select=*,computed_rel_overload(*)&limit=1\"\n        `shouldRespondWith`\n        [json|\n          [{\"id\":1,\"computed_rel_overload\":[{\"id\":1}]}]\n        |] { matchHeaders = [matchContentTypeJson] }\n      get \"/items2?select=*,computed_rel_overload(*)&limit=1\"\n        `shouldRespondWith`\n        [json|\n          [{\"id\":1,\"computed_rel_overload\":[{\"id\":1},{\"id\":2}]}]\n        |] { matchHeaders = [matchContentTypeJson] }\n\n    it \"rpc\" $ do\n      get \"/rpc/search?id=1&select=*,computed_rel_overload(*)\"\n        `shouldRespondWith`\n        [json|\n          [{\"id\":1,\"computed_rel_overload\":[{\"id\":1}]}]\n        |] { matchHeaders = [matchContentTypeJson] }\n      get \"/rpc/search2?id=1&select=*,computed_rel_overload(*)\"\n        `shouldRespondWith`\n        [json|\n          [{\"id\":1,\"computed_rel_overload\":[{\"id\":1},{\"id\":2}]}]\n        |] { matchHeaders = [matchContentTypeJson] }\n"
  },
  {
    "path": "test/spec/Feature/Query/CustomMediaSpec.hs",
    "content": "module Feature.Query.CustomMediaSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Network.Wai.Test    (SResponse (simpleBody, simpleHeaders, simpleStatus))\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\nimport Text.Heredoc        (str)\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"custom media types\" $ do\n  context \"for tables with aggregate\" $ do\n    it \"can query if there's an aggregate defined for the table\" $ do\n      r <- request methodGet \"/lines\" (acceptHdrs \"application/vnd.twkb\") \"\"\n      liftIO $ do\n        simpleBody r `shouldBe` readFixtureFile \"lines.twkb\"\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"application/vnd.twkb\")]\n        simpleHeaders r `shouldContain` [(\"Content-Length\", \"30\")]\n\n    it \"can query by id if there's an aggregate defined for the table\" $ do\n      r <- request methodGet \"/lines?id=eq.1\" (acceptHdrs \"application/vnd.twkb\") \"\"\n      liftIO $ do\n        simpleBody r `shouldBe` readFixtureFile \"1.twkb\"\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"application/vnd.twkb\")]\n\n    it \"will fail if there's no aggregate defined for the table\" $ do\n      request methodGet \"/lines\" (acceptHdrs \"text/plain\") \"\"\n        `shouldRespondWith`\n        [json| {\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: text/plain\"} |]\n        { matchStatus  = 406\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Length\" <:> \"110\" ]\n        }\n\n    it \"can get raw xml output with Accept: text/xml if there's an aggregate defined\" $ do\n      request methodGet \"/xmltest\" (acceptHdrs \"text/xml\") \"\"\n        `shouldRespondWith`\n        \"<myxml>foo</myxml>bar<foobar><baz/></foobar>\"\n        { matchStatus = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"text/xml; charset=utf-8\"\n                         , \"Content-Length\" <:> \"44\"]\n        }\n\n  -- TODO SOH (start of heading) is being added to results\n  context \"for tables with anyelement aggregate\" $ do\n    it \"will use the application/vnd.geo2+json media type for any table\" $\n      request methodGet \"/lines\" (acceptHdrs \"application/vnd.geo2+json\") \"\"\n        `shouldRespondWith`\n        \"\\SOH{\\\"type\\\": \\\"FeatureCollection\\\", \\\"hello\\\": \\\"world\\\"}\"\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/vnd.geo2+json\"\n                         , \"Content-Length\" <:> \"48\" ]\n        }\n\n    it \"will use the more specific application/vnd.geo2 handler for this table\" $ do\n      request methodGet \"/shop_bles\" (acceptHdrs \"application/vnd.geo2+json\") \"\"\n        `shouldRespondWith`\n        \"\\SOH\\\"anyelement overridden\\\"\"\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/vnd.geo2+json\"]\n        }\n\n      request methodGet \"/rpc/get_shop_bles\" (acceptHdrs \"application/vnd.geo2+json\") \"\"\n        `shouldRespondWith`\n        \"\\SOH\\\"anyelement overridden\\\"\"\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/vnd.geo2+json\"]\n        }\n\n  context \"Proc that returns scalar\" $ do\n    it \"can get raw output with Accept: text/html\" $ do\n      request methodGet \"/rpc/welcome.html\" (acceptHdrs \"text/html\") \"\"\n        `shouldRespondWith`\n        [str|\n            |<html>\n            |  <head>\n            |    <title>PostgREST</title>\n            |  </head>\n            |  <body>\n            |    <h1>Welcome to PostgREST</h1>\n            |  </body>\n            |</html>\n            |]\n        { matchStatus = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"text/html\"\n                         , \"Content-Length\" <:> \"117\" ]\n        }\n\n    it \"can get raw output with Accept: text/plain\" $ do\n      request methodGet \"/rpc/welcome\" (acceptHdrs \"text/plain\") \"\"\n        `shouldRespondWith` \"Welcome to PostgREST\"\n        { matchStatus = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"text/plain; charset=utf-8\"\n                         , \"Content-Length\" <:> \"20\" ]\n        }\n\n    it \"can get raw xml output with Accept: text/xml\" $ do\n      request methodGet \"/rpc/return_scalar_xml\" (acceptHdrs \"text/xml\") \"\"\n        `shouldRespondWith`\n        \"<my-xml-tag/>\"\n        { matchStatus = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"text/xml; charset=utf-8\"\n                         , \"Content-Length\" <:> \"13\" ]\n        }\n\n    it \"can get raw xml output with Accept: text/xml\" $ do\n      request methodGet \"/rpc/welcome.xml\" (acceptHdrs \"text/xml\") \"\"\n        `shouldRespondWith`\n        \"<html>\\n  <head>\\n    <title>PostgREST</title>\\n  </head>\\n  <body>\\n    <h1>Welcome to PostgREST</h1>\\n  </body>\\n</html>\"\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Type\" <:> \"text/xml; charset=utf-8\"]\n        }\n\n    it \"should fail with function returning text and Accept: text/xml\" $ do\n      request methodGet \"/rpc/welcome\" (acceptHdrs \"text/xml\") \"\"\n        `shouldRespondWith`\n        [json|\n          {\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: text/xml\"}\n        |]\n        { matchStatus = 406\n        , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n        }\n\n    it \"should not fail when the function doesn't return a row\" $ do\n      request methodGet \"/rpc/get_line?id=777\" (acceptHdrs \"application/vnd.twkb\") \"\"\n        `shouldRespondWith` \"\"\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/vnd.twkb\"]\n        }\n\n  context \"Proc that returns scalar based on a table\" $ do\n    it \"can get an image with Accept: image/png\" $ do\n      r <- request methodGet \"/rpc/ret_image\" (acceptHdrs \"image/png\") \"\"\n      liftIO $ do\n        simpleBody r `shouldBe` readFixtureFile \"A.png\"\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"image/png\")]\n        simpleHeaders r `shouldContain` [(\"Content-Length\", \"138\")]\n\n  context \"Proc that returns set of scalars and Accept: text/plain\" $\n    it \"will err because only scalars work with media type domains\" $ do\n      request methodGet \"/rpc/welcome_twice\"\n          (acceptHdrs \"text/plain\")\n          \"\"\n        `shouldRespondWith`\n          [json|{\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: text/plain\"}|]\n          { matchStatus = 406\n          , matchHeaders = [ \"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Length\" <:> \"110\"]\n          }\n\n  context \"Proc that returns rows and accepts custom media type\" $ do\n    it \"works if it has an aggregate defined\" $ do\n      r <- request methodGet \"/rpc/get_lines\" [(\"Accept\", \"application/vnd.twkb\")] \"\"\n      liftIO $ do\n        simpleBody r `shouldBe` readFixtureFile \"lines.twkb\"\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"application/vnd.twkb\")]\n\n    it \"fails if doesn't have an aggregate defined\" $ do\n      request methodGet \"/rpc/get_lines\"\n          (acceptHdrs \"application/octet-stream\") \"\"\n        `shouldRespondWith`\n          [json| {\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: application/octet-stream\"} |]\n          { matchStatus = 406 }\n\n    -- TODO SOH (start of heading) is being added to results\n    it \"works if there's an anyelement aggregate defined\" $ do\n      request methodGet \"/rpc/get_lines\" (acceptHdrs \"application/vnd.geo2+json\") \"\"\n        `shouldRespondWith`\n        \"\\SOH{\\\"type\\\": \\\"FeatureCollection\\\", \\\"hello\\\": \\\"world\\\"}\"\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/vnd.geo2+json\"]\n        }\n\n  context \"overriding\" $ do\n    it \"will override the application/json handler for a single table\" $\n      request methodGet \"/ov_json\" (acceptHdrs \"application/json\") \"\"\n        `shouldRespondWith`\n        [json| {\"overridden\": \"true\"} |]\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/json; charset=utf-8\"\n                         , \"Content-Length\" <:> \"22\" ]\n        }\n\n    -- TODO SOH (start of heading) is being added to results\n    it \"will override the application/geo+json handler for a single table\" $\n      request methodGet \"/lines?id=eq.1\" (acceptHdrs \"application/geo+json\") \"\"\n        `shouldRespondWith`\n        \"\\SOH{\\\"crs\\\": {\\\"type\\\": \\\"name\\\", \\\"properties\\\": {\\\"name\\\": \\\"EPSG:4326\\\"}}, \\\"type\\\": \\\"FeatureCollection\\\", \\\"features\\\": [{\\\"type\\\": \\\"Feature\\\", \\\"geometry\\\": {\\\"type\\\": \\\"LineString\\\", \\\"coordinates\\\": [[1, 1], [5, 5]]}, \\\"properties\\\": {\\\"id\\\": 1, \\\"name\\\": \\\"line-1\\\"}}]}\"\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/geo+json; charset=utf-8\"\n                         , \"Content-Length\" <:> \"239\" ]\n        }\n\n    it \"will not override vendored media types like application/vnd.pgrst.object\" $\n      request methodGet \"/projects?id=eq.1\" (acceptHdrs \"application/vnd.pgrst.object\") \"\"\n        `shouldRespondWith`\n        [json|{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}|]\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"application/vnd.pgrst.object+json; charset=utf-8\"\n                         , \"Content-Length\" <:> \"41\" ]\n        }\n\n  context \"matches requested media type correctly\" $ do\n    -- https://github.com/PostgREST/postgrest/issues/1462\n    it \"will match image/png according to q values\" $ do\n      r1 <- request methodGet \"/rpc/ret_image\" (acceptHdrs \"image/png, */*\") \"\"\n      liftIO $ do\n        simpleBody r1 `shouldBe` readFixtureFile \"A.png\"\n        simpleHeaders r1 `shouldContain` [(\"Content-Type\", \"image/png\")]\n        simpleHeaders r1 `shouldContain` [(\"Content-Length\", \"138\")]\n\n      r2 <- request methodGet \"/rpc/ret_image\" (acceptHdrs \"text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8\") \"\"\n      liftIO $ do\n        simpleBody r2 `shouldBe` readFixtureFile \"A.png\"\n        simpleHeaders r2 `shouldContain` [(\"Content-Type\", \"image/png\")]\n        simpleHeaders r2 `shouldContain` [(\"Content-Length\", \"138\")]\n\n    -- https://github.com/PostgREST/postgrest/issues/2170\n    it \"will match json in presence of text/plain\" $ do\n      r <- request methodGet \"/projects?id=eq.1\" (acceptHdrs \"text/plain, application/json\") \"\"\n      liftIO $ do\n        simpleStatus r `shouldBe` status200\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"application/json; charset=utf-8\")]\n\n    -- https://github.com/PostgREST/postgrest/issues/1102\n    it \"will match a custom text/tab-separated-values\" $ do\n      request methodGet \"/projects?id=in.(1,2)\" (acceptHdrs \"text/tab-separated-values\") \"\"\n        `shouldRespondWith`\n        \"id\\tname\\tclient_id\\n1\\tWindows 7\\t1\\n2\\tWindows 10\\t1\\n\"\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"text/tab-separated-values\"\n                         , \"Content-Length\" <:> \"47\" ]\n        }\n\n    -- https://github.com/PostgREST/postgrest/issues/1371#issuecomment-519248984\n    it \"will match a custom text/csv with BOM\" $ do\n      r <- request methodGet \"/lines\" (acceptHdrs \"text/csv\") \"\"\n      liftIO $ do\n        simpleBody r `shouldBe` readFixtureFile \"lines.csv\"\n        simpleHeaders r `shouldContain` [(\"Content-Type\", \"text/csv; charset=utf-8\")]\n        simpleHeaders r `shouldContain` [(\"Content-Disposition\", \"attachment; filename=\\\"lines.csv\\\"\")]\n        simpleHeaders r `shouldContain` [(\"Content-Length\", \"216\")]\n\n  -- https://github.com/PostgREST/postgrest/issues/3160\n  context \"using select query parameter\" $ do\n    it \"without select\" $ do\n      request methodGet \"/projects?id=in.(1,2)\" (acceptHdrs \"pg/outfunc\") \"\"\n        `shouldRespondWith`\n        [str|(1,\"Windows 7\",1)\n            |(2,\"Windows 10\",1)\n            |]\n        { matchStatus  = 200\n        , matchHeaders = [ \"Content-Type\" <:> \"pg/outfunc\"\n                         , \"Content-Length\" <:> \"37\" ]\n        }\n\n    it \"with fewer columns selected\" $ do\n      request methodGet \"/projects?id=in.(1,2)&select=id,name\" (acceptHdrs \"pg/outfunc\") \"\"\n        `shouldRespondWith`\n        [str|(1,\"Windows 7\")\n            |(2,\"Windows 10\")\n            |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"pg/outfunc\"]\n        }\n\n    it \"with columns in different order\" $ do\n      request methodGet \"/projects?id=in.(1,2)&select=name,id,client_id\" (acceptHdrs \"pg/outfunc\") \"\"\n        `shouldRespondWith`\n        [str|(\"Windows 7\",1,1)\n            |(\"Windows 10\",2,1)\n            |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"pg/outfunc\"]\n        }\n\n    it \"with computed columns\" $ do\n      request methodGet \"/items?id=in.(1,2)&select=id,always_true\" (acceptHdrs \"pg/outfunc\") \"\"\n        `shouldRespondWith`\n        [str|(1,t)\n            |(2,t)\n            |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"pg/outfunc\"]\n        }\n\n    -- TODO: Embeddings should not return JSON. Arrays of record would be much better.\n    it \"with embedding\" $ do\n      request methodGet \"/projects?id=in.(1,2)&select=*,clients(id)\" (acceptHdrs \"pg/outfunc\") \"\"\n        `shouldRespondWith`\n        [str|(1,\"Windows 7\",1,\"{\"\"id\"\": 1}\")\n            |(2,\"Windows 10\",1,\"{\"\"id\"\": 1}\")\n            |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"pg/outfunc\"]\n        }\n\n    it \"will fail for specific aggregate with fewer columns\" $ do\n      request methodGet \"/lines?select=id\" (acceptHdrs \"application/vnd.twkb\") \"\"\n        `shouldRespondWith` 406\n\n    it \"will fail for specific aggregate with more columns\" $ do\n      request methodGet \"/lines?select=id,name,geom,id\" (acceptHdrs \"application/vnd.twkb\") \"\"\n        `shouldRespondWith` 406\n\n    it \"will fail for specific aggregate with columns in different order\" $ do\n      request methodGet \"/lines?select=name,id,geom\" (acceptHdrs \"application/vnd.twkb\") \"\"\n        `shouldRespondWith` 406\n\n    -- This is just because it would be hard to detect this case, so we better error in this case, too.\n    it \"will fail for specific aggregate with columns in same order\" $ do\n      request methodGet \"/lines?select=id,name,geom\" (acceptHdrs \"application/vnd.twkb\") \"\"\n        `shouldRespondWith` 406\n\n  context \"any media type\" $ do\n    context \"on functions\" $ do\n      it \"returns application/json for */* if not explicitly set\" $ do\n        request methodGet \"/rpc/ret_any_mt\" (acceptHdrs \"*/*\") \"\"\n          `shouldRespondWith` \"any\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n      it \"accepts any media type and sets the generic octet-stream as content type\" $ do\n        request methodGet \"/rpc/ret_any_mt\" (acceptHdrs \"app/bingo\") \"\"\n          `shouldRespondWith` \"any\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n        request methodGet \"/rpc/ret_any_mt\" (acceptHdrs \"text/bango\") \"\"\n          `shouldRespondWith` \"any\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n        request methodGet \"/rpc/ret_any_mt\" (acceptHdrs \"image/boingo\") \"\"\n          `shouldRespondWith` \"any\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n      it \"returns custom media type for */* if explicitly set\" $ do\n        request methodGet \"/rpc/ret_some_mt\" (acceptHdrs \"*/*\") \"\"\n          `shouldRespondWith` \"groucho\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"app/groucho\"]\n          }\n\n      it \"accepts some media types if there's conditional logic\" $ do\n        request methodGet \"/rpc/ret_some_mt\" (acceptHdrs \"app/chico\") \"\"\n          `shouldRespondWith` \"chico\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"app/chico\"]\n          }\n\n        request methodGet \"/rpc/ret_some_mt\" (acceptHdrs \"app/harpo\") \"\"\n          `shouldRespondWith` \"harpo\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"app/harpo\"]\n          }\n\n        request methodGet \"/rpc/ret_some_mt\" (acceptHdrs \"text/csv\") \"\"\n          `shouldRespondWith` 406\n\n    context \"on tables\" $ do\n      it \"returns application/json for */* if not explicitly set\" $ do\n        request methodGet \"/some_numbers?val=eq.1\" (acceptHdrs \"*/*\") \"\"\n          `shouldRespondWith` \"anything\\n1\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n      it \"accepts any media type and sets it as a header\" $ do\n        request methodGet \"/some_numbers?val=eq.2\" (acceptHdrs \"magic/number\") \"\"\n          `shouldRespondWith` \"magic\\n2\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"magic/number\"]\n          }\n        request methodGet \"/some_numbers?val=eq.3\" (acceptHdrs \"crazy/bingo\") \"\"\n          `shouldRespondWith` \"crazy\\n3\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"crazy/bingo\"]\n          }\n        request methodGet \"/some_numbers?val=eq.4\" (acceptHdrs \"unknown/unknown\") \"\"\n          `shouldRespondWith` \"anything\\n4\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/octet-stream\"]\n          }\n\n  context \"media type parser fails\" $ do\n    it \"sends media type as is\" $\n      request methodGet \"/items\" (acceptHdrs \"undefined\") \"\"\n      `shouldRespondWith`\n      [json| {\"code\":\"PGRST107\",\"details\":null,\"hint\":null,\"message\":\"None of these media types are available: undefined\"} |]\n      { matchStatus = 406 }\n\n  context \"media type parser allowed characters\" $ do\n    it \"regression test allowing charset=utf-8\" $\n      request methodPost \"/rpc/overloaded_default\"\n        [(\"Content-Type\", \"application/json; charset=utf-8\")]\n        [json|{\"must_param\":1}|]\n        `shouldRespondWith`\n        [json|{\"val\":1}|]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n        }\n\n    it \"handle unrecognized parameters leniently\" $ do\n      request methodPost \"/rpc/overloaded_default\"\n        [(\"Content-Type\", \"application/json; $$ unrecognized-chars=ignored $$\")]\n        [json|{\"must_param\":1}|]\n        `shouldRespondWith`\n        [json|{\"val\":1}|]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n        }\n"
  },
  {
    "path": "test/spec/Feature/Query/DeleteSpec.hs",
    "content": "module Feature.Query.DeleteSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Deleting\" $ do\n    context \"existing record\" $ do\n      it \"succeeds with 204 and deletion count\" $\n        request methodDelete \"/items?id=eq.1\"\n            []\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"*/*\" ]\n            }\n\n      it \"returns the deleted item and count if requested\" $\n        request methodDelete \"/items?id=eq.2\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"count=exact\")] \"\"\n          `shouldRespondWith` [json|[{\"id\":2}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"*/1\"\n                           , \"Content-Length\" <:> \"10\"\n                           , \"Preference-Applied\" <:> \"return=representation, count=exact\"]\n          }\n\n      it \"ignores ?select= when return not set or return=minimal\" $ do\n        request methodDelete \"/items?id=eq.3&select=id\"\n            []\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"*/*\" ]\n            }\n        request methodDelete \"/items?id=eq.3&select=id\"\n            [(\"Prefer\", \"return=minimal\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n\n      it \"returns the deleted item and shapes the response\" $\n        request methodDelete \"/complex_items?id=eq.2&select=id,name\" [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith` [json|[{\"id\":2,\"name\":\"Two\"}]|]\n          { matchStatus  = 200\n          , matchHeaders = [ \"Content-Range\" <:> \"*/*\"\n                           , \"Content-Length\" <:> \"23\" ]\n          }\n\n      it \"can rename and cast the selected columns\" $\n        request methodDelete \"/complex_items?id=eq.3&select=ciId:id::text,ciName:name\" [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith` [json|[{\"ciId\":\"3\",\"ciName\":\"Three\"}]|]\n\n      it \"can embed (parent) entities\" $\n        request methodDelete \"/tasks?id=eq.8&select=id,name,project:projects(id)\" [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith` [json|[{\"id\":8,\"name\":\"Code OSX\",\"project\":{\"id\":4}}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n          }\n\n      it \"embeds an O2O relationship after delete\" $ do\n        request methodDelete \"/students?id=eq.1&select=name,students_info(address)\"\n                [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith`\n          [json|[\n            {\n              \"name\": \"John Doe\",\n              \"students_info\":{\"address\":\"Street 1\"}\n            }\n          ]|]\n          { matchStatus  = 200,\n            matchHeaders = [matchContentTypeJson]\n          }\n        request methodDelete \"/students_info?id=eq.1&select=address,students(name)\"\n                [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith`\n          [json|[\n            {\n              \"address\": \"Street 1\",\n              \"students\":{\"name\": \"John Doe\"}\n            }\n          ]|]\n          { matchStatus  = 200,\n            matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"known route, no records matched\" $\n      it \"includes [] body if return=rep\" $\n        request methodDelete \"/items?id=eq.101\"\n          [(\"Prefer\", \"return=representation\")] \"\"\n          `shouldRespondWith` \"[]\"\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n          }\n\n    context \"totally unknown route\" $\n      it \"fails with 404\" $\n        request methodDelete \"/foozle?id=eq.101\" [] \"\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.foozle' in the schema cache\"} |]\n          { matchStatus = 404\n          , matchHeaders = []\n          }\n\n    context \"table with limited privileges\" $ do\n      it \"fails deleting the row when return=representation and selecting all the columns\" $\n        request methodDelete \"/app_users?id=eq.1\" [(\"Prefer\", \"return=representation\")] mempty\n            `shouldRespondWith` 401\n\n      it \"succeeds deleting the row when return=representation and selecting only the privileged columns\" $\n        request methodDelete \"/app_users?id=eq.1&select=id,email\" [(\"Prefer\", \"return=representation\")]\n          [json| { \"password\": \"passxyz\" } |]\n            `shouldRespondWith` [json|[ { \"id\": 1, \"email\": \"test@123.com\" } ]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"suceeds deleting the row with no explicit select when using return=minimal\" $\n        request methodDelete \"/app_users?id=eq.2\"\n            [(\"Prefer\", \"return=minimal\")]\n            mempty\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Preference-Applied\" <:> \"return=minimal\" ]\n            }\n\n      it \"suceeds deleting the row with no explicit select by default\" $\n        request methodDelete \"/app_users?id=eq.3\"\n            []\n            mempty\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength]\n            }\n\n    context \"with ordering\" $\n      it \"works with request method DELETE and embedded resource\" $ do\n        request methodDelete \"/artists?id=lt.3&select=id,name,albums(title)&order=id.desc\"\n          [(\"Prefer\", \"return=representation\")]\n          \"\"\n          `shouldRespondWith`\n          [json| [ {\"id\":2,\"name\":\"black country, new road\",\"albums\":[{\"title\": \"ants from up above\"}]}, {\"id\":1,\"name\":\"duster\",\"albums\":[{\"title\": \"stratosphere\"},{\"title\": \"contemporary movement\"}]}]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/EmbedDisambiguationSpec.hs",
    "content": "module Feature.Query.EmbedDisambiguationSpec where\n\nimport Network.Wai (Application)\n\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"resource embedding disambiguation\" $ do\n    context \"ambiguous requests that give 300 Multiple Choices\" $ do\n      it \"errs when there are o2m and m2m cardinalities to the target table\" $\n        get \"/sites?select=*,big_projects(*)\" `shouldRespondWith`\n          [json|\n            {\n              \"details\": [\n                {\n                  \"cardinality\": \"many-to-one\",\n                  \"relationship\": \"main_project using sites(main_project_id) and big_projects(big_project_id)\",\n                  \"embedding\": \"sites with big_projects\"\n                },\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"jobs using jobs_site_id_fkey(site_id) and jobs_big_project_id_fkey(big_project_id)\",\n                  \"embedding\": \"sites with big_projects\"\n                },\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"main_jobs using jobs_site_id_fkey(site_id) and jobs_big_project_id_fkey(big_project_id)\",\n                  \"embedding\": \"sites with big_projects\"\n                }\n              ],\n              \"hint\": \"Try changing 'big_projects' to one of the following: 'big_projects!main_project', 'big_projects!jobs', 'big_projects!main_jobs'. Find the desired relationship in the 'details' key.\",\n              \"message\": \"Could not embed because more than one relationship was found for 'sites' and 'big_projects'\",\n              \"code\": \"PGRST201\"\n            }\n          |]\n          { matchStatus  = 300\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Length\" <:> \"828\" ]\n          }\n\n      it \"errs on an ambiguous embed that has a circular reference\" $\n        get \"/agents?select=*,departments(*)\" `shouldRespondWith`\n          [json|\n            {\n              \"details\": [\n                {\n                    \"cardinality\": \"one-to-many\",\n                    \"relationship\": \"departments_head_id_fkey using agents(id) and departments(head_id)\",\n                    \"embedding\": \"agents with departments\"\n                },\n                {\n                    \"cardinality\": \"many-to-one\",\n                    \"relationship\": \"agents_department_id_fkey using agents(department_id) and departments(id)\",\n                    \"embedding\": \"agents with departments\"\n                }\n              ],\n              \"hint\": \"Try changing 'departments' to one of the following: 'departments!departments_head_id_fkey', 'departments!agents_department_id_fkey'. Find the desired relationship in the 'details' key.\",\n              \"message\": \"Could not embed because more than one relationship was found for 'agents' and 'departments'\",\n              \"code\": \"PGRST201\"\n            }\n           |]\n          { matchStatus  = 300\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"errs when there are more than two fks on a junction table but it can be disambiguated with spread embeds\" $ do\n        -- We have 4 possibilities for doing the junction JOIN here.\n        get \"/whatev_sites?select=*,whatev_projects(*)\" `shouldRespondWith`\n          [json|\n            {\n              \"details\": [\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"whatev_jobs using whatev_jobs_site_id_1_fkey(site_id_1) and whatev_jobs_project_id_1_fkey(project_id_1)\",\n                  \"embedding\": \"whatev_sites with whatev_projects\"\n                },\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"whatev_jobs using whatev_jobs_site_id_1_fkey(site_id_1) and whatev_jobs_project_id_2_fkey(project_id_2)\",\n                  \"embedding\": \"whatev_sites with whatev_projects\"\n                },\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"whatev_jobs using whatev_jobs_site_id_2_fkey(site_id_2) and whatev_jobs_project_id_1_fkey(project_id_1)\",\n                  \"embedding\": \"whatev_sites with whatev_projects\"\n                },\n                {\n                  \"cardinality\": \"many-to-many\",\n                  \"relationship\": \"whatev_jobs using whatev_jobs_site_id_2_fkey(site_id_2) and whatev_jobs_project_id_2_fkey(project_id_2)\",\n                  \"embedding\": \"whatev_sites with whatev_projects\"\n                }\n              ],\n              \"hint\": \"Try changing 'whatev_projects' to one of the following: 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs'. Find the desired relationship in the 'details' key.\",\n              \"message\": \"Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'\",\n              \"code\": \"PGRST201\"\n            }\n          |]\n          { matchStatus  = 300\n          , matchHeaders = [matchContentTypeJson]\n          }\n        -- Each of those 4 possibilities can be done with spread embeds, by following the details in the error above\n        get \"/whatev_sites?select=*,whatev_jobs!site_id_1(...whatev_projects!project_id_1(*))\" `shouldRespondWith` [json|[]|]\n        get \"/whatev_sites?select=*,whatev_jobs!site_id_1(...whatev_projects!project_id_2(*))\" `shouldRespondWith` [json|[]|]\n        get \"/whatev_sites?select=*,whatev_jobs!site_id_2(...whatev_projects!project_id_1(*))\" `shouldRespondWith` [json|[]|]\n        get \"/whatev_sites?select=*,whatev_jobs!site_id_2(...whatev_projects!project_id_2(*))\" `shouldRespondWith` [json|[]|]\n\n      it \"can disambiguate a recursive m2m with spread embeds\" $ do\n        get \"/posters?select=*,subscribers:subscriptions!subscribed(...posters!subscriber(*))&limit=1\" `shouldRespondWith`\n          [json| [ {\"id\":1,\"name\":\"Mark\",\"subscribers\":[{\"id\":3,\"name\":\"Bill\"}, {\"id\":4,\"name\":\"Jeff\"}]}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/posters?select=*,subscriptions!subscriber(...posters!subscribed(*))&limit=1\" `shouldRespondWith`\n          [json| [{\"id\":1,\"name\":\"Mark\",\"subscriptions\":[{\"id\":2,\"name\":\"Elon\"}]}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"errs on an ambiguous embed that has two one-to-one relationships\" $\n        get \"/first?select=second(*)\" `shouldRespondWith`\n          [json| {\n            \"code\":\"PGRST201\",\n            \"details\":[\n              {\"cardinality\":\"one-to-one\",\"embedding\":\"first with second\",\"relationship\":\"first_second_id_1_fkey using first(second_id_1) and second(id)\"},\n              {\"cardinality\":\"one-to-one\",\"embedding\":\"first with second\",\"relationship\":\"first_second_id_2_fkey using first(second_id_2) and second(id)\"}\n            ],\n            \"hint\":\"Try changing 'second' to one of the following: 'second!first_second_id_1_fkey', 'second!first_second_id_2_fkey'. Find the desired relationship in the 'details' key.\",\"message\":\"Could not embed because more than one relationship was found for 'first' and 'second'\"\n            }|]\n          { matchStatus  = 300\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"errs with multiple references to the same composite key columns in a view\" $\n        get \"/i2459_composite_v2?select=*,i2459_composite_v1(*)\" `shouldRespondWith`\n          [json|\n            {\n              \"code\": \"PGRST201\",\n              \"details\": [\n                {\n                  \"cardinality\": \"many-to-one\",\n                  \"embedding\": \"i2459_composite_v2 with i2459_composite_v1\",\n                  \"relationship\": \"i2459_composite_t2_t1_a_t1_b_fkey using i2459_composite_v2(t1_a1, t1_b1) and i2459_composite_v1(a, b)\"\n                },\n                {\n                  \"cardinality\": \"many-to-one\",\n                  \"embedding\": \"i2459_composite_v2 with i2459_composite_v1\",\n                  \"relationship\": \"i2459_composite_t2_t1_a_t1_b_fkey using i2459_composite_v2(t1_a1, t1_b2) and i2459_composite_v1(a, b)\"\n                },\n                {\n                  \"cardinality\": \"many-to-one\",\n                  \"embedding\": \"i2459_composite_v2 with i2459_composite_v1\",\n                  \"relationship\": \"i2459_composite_t2_t1_a_t1_b_fkey using i2459_composite_v2(t1_a2, t1_b1) and i2459_composite_v1(a, b)\"\n                },\n                {\n                  \"cardinality\": \"many-to-one\",\n                  \"embedding\": \"i2459_composite_v2 with i2459_composite_v1\",\n                  \"relationship\": \"i2459_composite_t2_t1_a_t1_b_fkey using i2459_composite_v2(t1_a2, t1_b2) and i2459_composite_v1(a, b)\"\n                }\n              ],\n              \"hint\": \"Try changing 'i2459_composite_v1' to one of the following: 'i2459_composite_v1!i2459_composite_t2_t1_a_t1_b_fkey', 'i2459_composite_v1!i2459_composite_t2_t1_a_t1_b_fkey', 'i2459_composite_v1!i2459_composite_t2_t1_a_t1_b_fkey', 'i2459_composite_v1!i2459_composite_t2_t1_a_t1_b_fkey'. Find the desired relationship in the 'details' key.\",\n              \"message\": \"Could not embed because more than one relationship was found for 'i2459_composite_v2' and 'i2459_composite_v1'\"\n            }\n          |]\n          { matchStatus  = 300\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"disambiguating requests with embed hints\" $ do\n\n      context \"using FK to specify the relationship\" $ do\n        it \"can embed by FK name\" $\n          get \"/projects?id=in.(1,3)&select=id,name,client(id,name)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client\":{\"id\":2,\"name\":\"Apple\"}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can embed by FK name and select the FK column at the same time\" $\n          get \"/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client\":{\"id\":2,\"name\":\"Apple\"}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can embed parent with view!fk and grandparent by using fk\" $\n          get \"/tasks?id=eq.1&select=id,name,projects_view!project(id,name,client(id,name))\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Design w7\",\"projects_view\":{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1,\"name\":\"Microsoft\"}}}]|]\n\n        it \"can embed by using a composite FK name\" $\n          get \"/unit_workdays?select=unit_id,day,fst_shift(car_id,schedule(name)),snd_shift(camera_id,schedule(name))\" `shouldRespondWith`\n            [json| [\n              {\n                \"day\": \"2019-12-02\",\n                \"fst_shift\": {\n                    \"car_id\": \"CAR-349\",\n                    \"schedule\": {\n                        \"name\": \"morning\"\n                    }\n                },\n                \"snd_shift\": {\n                    \"camera_id\": \"CAM-123\",\n                    \"schedule\": {\n                        \"name\": \"night\"\n                    }\n                },\n                \"unit_id\": 1\n              }\n            ] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds by using two fks pointing to the same table\" $\n          get \"/orders?id=eq.1&select=id, name, billing(address), shipping(address)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"order 1\",\"billing\":{\"address\": \"address 1\"},\"shipping\":{\"address\": \"address 2\"}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"fails if the fk is not known\" $\n          get \"/message?select=id,sender:person!space(name)&id=lt.4\" `shouldRespondWith`\n            [json|{\n              \"hint\":null,\n              \"message\":\"Could not find a relationship between 'message' and 'person' in the schema cache\",\n              \"code\": \"PGRST200\",\n              \"details\":\"Searched for a foreign key relationship between 'message' and 'person' using the hint 'space' in the schema 'test', but no matches were found.\"}|]\n            { matchStatus = 400\n            , matchHeaders = [matchContentTypeJson] }\n\n        it \"can request a parent with fk\" $\n          get \"/comments?select=content,user(name)\" `shouldRespondWith`\n            [json|[ { \"content\": \"Needs to be delivered ASAP\", \"user\": { \"name\": \"Angela Martin\" } } ]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can request two parents with fks\" $\n          get \"/articleStars?select=createdAt,article:articles(id),user(name)&limit=1\"\n            `shouldRespondWith`\n              [json|[{\"createdAt\":\"2015-12-08T04:22:57.472738\",\"article\":{\"id\": 1},\"user\":{\"name\": \"Angela Martin\"}}]|]\n\n        it \"can specify a view!fk\" $\n          get \"/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4\" `shouldRespondWith`\n            [json|\n              [{\"id\":1,\"body\":\"Hello Jane\",\"sender\":{\"name\":\"John\",\"sent\":2},\"recipient\":{\"name\":\"Jane\",\"received\":2}},\n               {\"id\":2,\"body\":\"Hi John\",\"sender\":{\"name\":\"Jane\",\"sent\":1},\"recipient\":{\"name\":\"John\",\"received\":1}},\n               {\"id\":3,\"body\":\"How are you doing?\",\"sender\":{\"name\":\"John\",\"sent\":2},\"recipient\":{\"name\":\"Jane\",\"received\":2}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can specify a table!fk hint and request children 2 levels\" $\n          get \"/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))\" `shouldRespondWith`\n            [json|[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2}]},{\"id\":2,\"tasks\":[{\"id\":3},{\"id\":4}]}]}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can disambiguate with the fk in case of an o2m and m2m relationship to the same table\" $\n          get \"/sites?select=name,main_project(name)&site_id=eq.1\" `shouldRespondWith`\n            [json| [ { \"name\": \"site 1\", \"main_project\": { \"name\": \"big project 1\" } } ] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      context \"using the column name of the FK to specify the relationship\" $ do\n        it \"can embed by column\" $\n          get \"/projects?id=in.(1,3)&select=id,name,client_id(id,name)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":{\"id\":2,\"name\":\"Apple\"}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can embed by column and select the column at the same time, if aliased\" $\n          get \"/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client\":{\"id\":2,\"name\":\"Apple\"}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can embed parent by using view!column and grandparent by using the column\" $\n          get \"/tasks?id=eq.1&select=id,name,project:projects_view!project_id(id,name,client:client_id(id,name))\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Design w7\",\"project\":{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1,\"name\":\"Microsoft\"}}}]|]\n\n        it \"can specify table!column\" $\n          get \"/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4\" `shouldRespondWith`\n            [json|\n              [{\"id\":1,\"body\":\"Hello Jane\",\"sender\":{\"name\":\"John\"},\"recipient\":{\"name\":\"Jane\"}},\n               {\"id\":2,\"body\":\"Hi John\",\"sender\":{\"name\":\"Jane\"},\"recipient\":{\"name\":\"John\"}},\n               {\"id\":3,\"body\":\"How are you doing?\",\"sender\":{\"name\":\"John\"},\"recipient\":{\"name\":\"Jane\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"will embed using a column that has uppercase chars\" $\n          get \"/ghostBusters?select=escapeId(*)\" `shouldRespondWith`\n            [json| [{\"escapeId\":{\"so6meIdColumn\":1}},{\"escapeId\":{\"so6meIdColumn\":3}},{\"escapeId\":{\"so6meIdColumn\":5}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds by using two columns pointing to the same table\" $\n          get \"/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"order 1\",\"billing_address_id\":{\"id\":1},\"shipping_address_id\":{\"id\":2}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can disambiguate with the column in case of an o2m and m2m relationship to the same table\" $\n          get \"/sites?select=name,main_project_id(name)&site_id=eq.1\" `shouldRespondWith`\n            [json| [ { \"name\": \"site 1\", \"main_project_id\": { \"name\": \"big project 1\" } } ] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can specify all view column names that reference the same base column\" $ do\n          get \"/i2459_simple_v1?select=*,i2459_simple_v2!t1_id1(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/i2459_simple_v1?select=*,i2459_simple_v2!t1_id2(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/i2459_simple_v2?select=*,i2459_simple_v1!t1_id1(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/i2459_simple_v2?select=*,i2459_simple_v1!t1_id2(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      context \"using the junction to disambiguate the request\" $\n        it \"can specify the junction of an m2m relationship\" $ do\n          get \"/sites?select=*,big_projects!jobs(name)&site_id=in.(1,2)\" `shouldRespondWith`\n            [json|\n              [\n                {\n                  \"big_projects\": [\n                    {\n                      \"name\": \"big project 1\"\n                    }\n                  ],\n                  \"main_project_id\": 1,\n                  \"name\": \"site 1\",\n                  \"site_id\": 1\n                },\n                {\n                  \"big_projects\": [\n                    {\n                      \"name\": \"big project 1\"\n                    },\n                    {\n                      \"name\": \"big project 2\"\n                    }\n                  ],\n                  \"main_project_id\": null,\n                  \"name\": \"site 2\",\n                  \"site_id\": 2\n                }\n              ]\n            |]\n          get \"/sites?select=*,big_projects!main_jobs(name)&site_id=in.(1,2)\" `shouldRespondWith`\n            [json|\n              [\n                {\n                  \"big_projects\": [\n                    {\n                      \"name\": \"big project 1\"\n                    }\n                  ],\n                  \"main_project_id\": 1,\n                  \"name\": \"site 1\",\n                  \"site_id\": 1\n                },\n                {\n                  \"big_projects\": [],\n                  \"main_project_id\": null,\n                  \"name\": \"site 2\",\n                  \"site_id\": 2\n                }\n              ]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      context \"using a FK column and a FK to specify the relationship\" $\n        it \"embeds by using a column and a fk pointing to the same table\" $\n          get \"/orders?id=eq.1&select=id, name, billing_address_id(id), shipping(id)\" `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"order 1\",\"billing_address_id\":{\"id\":1},\"shipping\":{\"id\":2}}]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n    context \"tables with self reference foreign keys\" $ do\n      context \"one self reference foreign key\" $ do\n        it \"embeds parents recursively\" $\n          get \"/family_tree?id=in.(3,4)&select=id,parent(id,name,parent(*))\" `shouldRespondWith`\n            [json|[\n              { \"id\": \"3\", \"parent\": { \"id\": \"1\", \"name\": \"Parental Unit\", \"parent\": null } },\n              { \"id\": \"4\", \"parent\": { \"id\": \"2\", \"name\": \"Kid One\", \"parent\": { \"id\": \"1\", \"name\": \"Parental Unit\", \"parent\": null } } }\n            ]|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds children recursively\" $\n          get \"/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))\" `shouldRespondWith`\n            [json|[{\n              \"id\": \"1\", \"name\": \"Parental Unit\", \"children\": [\n                { \"id\": \"2\", \"name\": \"Kid One\", \"children\": [ { \"id\": \"4\", \"name\": \"Grandkid One\" } ] },\n                { \"id\": \"3\", \"name\": \"Kid Two\", \"children\": [ { \"id\": \"5\", \"name\": \"Grandkid Two\" } ] }\n              ]\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds parent and then embeds children\" $\n          get \"/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))\" `shouldRespondWith`\n            [json|[{\n              \"id\": \"2\", \"name\": \"Kid One\", \"parent\": {\n                \"id\": \"1\", \"name\": \"Parental Unit\", \"children\": [ { \"id\": \"2\", \"name\": \"Kid One\" }, { \"id\": \"3\", \"name\": \"Kid Two\"} ]\n              }\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds parent and then embeds children on a view\" $\n          get \"/job?select=id,parent_id(*),children:job!parent_id(id,parent_id)\" `shouldRespondWith`\n            [json|[\n              {\n                \"id\": 1,\n                \"parent_id\": null,\n                \"children\": [ { \"id\": 2, \"parent_id\": 1 } ]\n              },\n              {\n                \"id\": 2,\n                \"parent_id\": { \"id\": 1, \"parent_id\": null },\n                \"children\": []\n              }\n            ]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"can specify all view column names that reference the same base column\" $ do\n          get \"/i2459_self_v1?select=*,parent(*),grandparent(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/i2459_self_v2?select=*,parent(*),grandparent(*)\" `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      context \"two self reference foreign keys\" $ do\n        it \"embeds parents\" $\n          get \"/organizations?select=id,name,referee(id,name),auditor(id,name)&id=eq.3\" `shouldRespondWith`\n            [json|[{\n              \"id\": 3, \"name\": \"Acme\",\n              \"referee\": {\n                \"id\": 1,\n                \"name\": \"Referee Org\"\n              },\n              \"auditor\": {\n                \"id\": 2,\n                \"name\": \"Auditor Org\"\n              }\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds children\" $ do\n          get \"/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1\" `shouldRespondWith`\n            [json|[{\n              \"id\": 1, \"name\": \"Referee Org\",\n              \"refereeds\": [\n                {\n                  \"id\": 3,\n                  \"name\": \"Acme\"\n                },\n                {\n                  \"id\": 4,\n                  \"name\": \"Umbrella\"\n                }\n              ]\n            }]|] { matchHeaders = [matchContentTypeJson] }\n          get \"/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2\" `shouldRespondWith`\n            [json|[{\n              \"id\": 2, \"name\": \"Auditor Org\",\n              \"auditees\": [\n                {\n                  \"id\": 3,\n                  \"name\": \"Acme\"\n                },\n                {\n                  \"id\": 4,\n                  \"name\": \"Umbrella\"\n                }\n              ]\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n        it \"embeds other relations(manager) besides the self reference\" $ do\n          get \"/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5\" `shouldRespondWith`\n            [json|[{\n              \"name\":\"Cyberdyne\",\n              \"manager\":{\"name\":\"Cyberdyne Manager\"},\n              \"referee\":{\n                \"name\":\"Acme\",\n                \"manager\":{\"name\":\"Acme Manager\"},\n                \"auditor\":{\n                  \"name\":\"Auditor Org\",\n                  \"manager\":{\"name\":\"Auditor Manager\"}}},\n              \"auditor\":{\n                \"name\":\"Umbrella\",\n                \"manager\":{\"name\":\"Umbrella Manager\"},\n                \"referee\":{\n                  \"name\":\"Referee Org\",\n                  \"manager\":{\"name\":\"Referee Manager\"}}}\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n          get \"/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2\" `shouldRespondWith`\n            [json|[{\n              \"name\":\"Auditor Org\",\n              \"manager\":{\"name\":\"Auditor Manager\"},\n              \"auditees\":[\n                {\"name\":\"Acme\",\n                 \"manager\":{\"name\":\"Acme Manager\"},\n                 \"refereeds\":[\n                   {\"name\":\"Cyberdyne\",\n                    \"manager\":{\"name\":\"Cyberdyne Manager\"}},\n                   {\"name\":\"Oscorp\",\n                    \"manager\":{\"name\":\"Oscorp Manager\"}}]},\n                {\"name\":\"Umbrella\",\n                 \"manager\":{\"name\":\"Umbrella Manager\"},\n                 \"refereeds\":[]}]\n            }]|] { matchHeaders = [matchContentTypeJson] }\n\n    context \"m2m embed when there's a junction in an internal schema\" $ do\n      -- https://github.com/PostgREST/postgrest/issues/1736\n      it \"works with no ambiguity when there's an exposed view of the junction\" $ do\n        get \"/screens?select=labels(name)\" `shouldRespondWith`\n          [json|[{\"labels\":[{\"name\":\"fruit\"}]}, {\"labels\":[{\"name\":\"vehicles\"}]}, {\"labels\":[{\"name\":\"vehicles\"}, {\"name\":\"fruit\"}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/actors?select=*,films(*)\" `shouldRespondWith`\n          [json|[ {\"id\":1,\"name\":\"john\",\"films\":[{\"id\":12,\"title\":\"douze commandements\"}]},\n                  {\"id\":2,\"name\":\"mary\",\"films\":[{\"id\":2001,\"title\":\"odyssée de l'espace\"}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"doesn't work if the junction is only internal\" $\n        get \"/end_1?select=end_2(*)\" `shouldRespondWith`\n          [json|{\n            \"hint\": null,\n            \"message\":\"Could not find a relationship between 'end_1' and 'end_2' in the schema cache\",\n            \"code\":\"PGRST200\",\n            \"details\": \"Searched for a foreign key relationship between 'end_1' and 'end_2' in the schema 'test', but no matches were found.\"}|]\n          { matchStatus  = 400\n          , matchHeaders = [matchContentTypeJson] }\n      it \"shouldn't try to embed if the private junction has an exposed homonym\" $\n        -- ensures the \"invalid reference to FROM-clause entry for table \"rollen\" error doesn't happen.\n        -- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669\n        get \"/schauspieler?select=filme(*)\" `shouldRespondWith`\n          [json|{\n            \"hint\":null,\n            \"message\":\"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache\",\n            \"code\":\"PGRST200\",\n            \"details\":\"Searched for a foreign key relationship between 'schauspieler' and 'filme' in the schema 'test', but no matches were found.\"}|]\n          { matchStatus  = 400\n          , matchHeaders = [matchContentTypeJson] }\n\n    context \"embedding with col as a target doesn't consider views\" $ do\n      -- https://github.com/PostgREST/postgrest/issues/1643\n      it \"works with self reference both ways(m2o and o2m)\" $ do\n        get \"/test?select=id,parent_id,parent:parent_id(id)\" `shouldRespondWith`\n          [json| [\n            { \"id\": 1, \"parent_id\": null, \"parent\": null },\n            { \"id\": 2, \"parent_id\": 1, \"parent\": { \"id\": 1 } }\n          ] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/test?select=id,parent_id,childs:test(id)\" `shouldRespondWith`\n          [json| [\n            { \"id\": 1, \"parent_id\": null, \"childs\": [ { \"id\": 2 } ] },\n            { \"id\": 2, \"parent_id\": 1, \"childs\": [] }\n          ]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      -- https://github.com/PostgREST/postgrest/issues/2238\n      it \"has to be explicit for a view embedding\" $ do\n        get \"/adaptation_notifications?select=id,status,series(*)\" `shouldRespondWith`\n          [json| [] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/adaptation_notifications?select=id,status,series_popularity(*)\" `shouldRespondWith`\n          [json| [] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"resolves when there's a table and view that point to the same col\" $\n        get \"/message?select=id,body,sender(id,name)&id=eq.5\" `shouldRespondWith`\n          [json| [\n              {\n                \"id\": 5,\n                \"body\": \"What's up Jake\",\n                \"sender\": {\n                  \"id\": 4,\n                  \"name\": \"Julie\"\n                }\n              }\n            ] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"resolves when there's a table and view that point to the same fk (composite pk)\" $\n        get \"/activities?select=fst_shift(*)\" `shouldRespondWith`\n          [json| [\n              {\n                \"fst_shift\": [\n                  { \"unit_id\": 1, \"day\": \"2019-12-02\", \"fst_shift_activity_id\": 1, \"fst_shift_schedule_id\": 1, \"snd_shift_activity_id\": 2, \"snd_shift_schedule_id\": 3 }\n                ]\n              },\n              {\n                \"fst_shift\": []\n              }\n            ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    it \"should not expose hidden FKs\" $\n      get \"/va?select=vb(*)\" `shouldRespondWith` 200\n"
  },
  {
    "path": "test/spec/Feature/Query/EmbedInnerJoinSpec.hs",
    "content": "module Feature.Query.EmbedInnerJoinSpec where\n\nimport Network.HTTP.Types\nimport Network.Wai        (Application)\n\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Embedding with an inner join\" $ do\n    context \"many-to-one relationships\" $ do\n      it \"ignores null embeddings while the default left join doesn't\" $ do\n        get \"/projects?select=id,clients!inner(id)\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"clients\":{\"id\":1}}, {\"id\":2,\"clients\":{\"id\":1}},\n            {\"id\":3,\"clients\":{\"id\":2}}, {\"id\":4,\"clients\":{\"id\":2}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/projects?select=id,clients!left(id)\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"clients\":{\"id\":1}}, {\"id\":2,\"clients\":{\"id\":1}},\n            {\"id\":3,\"clients\":{\"id\":2}}, {\"id\":4,\"clients\":{\"id\":2}},\n            {\"id\":5,\"clients\":null}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/projects?select=id,clients!inner(id)\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-3/4\" ]\n          }\n\n      it \"filters source tables when the embedded table is filtered\" $ do\n        get \"/projects?select=id,clients!inner(id)&clients.id=eq.1\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"clients\":{\"id\":1}},\n            {\"id\":2,\"clients\":{\"id\":1}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/projects?select=id,clients!inner(id)&clients.id=eq.2\" `shouldRespondWith`\n          [json|[\n            {\"id\":3,\"clients\":{\"id\":2}},\n            {\"id\":4,\"clients\":{\"id\":2}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/projects?select=id,clients!inner(id)&clients.id=eq.0\" `shouldRespondWith`\n          [json|[]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/projects?select=id,clients!inner(id)&clients.id=eq.1\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-1/2\" ]\n          }\n\n      it \"filters source tables when a two levels below embedded table is filtered\" $ do\n        get \"/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"projects\":{\"id\":1,\"clients\":{\"id\":1}}},\n            {\"id\":2,\"projects\":{\"id\":1,\"clients\":{\"id\":1}}},\n            {\"id\":3,\"projects\":{\"id\":2,\"clients\":{\"id\":1}}},\n            {\"id\":4,\"projects\":{\"id\":2,\"clients\":{\"id\":1}}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.2\" `shouldRespondWith`\n          [json|[\n            {\"id\":5,\"projects\":{\"id\":3,\"clients\":{\"id\":2}}},\n            {\"id\":6,\"projects\":{\"id\":3,\"clients\":{\"id\":2}}},\n            {\"id\":7,\"projects\":{\"id\":4,\"clients\":{\"id\":2}}},\n            {\"id\":8,\"projects\":{\"id\":4,\"clients\":{\"id\":2}}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-3/4\" ]\n          }\n\n      it \"only affects the source table rows if his direct embedding is an inner join\" $ do\n        get \"/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"projects\":null},\n            {\"id\":2,\"projects\":null},\n            {\"id\":3,\"projects\":null},\n            {\"id\":4,\"projects\":null},\n            {\"id\":5,\"projects\":{\"id\":3,\"clients\":{\"id\":2}}},\n            {\"id\":6,\"projects\":{\"id\":3,\"clients\":{\"id\":2}}},\n            {\"id\":7,\"projects\":{\"id\":4,\"clients\":{\"id\":2}}},\n            {\"id\":8,\"projects\":{\"id\":4,\"clients\":{\"id\":2}}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-7/8\" ]\n          }\n\n      it \"works with views\" $ do\n        get \"/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell\" `shouldRespondWith`\n          [json| [{\"title\":\"1984\",\"authors\":{\"name\":\"George Orwell\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n    context \"one-to-many relationships\" $ do\n      it \"ignores empty array embeddings while the default left join doesn't\" $ do\n        get \"/entities?select=id,child_entities!inner(id)\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"child_entities\":[{\"id\":1}, {\"id\":2}, {\"id\":4}, {\"id\":5}]},\n            {\"id\":2,\"child_entities\":[{\"id\":3}, {\"id\":6}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/entities?select=id,child_entities!left(id)\" `shouldRespondWith`\n          [json| [\n            {\"id\":1,\"child_entities\":[{\"id\":1}, {\"id\":2}, {\"id\":4}, {\"id\":5}]},\n            {\"id\":2,\"child_entities\":[{\"id\":3}, {\"id\":6}]},\n            {\"id\":3,\"child_entities\":[]},\n            {\"id\":4,\"child_entities\":[]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/entities?select=id,child_entities!inner(id)\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-1/2\" ]\n          }\n\n      it \"filters source tables when the embedded table is filtered\" $ do\n        get \"/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1\" `shouldRespondWith`\n          [json|[{\"id\":1,\"child_entities\":[{\"id\":1}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/entities?select=id,child_entities!inner(id)&child_entities.id=eq.3\" `shouldRespondWith`\n          [json|[{\"id\":2,\"child_entities\":[{\"id\":3}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/entities?select=id,child_entities!inner(id)&child_entities.id=eq.0\" `shouldRespondWith`\n          [json|[]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n      it \"filters source tables when a two levels below embedded table is filtered\" $ do\n        get \"/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)\"\n          `shouldRespondWith`\n          [json|[\n            {\n              \"id\": 1,\n              \"child_entities\": [\n                { \"id\": 1, \"grandchild_entities\": [ { \"id\": 1 } ] },\n                { \"id\": 2, \"grandchild_entities\": [ { \"id\": 5 } ] }]\n            }\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=eq.2\" `shouldRespondWith`\n          [json|[\n            {\n              \"id\": 1,\n              \"child_entities\": [\n                { \"id\": 1, \"grandchild_entities\": [ { \"id\": 2 } ] } ]\n            }\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n      it \"only affects the source table rows if his direct embedding is an inner join\" $ do\n        get \"/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2\" `shouldRespondWith`\n          [json|[\n            {\n              \"id\": 1,\n              \"child_entities\": [\n                { \"id\": 1, \"grandchild_entities\": [ { \"id\": 2 } ] },\n                { \"id\": 2, \"grandchild_entities\": [] },\n                { \"id\": 4, \"grandchild_entities\": [] },\n                { \"id\": 5, \"grandchild_entities\": [] } ]\n            },\n            {\n              \"id\": 2,\n              \"child_entities\": [\n                { \"id\": 3, \"grandchild_entities\": [] },\n                { \"id\": 6, \"grandchild_entities\": [] } ]\n            }\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-1/2\" ]\n          }\n\n      it \"works with views\" $ do\n        get \"/authors?select=*,books!inner(*)&books.title=eq.1984\" `shouldRespondWith`\n          [json| [{\"id\":1,\"name\":\"George Orwell\",\"books\":[{\"id\":1,\"title\":\"1984\",\"publication_year\":1949,\"author_id\":1,\"first_publisher_id\":1}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/authors?select=*,books!inner(*)&books.title=eq.1984\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n    context \"many-to-many relationships\" $ do\n      it \"ignores empty array embeddings while the default left join doesn't\" $ do\n        get \"/products?select=id,suppliers!inner(id)\" `shouldRespondWith`\n          [json| [\n            {\"id\":1,\"suppliers\":[{\"id\":1}, {\"id\":2}]},\n            {\"id\":2,\"suppliers\":[{\"id\":1}, {\"id\":3}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/products?select=id,suppliers!left(id)\" `shouldRespondWith`\n          [json| [\n            {\"id\":1,\"suppliers\":[{\"id\":1}, {\"id\":2}]},\n            {\"id\":2,\"suppliers\":[{\"id\":1}, {\"id\":3}]},\n            {\"id\":3,\"suppliers\":[]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/products?select=id,suppliers!inner(id)\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-1/2\" ]\n          }\n\n      it \"filters source tables when the embedded table is filtered\" $ do\n        get \"/products?select=id,suppliers!inner(id)&suppliers.id=eq.2\" `shouldRespondWith`\n          [json| [{\"id\":1,\"suppliers\":[{\"id\":2}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/products?select=id,suppliers!inner(id)&suppliers.id=eq.3\" `shouldRespondWith`\n          [json| [{\"id\":2,\"suppliers\":[{\"id\":3}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/products?select=id,suppliers!inner(id)&suppliers.id=eq.0\" `shouldRespondWith`\n          [json| [] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/products?select=id,suppliers!inner(id)&suppliers.id=eq.2\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n      it \"filters source tables when a two levels below embedded table is filtered\" $ do\n        get \"/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3\"\n          `shouldRespondWith`\n          [json|[{\"id\":1,\"suppliers\":[{\"id\":2,\"trade_unions\":[{\"id\":3}]}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.4\"\n          `shouldRespondWith`\n          [json|[{\"id\":1,\"suppliers\":[{\"id\":2,\"trade_unions\":[{\"id\":4}]}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n      it \"only affects the source table rows if his direct embedding is an inner join\" $ do\n        get \"/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"suppliers\":[{\"id\":1,\"trade_unions\":[]}, {\"id\":2,\"trade_unions\":[{\"id\":3}]}]},\n            {\"id\":2,\"suppliers\":[{\"id\":1,\"trade_unions\":[]}, {\"id\":3,\"trade_unions\":[]}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-1/2\" ]\n          }\n\n      it \"works with views\" $ do\n        get \"/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements\" `shouldRespondWith`\n          [json| [{\"id\":1,\"name\":\"john\",\"films\":[{\"id\":12,\"title\":\"douze commandements\"}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/films?select=*,actors!inner(*)&actors.name=eq.john\" `shouldRespondWith`\n          [json| [{\"id\":12,\"title\":\"douze commandements\",\"actors\":[{\"id\":1,\"name\":\"john\"}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        request methodHead \"/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements\" [(\"Prefer\", \"count=exact\")] mempty\n          `shouldRespondWith` \"\"\n          { matchStatus  = 200\n          , matchHeaders = [ matchContentTypeJson\n                           , \"Content-Range\" <:> \"0-0/1\" ]\n          }\n\n    it \"works with m2o and m2m relationships combined\" $ do\n      get \"/projects?select=name,clients!inner(name),users!inner(name)\" `shouldRespondWith`\n        [json| [\n          {\"name\":\"Windows 7\",\"clients\":{\"name\":\"Microsoft\"},\"users\":[{\"name\":\"Angela Martin\"}, {\"name\":\"Dwight Schrute\"}]},\n          {\"name\":\"Windows 10\",\"clients\":{\"name\":\"Microsoft\"},\"users\":[{\"name\":\"Angela Martin\"}]},\n          {\"name\":\"IOS\",\"clients\":{\"name\":\"Apple\"},\"users\":[{\"name\":\"Michael Scott\"}, {\"name\":\"Dwight Schrute\"}]},\n          {\"name\":\"OSX\",\"clients\":{\"name\":\"Apple\"},\"users\":[{\"name\":\"Michael Scott\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      request methodHead \"/projects?select=name,clients!inner(name),users!inner(name)\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-3/4\" ]\n        }\n\n    it \"works with rpc\" $ do\n      get \"/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1\" `shouldRespondWith`\n        [json| [{\"id\":1,\"clients\":{\"id\":1}}, {\"id\":2,\"clients\":{\"id\":1}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      request methodHead \"/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-1/2\" ]\n        }\n\n    it \"works when using hints\" $ do\n      get \"/projects?select=id,clients!client!inner(id)&clients.id=eq.2\" `shouldRespondWith`\n        [json| [{\"id\":3,\"clients\":{\"id\":2}}, {\"id\":4,\"clients\":{\"id\":2}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/projects?select=id,client!inner(id)&client.id=eq.2\" `shouldRespondWith`\n        [json| [{\"id\":3,\"client\":{\"id\":2}}, {\"id\":4,\"client\":{\"id\":2}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      request methodHead \"/projects?select=id,clients!client!inner(id)&clients.id=eq.2\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-1/2\" ]\n        }\n\n    it \"works with many one-to-many relationships\" $ do\n      -- https://github.com/PostgREST/postgrest/issues/1977\n      get \"/client?select=id,name,contact!inner(name),clientinfo!inner(other)\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\",\"contact\":[{\"name\":\"Wally Walton\"}, {\"name\":\"Wilma Wellers\"}],\"clientinfo\":[{\"other\":\"123 Main St\"}]},\n          {\"id\":2,\"name\":\"Target\", \"contact\":[{\"name\":\"Tabby Targo\"}],\"clientinfo\":[{\"other\":\"456 South 3rd St\"}]},\n          {\"id\":3,\"name\":\"Big Lots\",\"contact\":[{\"name\":\"Bobby Bots\"}, {\"name\":\"Bonnie Bits\"}, {\"name\":\"Billy Boats\"}],\"clientinfo\":[{\"other\":\"789 Palm Tree Ln\"}]}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/client?select=id,name,contact!inner(name),clientinfo!inner(other)&contact.name=eq.Wally%20Walton\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\",\"contact\":[{\"name\":\"Wally Walton\"}],\"clientinfo\":[{\"other\":\"123 Main St\"}]}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/client?select=id,name,contact!inner(name),clientinfo!inner(other)&clientinfo.other=eq.456%20South%203rd%20St\" `shouldRespondWith`\n        [json|[\n          {\"id\":2,\"name\":\"Target\",\"clientinfo\":[{\"other\":\"456 South 3rd St\"}],\"contact\":[{\"name\":\"Tabby Targo\"}]}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n      request methodHead \"/client?select=id,name,contact!inner(name),clientinfo!inner(other)\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-2/3\" ]\n        }\n\n    it \"works alongside another embedding\" $ do\n      -- https://github.com/PostgREST/postgrest/issues/2342\n      get \"/books?select=id,authors(name),publishers!inner(name)&id=gte.7\"\n        `shouldRespondWith`\n        [json| [\n          {\"id\":7,\"authors\":{\"name\":\"Harper Lee\"},\"publishers\":{\"name\":\"J. B. Lippincott & Co.\"}},\n          {\"id\":8,\"authors\":{\"name\":\"Kurt Vonnegut\"},\"publishers\":{\"name\":\"Delacorte\"}},\n          {\"id\":9,\"authors\":{\"name\":\"Ken Kesey\"},\"publishers\":{\"name\":\"Viking Press & Signet Books\"}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      request methodHead \"/books?select=id,authors(name),publishers!inner(name)&id=gte.7\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-2/3\" ]\n        }\n      request methodHead \"/books?select=id,publishers!inner(name),authors(name)&id=gte.7\" [(\"Prefer\", \"count=exact\")] mempty\n        `shouldRespondWith` \"\"\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-2/3\" ]\n        }\n"
  },
  {
    "path": "test/spec/Feature/Query/ErrorSpec.hs",
    "content": "module Feature.Query.ErrorSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\npgErrorCodeMapping :: SpecWith ((), Application)\npgErrorCodeMapping = do\n  describe \"PostreSQL error code mappings\" $ do\n    it \"should return 500 for cardinality_violation\" $\n      get \"/bad_subquery\" `shouldRespondWith` 500\n\n    it \"should return 500 for statement too complex\" $\n      request methodPost \"/infinite_inserts\"\n        []\n        [json|{\"id\": 3, \"name\": \"qwer\"}|]\n        `shouldRespondWith`\n        [json|\n          {\"code\": \"54001\",\n           \"details\": null,\n           \"hint\": \"Increase the configuration parameter \\\"max_stack_depth\\\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.\",\n            \"message\": \"stack depth limit exceeded\"}|]\n        { matchStatus = 500\n        , matchHeaders = [\"Content-Length\" <:> \"217\"] }\n\n    context \"includes the proxy-status header on the response\" $ do\n      it \"works with ApiRequest error\" $\n        get \"/invalid/nested/paths\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST125\",\"details\":null,\"hint\":null,\"message\":\"Invalid path specified in request URL\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST125\"\n                           , \"Content-Length\" <:> \"96\" ]\n          }\n\n      it \"works with SchemaCache error\" $\n        get \"/non_existent_table\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.non_existent_table' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST205\"\n                           , \"Content-Length\" <:> \"129\" ]\n          }\n\n      it \"works with Jwt error\" $ do\n        let auth = authHeaderJWT \"ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0\"\n        request methodGet \"/authors_only\" [auth] \"\"\n          `shouldRespondWith`\n          [json| {\"message\":\"Expected 3 parts in JWT; got 2\",\"code\":\"PGRST301\",\"hint\":null,\"details\":null} |]\n          { matchStatus = 401\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST301\"\n                           , \"Content-Length\" <:> \"89\" ]\n          }\n\n      it \"works with raise sqlstate custom error\" $\n        get \"/rpc/raise_pt402\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PT402\",\"details\":\"Quota exceeded\",\"hint\":\"Upgrade your plan\",\"message\":\"Payment Required\"} |]\n          { matchStatus  = 402\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PT402\"\n                           , \"Content-Length\" <:> \"99\" ]\n          }\n\n      it \"works with sqlstate PGRST custom error\" $\n        get \"/rpc/raise_sqlstate_test1\"\n          `shouldRespondWith`\n          [json| {\"code\":\"123\",\"details\":\"DEF\",\"hint\":\"XYZ\",\"message\":\"ABC\"} |]\n          { matchStatus  = 332\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=123\"\n                           , \"Content-Length\" <:> \"59\" ]\n          }\n\n    context \"show hint on PGRST205 table not found error\" $ do\n      it \"show hint when similarity score is at least 75%\" $ do\n        get \"/projectx\" -- at least 75% similar to \"projects\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":\"Perhaps you meant the table 'test.projects'\",\"message\":\"Could not find the table 'test.projectx' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST205\"\n                           , \"Content-Length\" <:> \"160\" ]\n          }\n\n        get \"/projecxx\" -- at least 75% similar to \"projects\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":\"Perhaps you meant the table 'test.projects'\",\"message\":\"Could not find the table 'test.projecxx' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST205\"\n                           , \"Content-Length\" <:> \"160\" ]\n          }\n\n      it \"don't show hint when similarity score is less than 75%\" $\n        get \"/projxxxx\" -- less than 75% similar to \"projects\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.projxxxx' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Proxy-Status\" <:> \"PostgREST; error=PGRST205\"\n                           , \"Content-Length\" <:> \"119\" ]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/InsertSpec.hs",
    "content": "module Feature.Query.InsertSpec where\n\nimport Data.List              (lookup)\nimport Network.Wai            (Application)\nimport Network.Wai.Test       (SResponse (simpleHeaders))\nimport Test.Hspec             hiding (pendingWith)\nimport Test.Hspec.Wai.Matcher (bodyEquals)\n\nimport Network.HTTP.Types\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\nimport Text.Heredoc\n\nimport PostgREST.Config.PgVersion (PgVersion, pgVersion140)\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: PgVersion -> SpecWith ((), Application)\nspec actualPgVersion = do\n  describe \"Posting new record\" $ do\n    context \"disparate json types\" $ do\n      it \"accepts disparate json types\" $ do\n        post \"/menagerie\"\n          [json| {\n            \"integer\": 13, \"double\": 3.14159, \"varchar\": \"testing!\"\n          , \"boolean\": false, \"date\": \"1900-01-01\", \"money\": \"$3.99\"\n          , \"enum\": \"foo\"\n          } |] `shouldRespondWith` \"\"\n          { matchStatus  = 201\n            -- should not have content type set when body is empty\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , \"Content-Length\" <:> \"0\"]\n          }\n\n      it \"filters columns in result using &select\" $\n        request methodPost \"/menagerie?select=integer,varchar\" [(\"Prefer\", \"return=representation\")]\n          [json| [{\n            \"integer\": 14, \"double\": 3.14159, \"varchar\": \"testing!\"\n          , \"boolean\": false, \"date\": \"1900-01-01\", \"money\": \"$3.99\"\n          , \"enum\": \"foo\"\n          }] |] `shouldRespondWith` [json|[{\"integer\":14,\"varchar\":\"testing!\"}]|]\n          { matchStatus  = 201\n          , matchHeaders = [matchContentTypeJson\n                           , \"Content-Length\" <:> \"37\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"ignores &select when return not set or using return=minimal\" $ do\n        request methodPost \"/menagerie?select=integer,varchar\"\n            []\n            [json| [{\n              \"integer\": 15, \"double\": 3.14159, \"varchar\": \"testing!\",\n              \"boolean\": false, \"date\": \"1900-01-01\", \"money\": \"$3.99\",\n              \"enum\": \"foo\"\n            }] |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [matchHeaderAbsent hContentType]\n            }\n        request methodPost \"/menagerie?select=integer,varchar\"\n            [(\"Prefer\", \"return=minimal\")]\n            [json| [{\n              \"integer\": 16, \"double\": 3.14159, \"varchar\": \"testing!\",\n              \"boolean\": false, \"date\": \"1900-01-01\", \"money\": \"$3.99\",\n              \"enum\": \"foo\"\n            }] |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n\n    context \"non uniform json array\" $ do\n      it \"rejects json array that isn't exclusivily composed of objects\" $\n        post \"/articles\"\n             [json| [{\"id\": 100, \"body\": \"xxxxx\"}, 123, \"xxxx\", {\"id\": 111, \"body\": \"xxxx\"}] |]\n        `shouldRespondWith`\n             [json| {\"message\":\"All object keys must match\",\"code\":\"PGRST102\",\"hint\":null,\"details\":null} |]\n             { matchStatus  = 400\n             , matchHeaders = [matchContentTypeJson]\n             }\n\n      it \"rejects json array that has objects with different keys\" $\n        post \"/articles\"\n             [json| [{\"id\": 100, \"body\": \"xxxxx\"}, {\"id\": 111, \"body\": \"xxxx\", \"owner\": \"me\"}] |]\n        `shouldRespondWith`\n             [json| {\"message\":\"All object keys must match\",\"code\":\"PGRST102\",\"hint\":null,\"details\":null} |]\n             { matchStatus  = 400\n             , matchHeaders = [matchContentTypeJson]\n             }\n\n    context \"requesting full representation\" $ do\n      it \"includes related data after insert\" $\n        request methodPost \"/projects?select=id,name,clients(id,name)\"\n                [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"count=exact\")]\n          [json|{\"id\":6,\"name\":\"New Project\",\"client_id\":2}|] `shouldRespondWith` [json|[{\"id\":6,\"name\":\"New Project\",\"clients\":{\"id\":2,\"name\":\"Apple\"}}]|]\n          { matchStatus  = 201\n          , matchHeaders = [ matchContentTypeJson\n                           , matchHeaderAbsent hLocation\n                           , \"Content-Range\" <:> \"*/1\"\n                           , \"Preference-Applied\" <:> \"return=representation, count=exact\"]\n          }\n\n      it \"can rename and cast the selected columns\" $\n        request methodPost \"/projects?select=pId:id::text,pName:name,cId:client_id::text\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"id\":7,\"name\":\"New Project\",\"client_id\":2}|] `shouldRespondWith`\n          [json|[{\"pId\":\"7\",\"pName\":\"New Project\",\"cId\":\"2\"}]|]\n          { matchStatus  = 201\n          , matchHeaders = [ matchContentTypeJson\n                           , matchHeaderAbsent hLocation\n                           , \"Content-Range\" <:> \"*/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"should not throw and return location header when selecting without PK\" $\n        request methodPost \"/projects?select=name,client_id\" [(\"Prefer\", \"return=representation\")]\n          [json|{\"id\":10,\"name\":\"New Project\",\"client_id\":2}|] `shouldRespondWith`\n          [json|[{\"name\":\"New Project\",\"client_id\":2}]|]\n          { matchStatus  = 201\n          , matchHeaders = [ matchContentTypeJson\n                           , matchHeaderAbsent hLocation\n                           , \"Content-Range\" <:> \"*/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n    context \"requesting headers only representation\" $ do\n      it \"should not throw and return location header when selecting without PK\" $\n        request methodPost \"/projects?select=name,client_id\"\n            [(\"Prefer\", \"return=headers-only\")]\n            [json|{\"id\":11,\"name\":\"New Project\",\"client_id\":2}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/projects?id=eq.11\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n      it \"should not throw and return location header for partitioned tables when selecting without PK\" $\n        request methodPost \"/car_models\"\n            [(\"Prefer\", \"return=headers-only\")]\n            [json|{\"name\":\"Enzo\",\"year\":2021}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/car_models?name=eq.Enzo&year=eq.2021\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n    context \"requesting no representation\" $\n      it \"should not throw and return no location header when selecting without PK\" $\n        request methodPost \"/projects?select=name,client_id\"\n            []\n            [json|{\"id\":12,\"name\":\"New Project\",\"client_id\":2}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hLocation\n                             , \"Content-Length\" <:> \"0\"]\n            }\n\n    context \"from an html form\" $\n      it \"accepts disparate json types\" $ do\n        request methodPost \"/menagerie\"\n            [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n             (\"integer=7&double=2.71828&varchar=forms+are+fun&\" <>\n              \"boolean=false&date=1900-01-01&money=$3.99&enum=foo\")\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Content-Length\" <:> \"0\"]\n            }\n\n    context \"with no pk supplied\" $ do\n      context \"into a table with auto-incrementing pk\" $\n        it \"succeeds with 201 and location header\" $ do\n          -- reset pk sequence first to make test repeatable\n          request methodPost \"/rpc/reset_sequence\"\n              [(\"Prefer\", \"tx=commit\")]\n              [json|{\"name\": \"auto_incrementing_pk_id_seq\", \"value\": 2}|]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 204\n              , matchHeaders = [  matchHeaderAbsent hContentType\n                                , matchHeaderAbsent hContentLength ]\n              }\n\n          request methodPost \"/auto_incrementing_pk\"\n              [(\"Prefer\", \"return=headers-only\")]\n              [json| { \"non_nullable_string\":\"not null\"} |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 201\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , \"Content-Length\" <:> \"0\"\n                               , \"Location\" <:> \"/auto_incrementing_pk?id=eq.2\"\n                               , \"Preference-Applied\" <:> \"return=headers-only\"]\n              }\n\n      context \"into a table with simple pk\" $\n        it \"fails with 400 and error\" $\n          post \"/simple_pk\" [json| { \"extra\":\"foo\"} |]\n          `shouldRespondWith`\n          [json|{\"hint\":null,\"details\":\"Failing row contains (null, foo).\",\"code\":\"23502\",\"message\":\"null value in column \\\"k\\\" of relation \\\"simple_pk\\\" violates not-null constraint\"}|]\n          { matchStatus  = 400\n          , matchHeaders = [ matchContentTypeJson]\n          }\n\n      context \"into a table with no pk\" $ do\n        it \"succeeds with 201 but no location header\" $ do\n          post \"/no_pk\"\n              [json| { \"a\":\"foo\", \"b\":\"bar\" } |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 201\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hLocation ]\n              }\n\n        it \"returns full details of inserted record if asked\" $ do\n          request methodPost \"/no_pk\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| { \"a\":\"bar\", \"b\":\"baz\" } |]\n            `shouldRespondWith`\n              [json| [{ \"a\":\"bar\", \"b\":\"baz\" }] |]\n              { matchStatus  = 201\n              , matchHeaders = [matchHeaderAbsent hLocation\n                               , \"Preference-Applied\" <:> \"return=representation\"]\n              }\n\n        it \"returns empty array when no items inserted, and return=rep\" $ do\n          request methodPost \"/no_pk\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [] |]\n            `shouldRespondWith`\n              [json| [] |]\n              { matchStatus = 201 }\n\n        it \"can post nulls\" $ do\n          request methodPost \"/no_pk\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| { \"a\":null, \"b\":\"foo\" } |]\n            `shouldRespondWith`\n              [json| [{ \"a\":null, \"b\":\"foo\" }] |]\n              { matchStatus  = 201\n              , matchHeaders = [matchHeaderAbsent hLocation]\n              }\n\n    context \"with compound pk supplied\" $\n      it \"builds response location header appropriately\" $ do\n        request methodPost \"/compound_pk\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { \"k1\":12, \"k2\":\"Rock & R+ll\" } |]\n          `shouldRespondWith`\n            [json|[ { \"k1\":12, \"k2\":\"Rock & R+ll\", \"extra\": null } ]|]\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hLocation ]\n            }\n\n    context \"with bulk insert\" $\n      it \"returns 201 but no location header\" $ do\n        let bulkData = [json| [ {\"k1\":21, \"k2\":\"hello world\"}\n                              , {\"k1\":22, \"k2\":\"bye for now\"}]\n                            |]\n        request methodPost \"/compound_pk\"\n            []\n            bulkData\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hLocation ]\n            }\n\n    context \"with invalid json payload\" $\n      it \"fails with 400 and error\" $\n        post \"/simple_pk\" \"}{ x = 2\"\n        `shouldRespondWith`\n        [json|{\"message\":\"Empty or invalid json\",\"code\":\"PGRST102\",\"details\":null,\"hint\":null}|]\n        { matchStatus  = 400\n        , matchHeaders = [ matchContentTypeJson,\n                           \"Content-Length\" <:> \"80\" ]\n        }\n\n    context \"with no payload\" $\n      it \"fails with 400 and error\" $\n        post \"/simple_pk\" \"\"\n        `shouldRespondWith`\n        [json|{\"message\":\"Empty or invalid json\",\"code\":\"PGRST102\",\"details\":null,\"hint\":null}|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    context \"with valid json payload\" $\n      it \"succeeds and returns 201 created\" $\n        post \"/simple_pk\"\n            [json| { \"k\":\"k1\", \"extra\":\"e1\" } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 201\n            , matchHeaders = [matchHeaderAbsent hContentType]\n            }\n\n    context \"attempting to insert a row with the same primary key\" $\n      it \"fails returning a 409 Conflict\" $\n        post \"/simple_pk\"\n            [json| { \"k\":\"xyyx\", \"extra\":\"e1\" } |]\n          `shouldRespondWith`\n            [json|{\"hint\":null,\"details\":\"Key (k)=(xyyx) already exists.\",\"code\":\"23505\",\"message\":\"duplicate key value violates unique constraint \\\"simple_pk_pkey\\\"\"}|]\n            { matchStatus  = 409 }\n\n    context \"attempting to insert a row with conflicting unique constraint\" $\n      it \"fails returning a 409 Conflict\" $\n        post \"/withUnique\"  [json| { \"uni\":\"nodup\", \"extra\":\"e2\" } |] `shouldRespondWith` 409\n\n    context \"jsonb\" $ do\n      it \"serializes nested object\" $ do\n        let inserted = [json| { \"data\": { \"foo\":\"bar\" } } |]\n        request methodPost \"/json_table\"\n                     [(\"Prefer\", \"return=representation\")]\n                     inserted\n          `shouldRespondWith` [json|[{\"data\":{\"foo\":\"bar\"}}]|]\n          { matchStatus  = 201\n          }\n\n      it \"serializes nested array\" $ do\n        let inserted = [json| { \"data\": [1,2,3] } |]\n        request methodPost \"/json_table\"\n                     [(\"Prefer\", \"return=representation\")]\n                     inserted\n          `shouldRespondWith` [json|[{\"data\":[1,2,3]}]|]\n          { matchStatus  = 201\n          }\n\n    context \"empty objects\" $ do\n      it \"successfully inserts a row with all-default columns\" $ do\n        post \"/items\"\n            [json|{}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [matchHeaderAbsent hContentType]\n            }\n        post \"/items\" \"[{}]\" `shouldRespondWith` \"\"\n          { matchStatus  = 201\n          , matchHeaders = []\n          }\n\n      it \"successfully inserts two rows with all-default columns\" $\n        post \"/items\"\n            [json|[{}, {}]|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [matchHeaderAbsent hContentType]\n            }\n\n      it \"successfully inserts a row with all-default columns with prefer=rep\" $ do\n        -- reset pk sequence first to make test repeatable\n        request methodPost \"/rpc/reset_sequence\"\n            [(\"Prefer\", \"tx=commit\")]\n            [json|{\"name\": \"items2_id_seq\", \"value\": 20}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [  matchHeaderAbsent hContentType\n                              , matchHeaderAbsent hContentLength ]\n            }\n\n        request methodPost \"/items2\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{}|]\n          `shouldRespondWith`\n            [json|[{ id: 20 }]|]\n            { matchStatus  = 201 }\n\n      it \"successfully inserts a row with all-default columns with prefer=rep and &select=\" $ do\n        -- reset pk sequence first to make test repeatable\n        request methodPost \"/rpc/reset_sequence\"\n            [(\"Prefer\", \"tx=commit\")]\n            [json|{\"name\": \"items3_id_seq\", \"value\": 20}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [  matchHeaderAbsent hContentType\n                              , matchHeaderAbsent hContentLength ]\n            }\n\n        request methodPost \"/items3?select=id\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{}|]\n          `shouldRespondWith` [json|[{ id: 20 }]|]\n            { matchStatus  = 201 }\n\n    context \"insignificant whitespace\" $ do\n      it \"ignores it and successfuly inserts with json payload\" $ do\n        request methodPost \"/json_table\"\n                     [(\"Prefer\", \"return=representation\")]\n                     \"\\t \\n \\r { \\\"data\\\": { \\\"foo\\\":\\\"bar\\\" } }\\t \\n \\r \"\n          `shouldRespondWith` [json|[{\"data\":{\"foo\":\"bar\"}}]|]\n          { matchStatus  = 201\n          }\n\n        request methodPost \"/json_table\"\n                     [(\"Prefer\", \"return=representation\")]\n                     \"\\t \\n \\r [{ \\\"data\\\": { \\\"foo\\\":\\\"bar\\\" } }, \\t \\n \\r {\\\"data\\\": 34}]\\t \\n \\r \"\n          `shouldRespondWith` [json|[{\"data\":{\"foo\":\"bar\"}}, {\"data\":34}]|]\n          { matchStatus  = 201\n          }\n\n    -- https://github.com/PostgREST/postgrest/issues/2861\n    context \"bit and char columns with length\" $ do\n      it \"should insert to a bit column with length\" $\n        request methodPost \"/bitchar_with_length?select=bit\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{\"bit\": \"10101\"}|]\n          `shouldRespondWith` [json|[{ \"bit\": \"10101\" }]|]\n            { matchStatus  = 201 }\n\n      it \"should insert to a char column with length\" $\n        request methodPost \"/bitchar_with_length?select=char\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{\"char\": \"abcde\"}|]\n          `shouldRespondWith` [json|[{ \"char\": \"abcde\" }]|]\n            { matchStatus  = 201 }\n\n    context \"POST with ?columns parameter\" $ do\n      it \"ignores json keys not included in ?columns\" $ do\n        request methodPost \"/articles?columns=id,body\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\": 200, \"body\": \"xxx\", \"smth\": \"here\", \"other\": \"stuff\", \"fake_id\": 13} |] `shouldRespondWith`\n          [json|[{\"id\": 200, \"body\": \"xxx\", \"owner\": \"postgrest_test_anonymous\"}]|]\n          { matchStatus  = 201\n          , matchHeaders = [] }\n        request methodPost \"/articles?columns=id,body&select=id,body\" [(\"Prefer\", \"return=representation\")]\n          [json| [\n            {\"id\": 201, \"body\": \"yyy\", \"smth\": \"here\", \"other\": \"stuff\", \"fake_id\": 13},\n            {\"id\": 202, \"body\": \"zzz\", \"garbage\": \"%%$&\", \"kkk\": \"jjj\"},\n            {\"id\": 203, \"body\": \"aaa\", \"hey\": \"ho\"} ]|] `shouldRespondWith`\n          [json|[\n            {\"id\": 201, \"body\": \"yyy\"},\n            {\"id\": 202, \"body\": \"zzz\"},\n            {\"id\": 203, \"body\": \"aaa\"} ]|]\n          { matchStatus  = 201\n          , matchHeaders = [] }\n\n      -- TODO parse columns error message needs to be improved\n      it \"disallows blank ?columns\" $\n        post \"/articles?columns=\"\n          [json|[\n            {\"id\": 204, \"body\": \"yyy\"},\n            {\"id\": 205, \"body\": \"zzz\"}]|]\n          `shouldRespondWith`\n          [json|  {\"details\":\"unexpected end of input expecting field name (* or [a..z0..9_$])\",\"message\":\"\\\"failed to parse columns parameter ()\\\" (line 1, column 1)\",\"code\":\"PGRST100\",\"hint\":null} |]\n          { matchStatus  = 400\n          , matchHeaders = []\n          }\n\n      it \"disallows ?columns which don't exist\" $\n        post \"/articles?columns=helicopter\"\n          [json|[\n            {\"id\": 204, \"body\": \"yyy\"},\n            {\"id\": 205, \"body\": \"zzz\"}]|]\n          `shouldRespondWith`\n          [json|{\"code\":\"PGRST204\",\"details\":null,\"hint\":null,\"message\":\"Could not find the 'helicopter' column of 'articles' in the schema cache\"} |]\n          { matchStatus  = 400\n          , matchHeaders = []\n          }\n\n      it \"returns missing table error even if also has invalid ?columns\" $\n        post \"/garlic?columns=helicopter\"\n          [json|[\n            {\"id\": 204, \"body\": \"yyy\"},\n            {\"id\": 205, \"body\": \"zzz\"}]|]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.garlic' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = []\n          }\n\n      it \"disallows array elements that are not json objects\" $\n        post \"/articles?columns=id,body\"\n          [json|[\n            {\"id\": 204, \"body\": \"yyy\"},\n            333,\n            \"asdf\",\n            {\"id\": 205, \"body\": \"zzz\"}]|] `shouldRespondWith` 400\n\n      context \"apply defaults on missing values\" $ do\n        it \"inserts table default values(field-with_sep) when json keys are undefined\" $\n          request methodPost \"/complex_items?columns=id,name,field-with_sep,arr_data\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json|[\n                {\"id\": 4, \"name\": \"Vier\"},\n                {\"id\": 5, \"name\": \"Funf\", \"arr_data\": null},\n                {\"id\": 6, \"name\": \"Sechs\", \"field-with_sep\": 6, \"arr_data\": \"{1,2,3}\"}\n              ]|]\n            `shouldRespondWith`\n              [json|[\n                {\"id\": 4, \"name\": \"Vier\", \"field-with_sep\": 1, \"settings\":null,\"arr_data\":null},\n                {\"id\": 5, \"name\": \"Funf\", \"field-with_sep\": 1, \"settings\":null,\"arr_data\":null},\n                {\"id\": 6, \"name\": \"Sechs\", \"field-with_sep\": 6, \"settings\":null,\"arr_data\":[1,2,3]}\n              ]|]\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n        it \"inserts view default values(field-with_sep) when json keys are undefined\" $\n          request methodPost \"/complex_items_view?columns=id,name\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json|[\n                {\"id\": 7, \"name\": \"Sieben\"},\n                {\"id\": 8}\n              ]|]\n            `shouldRespondWith`\n              [json|[\n                {\"id\": 7, \"name\": \"Sieben\", \"field-with_sep\": 1, \"settings\":null,\"arr_data\":null},\n                {\"id\": 8, \"name\": \"Default\", \"field-with_sep\": 1, \"settings\":null,\"arr_data\":null}\n              ]|]\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n        it \"doesn't insert json duplicate keys(since it uses jsonb)\" $\n          request methodPost \"/tbl_w_json?columns=id,data\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json| { \"data\": { \"a\": 1, \"a\": 2 }, \"id\": 3 } |]\n            `shouldRespondWith`\n              [json| [ { \"data\": { \"a\": 2 }, \"id\": 3 } ] |]\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n        it \"inserts a default on a generated by default as identity column\" $\n          request methodPost \"/channels?columns=id,data,slug&select=data,slug\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json| { \"slug\": \"foo\" } |]\n            `shouldRespondWith`\n              [json| [{\"data\":{\"foo\": \"bar\"},\"slug\":\"foo\"}] |] -- id 1 was inserted here, we don't get it for idempotence in the tests\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n        it \"fails with a good error message on generated always columns\" $\n          request methodPost \"/foo?columns=a,b\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json| [\n                {\"a\": \"val\"},\n                {\"a\": \"val\", \"b\": \"val\"}\n              ]|]\n            `shouldRespondWith`\n              (if actualPgVersion < pgVersion140\n                then [json| {\n                  \"code\": \"42601\",\n                  \"details\": \"Column \\\"b\\\" is a generated column.\",\n                  \"hint\": null,\n                  \"message\": \"cannot insert into column \\\"b\\\"\"\n                }|]\n                else [json| {\n                  \"code\": \"428C9\",\n                  \"details\": \"Column \\\"b\\\" is a generated column.\",\n                  \"hint\": null,\n                  \"message\": \"cannot insert a non-DEFAULT value into column \\\"b\\\"\"\n                }|])\n              { matchStatus  = 400 }\n\n        it \"inserts a default on a DOMAIN with default\" $\n          request methodPost \"/evil_friends?columns=id,name\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json| { \"name\": \"Lu\" } |]\n            `shouldRespondWith`\n              [json| [{\"id\": 666, \"name\": \"Lu\"}] |]\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n        it \"inserts a COLUMN default before a DOMAIN default with missing=default\" $\n          request methodPost \"/evil_friends_with_column_default?columns=id,name\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n              [json| { \"name\": \"Demon\" } |]\n            `shouldRespondWith`\n              [json| [{\"id\": 420, \"name\": \"Demon\"}] |]\n              { matchStatus  = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n              }\n\n    it \"inserts json that has duplicate keys\" $ do\n      request methodPost \"/tbl_w_json\" [(\"Prefer\", \"return=representation\")]\n          [json| { \"data\": { \"a\": 1, \"a\": 2 }, \"id\": 3 } |]\n        `shouldRespondWith`\n          [json| [ { \"data\": { \"a\": 1, \"a\": 2 }, \"id\": 3 } ] |]\n          { matchStatus  = 201\n          }\n      request methodPost \"/tbl_w_json?columns=id,data\" [(\"Prefer\", \"return=representation\")]\n          [json| { \"data\": { \"a\": 1, \"a\": 2 }, \"id\": 3 } |]\n        `shouldRespondWith`\n          [json| [ { \"data\": { \"a\": 1, \"a\": 2 }, \"id\": 3 } ] |]\n          { matchStatus  = 201\n          }\n\n    context \"with unicode values\" $ do\n      it \"succeeds and returns full representation\" $\n        request methodPost \"/simple_pk2?select=extra,k\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { \"k\":\"棋圍\", \"extra\":\"￥\" } |]\n        `shouldRespondWith`\n          [json|[ { \"k\":\"棋圍\", \"extra\":\"￥\" } ]|]\n          { matchStatus = 201 }\n\n      it \"succeeds and returns usable location header\" $ do\n        p <- request methodPost \"/simple_pk2?select=extra,k\"\n            [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=headers-only\")]\n            [json| { \"k\":\"圍棋\", \"extra\":\"￥\" } |]\n        pure p `shouldRespondWith`\n          \"\"\n          { matchStatus = 201 }\n\n        Just location <- pure $ lookup hLocation $ simpleHeaders p\n        get location\n          `shouldRespondWith`\n            [json|[ { \"k\":\"圍棋\", \"extra\":\"￥\" } ]|]\n\n        request methodDelete location\n            [(\"Prefer\", \"tx=commit\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength ]\n            }\n\n  describe \"CSV insert\" $ do\n    context \"disparate csv types\" $\n      it \"succeeds with multipart response\" $ do\n        pendingWith \"Decide on what to do with CSV insert\"\n        let inserted = [str|integer,double,varchar,boolean,date,money,enum\n            |13,3.14159,testing!,false,1900-01-01,$3.99,foo\n            |12,0.1,a string,true,1929-10-01,12,bar\n            |]\n        request methodPost \"/menagerie\" [(\"Content-Type\", \"text/csv\"), (\"Accept\", \"text/csv\"), (\"Prefer\", \"return=representation\")] inserted\n           `shouldRespondWith` ResponseMatcher\n           { matchStatus  = 201\n           , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n           , matchBody = bodyEquals inserted\n           }\n\n    context \"requesting full representation\" $ do\n      it \"returns full details of inserted record\" $\n        request methodPost \"/no_pk\"\n                     [(\"Content-Type\", \"text/csv\"), (\"Accept\", \"text/csv\"),  (\"Prefer\", \"return=representation\")]\n                     \"a,b\\nbar,baz\"\n          `shouldRespondWith` \"a,b\\nbar,baz\"\n          { matchStatus  = 201\n          , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n          }\n\n      it \"can post nulls\" $\n        request methodPost \"/no_pk\"\n                     [(\"Content-Type\", \"text/csv\"), (\"Accept\", \"text/csv\"), (\"Prefer\", \"return=representation\")]\n                     \"a,b\\nNULL,foo\"\n          `shouldRespondWith` \"a,b\\n,foo\"\n          { matchStatus  = 201\n          , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n          }\n\n      it \"only returns the requested column header with its associated data\" $\n        request methodPost \"/projects?select=id\"\n                     [(\"Content-Type\", \"text/csv\"), (\"Accept\", \"text/csv\"), (\"Prefer\", \"return=representation\")]\n                     \"id,name,client_id\\n8,Xenix,1\\n9,Windows NT,1\"\n          `shouldRespondWith` \"id\\n8\\n9\"\n          { matchStatus  = 201\n          , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\",\n                            \"Content-Range\" <:> \"*/*\"]\n          }\n\n    context \"with wrong number of columns\" $\n      it \"fails for too few\" $\n        request methodPost \"/no_pk\" [(\"Content-Type\", \"text/csv\")] \"a,b\\nfoo,bar\\nbaz\"\n        `shouldRespondWith`\n        [json|{\"message\":\"All lines must have same number of fields\",\"code\":\"PGRST102\",\"details\":null,\"hint\":null}|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n  describe \"Row level permission\" $\n    it \"set user_id when inserting rows\" $ do\n      request methodPost \"/authors_only\"\n          [ authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0\", (\"Prefer\", \"return=representation\") ]\n          [json| { \"secret\": \"nyancat\" } |]\n        `shouldRespondWith`\n          [json|[{\"owner\":\"jdoe\",\"secret\":\"nyancat\"}]|]\n          { matchStatus  = 201 }\n\n      request methodPost \"/authors_only\"\n          -- jwt token for jroe\n          [ authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8\", (\"Prefer\", \"return=representation\") ]\n          [json| { \"secret\": \"lolcat\", \"owner\": \"hacker\" } |]\n        `shouldRespondWith`\n          [json|[{\"owner\":\"jroe\",\"secret\":\"lolcat\"}]|]\n          { matchStatus  = 201 }\n\n  context \"tables with self reference foreign keys\" $ do\n    it \"embeds parent after insert\" $\n      request methodPost \"/web_content?select=id,name,parent_content:p_web_id(name)\"\n              [(\"Prefer\", \"return=representation\")]\n        [json|{\"id\":6, \"name\":\"wot\", \"p_web_id\":4}|]\n        `shouldRespondWith`\n        [json|[{\"id\":6,\"name\":\"wot\",\"parent_content\":{\"name\":\"wut\"}}]|]\n        { matchStatus  = 201\n        , matchHeaders = [ matchContentTypeJson , matchHeaderAbsent hLocation ]\n        }\n\n  context \"table with limited privileges\" $ do\n    it \"succeeds inserting if correct select is applied\" $\n      request methodPost \"/limited_article_stars?select=article_id,user_id\" [(\"Prefer\", \"return=representation\")]\n        [json| {\"article_id\": 2, \"user_id\": 1} |] `shouldRespondWith` [json|[{\"article_id\":2,\"user_id\":1}]|]\n        { matchStatus  = 201\n        , matchHeaders = []\n        }\n\n    it \"fails inserting if more columns are selected\" $\n      request methodPost \"/limited_article_stars?select=article_id,user_id,created_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"article_id\": 2, \"user_id\": 2} |] `shouldRespondWith`\n      [json|{\"hint\":null,\"details\":null,\"code\":\"42501\",\"message\":\"permission denied for view limited_article_stars\"}|]\n        { matchStatus  = 401\n        , matchHeaders = []\n        }\n\n    it \"fails inserting if select is not specified\" $\n      request methodPost \"/limited_article_stars\" [(\"Prefer\", \"return=representation\")]\n        [json| {\"article_id\": 3, \"user_id\": 1} |] `shouldRespondWith`\n      [json|{\"hint\":null,\"details\":null,\"code\":\"42501\",\"message\":\"permission denied for view limited_article_stars\"}|]\n        { matchStatus  = 401\n        , matchHeaders = []\n        }\n\n    it \"can insert in a table with no select and return=minimal\" $ do\n      request methodPost \"/insertonly\"\n          [(\"Prefer\", \"return=minimal\")]\n          [json| { \"v\":\"some value\" } |]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 201\n          , matchHeaders = [matchHeaderAbsent hContentType\n                           , \"Content-Length\" <:> \"0\"\n                           , \"Preference-Applied\" <:> \"return=minimal\"]\n          }\n\n  describe \"Inserting into VIEWs\" $ do\n    context \"requesting no representation\" $ do\n      it \"succeeds with 201\" $\n        post \"/compound_pk_view\"\n            [json|{\"k1\":1,\"k2\":\"test\",\"extra\":2}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hLocation\n                             , \"Content-Length\" <:> \"0\"]\n            }\n\n      it \"returns a location header with pks from both tables\" $\n        request methodPost \"/with_multiple_pks\" [(\"Prefer\", \"return=headers-only\")]\n            [json|{\"pk1\":1,\"pk2\":2}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/with_multiple_pks?pk1=eq.1&pk2=eq.2\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n    context \"requesting header only representation\" $ do\n      it \"returns a location header with a composite PK col\" $\n        request methodPost \"/compound_pk_view\" [(\"Prefer\", \"return=headers-only\")]\n            [json|{\"k1\":1,\"k2\":\"test\",\"extra\":2}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/compound_pk_view?k1=eq.1&k2=eq.test\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n      it \"should not throw and return location header when a PK is null\" $\n        request methodPost \"/test_null_pk_competitors_sponsors\" [(\"Prefer\", \"return=headers-only\")]\n            [json|{\"id\":1}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n\n  describe \"Data representations\" $ do\n    context \"on regular table\" $ do\n      it \"parses values in POST body\" $\n        -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a\n        -- an \"invalid input syntax for type integer:\" error.\n        request methodPost \"/datarep_todos\" [(\"Prefer\", \"return=headers-only\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\"} |]\n          `shouldRespondWith`\n          \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/datarep_todos?id=eq.5\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n      it \"parses values in POST body and formats individually selected values in return=representation\" $\n        request methodPost \"/datarep_todos?select=id,label_color\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5, \"label_color\": \"#001100\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"parses values in POST body and formats values in return=representation\" $\n        request methodPost \"/datarep_todos\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\", \"icon_image\": \"3q2+7w\", \"created_at\":-15, \"budget\": \"-100000000000000.13\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5,\"name\": \"party\", \"label_color\": \"#001100\", \"due_at\":\"2018-01-03T11:00:00Z\", \"icon_image\": \"3q2+7w==\", \"created_at\":-15, \"budget\": \"-100000000000000.13\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n    context \"with ?columns parameter\" $ do\n      it \"ignores json keys not included in ?columns; parses only the ones specified\" $\n        request methodPost \"/datarep_todos?columns=id,label_color&select=id,name,label_color,due_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"invalid but should be ignored\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5, \"name\":null, \"label_color\": \"#001100\", \"due_at\": \"2018-01-01T00:00:00Z\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"fails without parsing anything if at least one specified column doesn't exist\" $\n        request methodPost \"/datarep_todos?columns=id,label_color,helicopters&select=id,name,label_color,due_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"due_at\": \"2019-01-03T11:00:00+00\", \"smth\": \"here\", \"label_color\": \"invalid\", \"fake_id\": 13} |]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST204\",\"details\":null,\"hint\":null,\"message\":\"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache\"} |]\n            { matchStatus  = 400\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n            }\n\n    context \"on updatable view\" $ do\n      it \"parses values in POST body\" $\n        -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a\n        -- an \"invalid input syntax for type integer:\" error.\n        request methodPost \"/datarep_todos_computed\" [(\"Prefer\", \"return=headers-only\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\"} |]\n          `shouldRespondWith`\n          \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/datarep_todos_computed?id=eq.5\"\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Content-Length\" <:> \"0\"\n                             , \"Preference-Applied\" <:> \"return=headers-only\"]\n            }\n\n      it \"parses values in POST body and formats individually selected values in return=representation\" $\n        request methodPost \"/datarep_todos_computed?select=id,label_color\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5, \"label_color\": \"#001100\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"parses values in POST body and formats values in return=representation\" $\n        request methodPost \"/datarep_todos_computed\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"2018-01-03T11:00:00+00\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5,\"name\": \"party\", \"label_color\": \"#001100\", \"due_at\":\"2018-01-03T11:00:00Z\", \"dark_color\":\"#000880\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n    context \"on updatable views with ?columns parameter\" $ do\n      it \"ignores json keys not included in ?columns; parses only the ones specified\" $\n        request methodPost \"/datarep_todos_computed?columns=id,label_color&select=id,name,label_color,due_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"id\":5, \"name\": \"party\", \"label_color\": \"#001100\", \"due_at\": \"invalid but should be ignored\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":5, \"name\":null, \"label_color\": \"#001100\", \"due_at\": \"2018-01-01T00:00:00Z\"}] |]\n            { matchStatus  = 201\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\",\n                              \"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"fails without parsing anything if at least one specified column doesn't exist\" $\n        request methodPost \"/datarep_todos_computed?columns=id,label_color,helicopters&select=id,name,label_color,due_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"due_at\": \"2019-01-03T11:00:00+00\", \"smth\": \"here\", \"label_color\": \"invalid\", \"fake_id\": 13} |]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST204\",\"details\":null,\"hint\":null,\"message\":\"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache\"} |]\n            { matchStatus  = 400\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n            }\n"
  },
  {
    "path": "test/spec/Feature/Query/JsonOperatorSpec.hs",
    "content": "module Feature.Query.JsonOperatorSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"json and jsonb operators\" $ do\n  context \"Shaping response with select parameter\" $ do\n    it \"obtains a json subfield one level with casting\" $\n      get \"/complex_items?id=eq.1&select=settings->>foo::json\" `shouldRespondWith`\n        [json| [{\"foo\":{\"int\":1,\"bar\":\"baz\"}}] |] -- the value of foo here is of type \"text\"\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"renames json subfield one level with casting\" $\n      get \"/complex_items?id=eq.1&select=myFoo:settings->>foo::json\" `shouldRespondWith`\n        [json| [{\"myFoo\":{\"int\":1,\"bar\":\"baz\"}}] |] -- the value of foo here is of type \"text\"\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"fails on bad casting (data of the wrong format)\" $\n      get \"/complex_items?select=settings->foo->>bar::integer\"\n        `shouldRespondWith`\n        [json| {\"hint\":null,\"details\":null,\"code\":\"22P02\",\"message\":\"invalid input syntax for type integer: \\\"baz\\\"\"} |]\n        { matchStatus  = 400 , matchHeaders = [] }\n\n    it \"obtains a json subfield two levels (string)\" $\n      get \"/complex_items?id=eq.1&select=settings->foo->>bar\" `shouldRespondWith`\n        [json| [{\"bar\":\"baz\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"renames json subfield two levels (string)\" $\n      get \"/complex_items?id=eq.1&select=myBar:settings->foo->>bar\" `shouldRespondWith`\n        [json| [{\"myBar\":\"baz\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"obtains a json subfield two levels with casting (int)\" $\n      get \"/complex_items?id=eq.1&select=settings->foo->>int::integer\" `shouldRespondWith`\n        [json| [{\"int\":1}] |] -- the value in the db is an int, but here we expect a string for now\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"renames json subfield two levels with casting (int)\" $\n      get \"/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer\" `shouldRespondWith`\n        [json| [{\"myInt\":1}] |] -- the value in the db is an int, but here we expect a string for now\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"accepts non reserved special characters in the key's name\" $\n      get \"/json_arr?id=eq.10&select=data->!@#$%^%26*_d->>!@#$%^%26*_e::integer\" `shouldRespondWith`\n        [json| [{\"!@#$%^&*_e\":3}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"fails when there is a reserved special character in the key's name\" $\n      get \"/json_arr?id=eq.10&select=data->(!@#$%^%26*_d->>!@#$%^%26*_e::integer\" `shouldRespondWith`\n        [json| {\n          \"code\":\"PGRST100\",\n          \"details\":\"unexpected \\\"(\\\" expecting \\\"-\\\", digit or any non reserved character different from: .,>()\",\n          \"hint\":null,\n          \"message\":\"\\\"failed to parse select parameter (data->(!@#$%^&*_d->>!@#$%^&*_e::integer)\\\" (line 1, column 7)\"}\n        |]\n        { matchStatus  = 400 , matchHeaders = [] }\n\n    -- TODO the status code for the error is 404, this is because 42883 represents undefined function\n    -- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate\n    it \"fails when a double arrow ->> is followed with a single arrow ->\" $ do\n      get \"/json_arr?select=data->>c->1\"\n        `shouldRespondWith`\n        [json|\n          {\"hint\":\"No operator matches the given name and argument types. You might need to add explicit type casts.\",\n           \"details\":null,\"code\":\"42883\",\"message\":\"operator does not exist: text -> integer\"} |]\n        { matchStatus  = 404 , matchHeaders = [] }\n      get \"/json_arr?select=data->>c->b\"\n        `shouldRespondWith`\n        [json|\n          {\"hint\":\"No operator matches the given name and argument types. You might need to add explicit type casts.\",\n           \"details\":null,\"code\":\"42883\",\"message\":\"operator does not exist: text -> unknown\"} |]\n        { matchStatus  = 404 , matchHeaders = [] }\n\n    context \"with array index\" $ do\n      it \"can get array of ints and alias/cast it\" $ do\n        get \"/json_arr?select=data->>0::int&id=in.(1,2)\" `shouldRespondWith`\n          [json| [{\"data\":1}, {\"data\":4}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=idx0:data->>0::int,idx1:data->>1::int&id=in.(1,2)\" `shouldRespondWith`\n          [json| [{\"idx0\":1,\"idx1\":2}, {\"idx0\":4,\"idx1\":5}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can get nested array of ints\" $ do\n        get \"/json_arr?select=data->0->>1::int&id=in.(3,4)\" `shouldRespondWith`\n          [json| [{\"data\":8}, {\"data\":7}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=data->0->0->>1::int&id=in.(3,4)\" `shouldRespondWith`\n          [json| [{\"data\":null}, {\"data\":6}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can get array of objects\" $ do\n        get \"/json_arr?select=data->0->>a&id=in.(5,6)\" `shouldRespondWith`\n          [json|[{\"a\":\"A\"}, {\"a\":\"[1,2,3]\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=data->0->a->>2&id=in.(5,6)\" `shouldRespondWith`\n          [json| [{\"a\":null}, {\"a\":\"3\"}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can get array in object keys\" $ do\n        get \"/json_arr?select=data->c->>0::json&id=in.(7,8)\" `shouldRespondWith`\n          [json| [{\"c\":1}, {\"c\":{\"d\": [4,5,6,7,8]}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=data->c->0->d->>4::int&id=in.(7,8)\" `shouldRespondWith`\n          [json| [{\"d\":null}, {\"d\":8}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"only treats well formed numbers as indexes\" $\n        get \"/json_arr?select=data->0->0xy1->1->23-xy-45->1->xy-6->>0::int&id=eq.9\" `shouldRespondWith`\n          [json| [{\"xy-6\":3}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"finishing json path with single arrow ->\" $ do\n      it \"works when finishing with a key\" $ do\n        get \"/json_arr?select=data->c&id=in.(7,8)\" `shouldRespondWith`\n          [json| [{\"c\":[1,2,3]}, {\"c\":[{\"d\": [4,5,6,7,8]}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=data->0->a&id=in.(5,6)\" `shouldRespondWith`\n          [json| [{\"a\":\"A\"}, {\"a\":[1,2,3]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when finishing with an index\" $ do\n        get \"/json_arr?select=data->0->a&id=in.(5,6)\" `shouldRespondWith`\n          [json| [{\"a\":\"A\"}, {\"a\":[1,2,3]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/json_arr?select=data->c->0->d&id=eq.8\" `shouldRespondWith`\n          [json| [{\"d\":[4,5,6,7,8]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n    it \"obtains a composite type field\" $ do\n      get \"/fav_numbers?select=num->i\"\n        `shouldRespondWith`\n          [json| [{\"i\":0.5},{\"i\":0.6}] |]\n      get \"/fav_numbers?select=num->>i\"\n        `shouldRespondWith`\n          [json| [{\"i\":\"0.5\"},{\"i\":\"0.6\"}] |]\n\n    it \"obtains an array item\" $ do\n      get \"/arrays?select=a:numbers->0,b:numbers->1,c:numbers_mult->0->0,d:numbers_mult->1->2\"\n        `shouldRespondWith`\n          [json| [{\"a\":1,\"b\":2,\"c\":1,\"d\":6},{\"a\":11,\"b\":12,\"c\":11,\"d\":16}] |]\n      get \"/arrays?select=a:numbers->>0,b:numbers->>1,c:numbers_mult->0->>0,d:numbers_mult->1->>2\"\n        `shouldRespondWith`\n          [json| [{\"a\":\"1\",\"b\":\"2\",\"c\":\"1\",\"d\":\"6\"},{\"a\":\"11\",\"b\":\"12\",\"c\":\"11\",\"d\":\"16\"}] |]\n\n  context \"filtering response\" $ do\n    it \"can filter by properties inside json column\" $ do\n      get \"/json_table?data->foo->>bar=eq.baz\" `shouldRespondWith`\n        [json| [{\"data\": {\"id\": 1, \"foo\": {\"bar\": \"baz\"}}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_table?data->foo->>bar=eq.fake\" `shouldRespondWith`\n        [json| [] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter by properties inside json column using not\" $\n      get \"/json_table?data->foo->>bar=not.eq.baz\" `shouldRespondWith`\n        [json| [] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter by properties inside json column using ->>\" $\n      get \"/json_table?data->>id=eq.1\" `shouldRespondWith`\n        [json| [{\"data\": {\"id\": 1, \"foo\": {\"bar\": \"baz\"}}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can be filtered with and/or\" $\n      get \"/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id\" `shouldRespondWith`\n        [json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter when the key's name has non reserved special characters\" $\n      get \"/json_arr?select=data->!@#$%^%26*_d&data->!@#$%^%26*_d->>!@#$%^%26*_e=eq.3\" `shouldRespondWith`\n        [json| [{\"!@#$%^&*_d\": {\"!@#$%^&*_e\": 3}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter by array indexes\" $ do\n      get \"/json_arr?select=data&data->>0=eq.1\" `shouldRespondWith`\n        [json| [{\"data\":[1, 2, 3]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->1->>2=eq.13\" `shouldRespondWith`\n        [json| [{\"data\":[[9, 8, 7], [11, 12, 13]]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->1->>b=eq.B\" `shouldRespondWith`\n        [json| [{\"data\":[{\"a\": \"A\"}, {\"b\": \"B\"}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->1->b->>1=eq.5\" `shouldRespondWith`\n        [json| [{\"data\":[{\"a\": [1,2,3]}, {\"b\": [4,5]}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter jsonb\" $ do\n      get \"/jsonb_test?data=eq.{\\\"e\\\":1}\" `shouldRespondWith`\n        [json| [{\"id\":4,\"data\":{\"e\": 1}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/jsonb_test?data->a=eq.{\\\"b\\\":2}\" `shouldRespondWith`\n        [json| [{\"id\":1,\"data\":{\"a\": {\"b\": 2}}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/jsonb_test?data->c=eq.[1,2,3]\" `shouldRespondWith`\n        [json| [{\"id\":2,\"data\":{\"c\": [1, 2, 3]}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/jsonb_test?data->0=eq.{\\\"d\\\":\\\"test\\\"}\" `shouldRespondWith`\n        [json| [{\"id\":3,\"data\":[{\"d\": \"test\"}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter composite type field\" $\n      get \"/fav_numbers?num->>i=gt.0.5\"\n        `shouldRespondWith`\n          [json| [{\"num\":{\"r\":0.6,\"i\":0.6},\"person\":\"B\"}] |]\n\n    it \"can filter array item\" $ do\n      get \"/arrays?select=id&numbers->0=eq.1\"\n        `shouldRespondWith`\n          [json| [{\"id\":0}] |]\n      get \"/arrays?select=id&numbers->>0=eq.11\"\n        `shouldRespondWith`\n          [json| [{\"id\":1}] |]\n      get \"/arrays?select=id&numbers_mult->1->1=eq.5\"\n        `shouldRespondWith`\n          [json| [{\"id\":0}] |]\n      get \"/arrays?select=id&numbers_mult->2->>2=eq.19\"\n        `shouldRespondWith`\n          [json| [{\"id\":1}] |]\n\n  context \"ordering response\" $ do\n    it \"orders by a json column property asc\" $\n      get \"/json_table?order=data->>id.asc\" `shouldRespondWith`\n        [json| [{\"data\": {\"id\": 0}}, {\"data\": {\"id\": 1, \"foo\": {\"bar\": \"baz\"}}}, {\"data\": {\"id\": 3}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"orders by a json column with two level property nulls first\" $\n      get \"/json_table?order=data->foo->>bar.nullsfirst\" `shouldRespondWith`\n        [json| [{\"data\": {\"id\": 3}}, {\"data\": {\"id\": 0}}, {\"data\": {\"id\": 1, \"foo\": {\"bar\": \"baz\"}}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"orders by composite type field\" $ do\n      get \"/fav_numbers?order=num->i.asc\"\n        `shouldRespondWith`\n          [json| [{\"num\":{\"r\":0.5,\"i\":0.5},\"person\":\"A\"}, {\"num\":{\"r\":0.6,\"i\":0.6},\"person\":\"B\"}] |]\n      get \"/fav_numbers?order=num->>i.desc\"\n        `shouldRespondWith`\n          [json| [{\"num\":{\"r\":0.6,\"i\":0.6},\"person\":\"B\"}, {\"num\":{\"r\":0.5,\"i\":0.5},\"person\":\"A\"}] |]\n\n    it \"orders by array item\" $ do\n      get \"/arrays?select=id&order=numbers->0.desc\"\n        `shouldRespondWith`\n          [json| [{\"id\":1},{\"id\":0}] |]\n      get \"/arrays?select=id&order=numbers->1.asc\"\n        `shouldRespondWith`\n          [json| [{\"id\":0},{\"id\":1}] |]\n      get \"/arrays?select=id&order=numbers_mult->0->0.desc\"\n        `shouldRespondWith`\n          [json| [{\"id\":1},{\"id\":0}] |]\n      get \"/arrays?select=id&order=numbers_mult->2->2.asc\"\n        `shouldRespondWith`\n          [json| [{\"id\":0},{\"id\":1}] |]\n\n  context \"Patching record, in a nonempty table\" $\n    it \"can set a json column to escaped value\" $ do\n      request methodPatch \"/json_table?data->>id=eq.3\"\n          [(\"Prefer\", \"return=representation\")]\n          [json| { \"data\": { \"id\":\" \\\"escaped\" } } |]\n        `shouldRespondWith`\n          [json| [{ \"data\": { \"id\":\" \\\"escaped\" } }] |]\n\n  context \"json array negative index\" $ do\n    it \"can select with negative indexes\" $ do\n      get \"/json_arr?select=data->>-1::int&id=in.(1,2)\" `shouldRespondWith`\n        [json| [{\"data\":3}, {\"data\":6}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data->0->>-2::int&id=in.(3,4)\" `shouldRespondWith`\n        [json| [{\"data\":8}, {\"data\":7}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data->-2->>a&id=in.(5,6)\" `shouldRespondWith`\n        [json| [{\"a\":\"A\"}, {\"a\":\"[1,2,3]\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"can filter with negative indexes\" $ do\n      get \"/json_arr?select=data&data->>-3=eq.1\" `shouldRespondWith`\n        [json| [{\"data\":[1, 2, 3]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->-1->>-3=eq.11\" `shouldRespondWith`\n        [json| [{\"data\":[[9, 8, 7], [11, 12, 13]]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->-1->>b=eq.B\" `shouldRespondWith`\n        [json| [{\"data\":[{\"a\": \"A\"}, {\"b\": \"B\"}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/json_arr?select=data&data->-1->b->>-1=eq.5\" `shouldRespondWith`\n        [json| [{\"data\":[{\"a\": [1,2,3]}, {\"b\": [4,5]}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n  it \"gives a meaningful error on bad syntax\" $\n    get \"/json_arr?select=data->>--34\" `shouldRespondWith`\n      [json|\n        {\"details\": \"unexpected \\\"-\\\" expecting digit\",\n         \"message\": \"\\\"failed to parse select parameter (data->>--34)\\\" (line 1, column 9)\",\n         \"code\": \"PGRST100\",\n         \"hint\": null} |]\n      { matchStatus = 400, matchHeaders = [matchContentTypeJson] }\n\n  it \"works when an RPC returns a dynamic TABLE with a composite type\" $\n    get \"/rpc/returns_complex?select=val->r&val->i=gt.0.5&order=val->>i.desc\" `shouldRespondWith`\n      [json|[\n        {\"r\":0.3},\n        {\"r\":0.2}\n      ]|]\n      { matchStatus = 200, matchHeaders = [matchContentTypeJson] }\n"
  },
  {
    "path": "test/spec/Feature/Query/MultipleSchemaSpec.hs",
    "content": "module Feature.Query.MultipleSchemaSpec where\n\nimport Control.Lens    ((^?))\nimport Data.Aeson.Lens\nimport Data.Aeson.QQ\n\nimport Network.HTTP.Types\nimport Network.Wai        (Application)\nimport Network.Wai.Test   (SResponse (simpleHeaders), simpleBody)\n\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"multiple schemas in single instance\" $ do\n    context \"Reading tables on different schemas\" $ do\n      it \"succeeds in reading table from default schema v1 if no schema is selected via header\" $\n        request methodGet \"/parents\" [] \"\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"name\":\"parent v1-1\"},\n            {\"id\":2,\"name\":\"parent v1-2\"}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v1\"]\n          }\n\n      it \"succeeds in reading table from default schema v1 after explicitly passing it in the header\" $\n        request methodGet \"/parents\" [(\"Accept-Profile\", \"v1\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"id\":1,\"name\":\"parent v1-1\"},\n            {\"id\":2,\"name\":\"parent v1-2\"}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v1\"]\n          }\n\n      it \"succeeds in reading table from schema v2\" $\n        request methodGet \"/parents\" [(\"Accept-Profile\", \"v2\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"id\":3,\"name\":\"parent v2-3\"},\n            {\"id\":4,\"name\":\"parent v2-4\"}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds in reading another_table from schema v2\" $\n        request methodGet \"/another_table\" [(\"Accept-Profile\", \"v2\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"id\":5,\"another_value\":\"value 5\"},\n            {\"id\":6,\"another_value\":\"value 6\"}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"doesn't find another_table in schema v1\" $\n        request methodGet \"/another_table\"\n          [(\"Accept-Profile\", \"v1\")] \"\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'v1.another_table' in the schema cache\"} |]\n          { matchStatus = 404\n          , matchHeaders = []\n          }\n\n      it \"fails trying to read table from unknown schema\" $\n        request methodGet \"/parents\" [(\"Accept-Profile\", \"unknown\")] \"\" `shouldRespondWith`\n          [json|{\"message\":\"Invalid schema: unknown\",\"code\":\"PGRST106\",\"details\":null,\"hint\":\"Only the following schemas are exposed: v1, v2, SPECIAL \\\"@/\\\\#~_-\"}|]\n          {\n            matchStatus = 406\n          }\n\n      it \"succeeds in reading a table from a schema with uppercase and special characters in its name\" $\n        request methodGet \"/names?select=id,name\" [(\"Accept-Profile\", \"SPECIAL \\\"@/\\\\#~_-\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"id\": 1, \"name\":\"John\"},\n            {\"id\": 2, \"name\":\"Mary\"},\n            {\"id\": 3, \"name\":\"José\"}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"SPECIAL \\\"@/\\\\#~_-\"]\n          }\n\n      it \"succeeds in embedding with FK when the schema name has special characters\" $\n        request methodGet \"/names?select=name,languages(name)&id=in.(1,3)\" [(\"Accept-Profile\", \"SPECIAL \\\"@/\\\\#~_-\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"name\": \"John\", languages: {\"name\": \"English\"}},\n            {\"name\": \"José\", languages: {\"name\": \"Spanish\"}}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"SPECIAL \\\"@/\\\\#~_-\"]\n          }\n\n      it \"succeeds in embedding with computed relationships when the schema name has special characters\" $\n        request methodGet \"/names?select=name,computed_languages(name)&id=in.(1,3)\" [(\"Accept-Profile\", \"SPECIAL \\\"@/\\\\#~_-\")] \"\" `shouldRespondWith`\n          [json|[\n            {\"name\": \"John\", computed_languages: {\"name\": \"English\"}},\n            {\"name\": \"José\", computed_languages: {\"name\": \"Spanish\"}}\n          ]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"SPECIAL \\\"@/\\\\#~_-\"]\n          }\n\n    context \"Inserting tables on different schemas\" $ do\n      it \"succeeds inserting on default schema and returning it\" $\n        request methodPost \"/children\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{\"id\": 0, \"name\": \"child v1-1\", \"parent_id\": 1}|]\n          `shouldRespondWith`\n            [json|[{\"id\": 0, \"name\": \"child v1-1\", \"parent_id\": 1}]|]\n            {\n              matchStatus = 201\n            , matchHeaders = [\"Content-Profile\" <:> \"v1\"]\n            }\n\n      it \"succeeds inserting on the v1 schema and returning its parent\" $\n        request methodPost \"/children?select=id,parent(*)\"\n            [(\"Prefer\", \"return=representation\"), (\"Content-Profile\", \"v1\")]\n            [json|{\"id\": 0, \"name\": \"child v1-2\", \"parent_id\": 2}|]\n          `shouldRespondWith`\n            [json|[{\"id\": 0, \"parent\": {\"id\": 2, \"name\": \"parent v1-2\"}}]|]\n            {\n              matchStatus = 201\n            , matchHeaders = [\"Content-Profile\" <:> \"v1\"]\n            }\n\n      it \"succeeds inserting on the v2 schema and returning its parent\" $\n        request methodPost \"/children?select=id,parent(*)\"\n            [(\"Prefer\", \"return=representation\"), (\"Content-Profile\", \"v2\")]\n            [json|{\"id\": 0, \"name\": \"child v2-3\", \"parent_id\": 3}|]\n          `shouldRespondWith`\n            [json|[{\"id\": 0, \"parent\": {\"id\": 3, \"name\": \"parent v2-3\"}}]|]\n            {\n              matchStatus = 201\n            , matchHeaders = [\"Content-Profile\" <:> \"v2\"]\n            }\n\n      it \"fails when inserting on an unknown schema\" $\n        request methodPost \"/children\" [(\"Content-Profile\", \"unknown\")]\n          [json|{\"name\": \"child 4\", \"parent_id\": 4}|]\n          `shouldRespondWith`\n          [json|{\"message\":\"Invalid schema: unknown\",\"code\":\"PGRST106\",\"details\":null,\"hint\":\"Only the following schemas are exposed: v1, v2, SPECIAL \\\"@/\\\\#~_-\"}|]\n          {\n            matchStatus = 406\n          }\n\n      it \"succeeds in calling handler with a domain on another schema\" $\n        request methodGet \"/another_table\" [(\"Accept-Profile\", \"v2\"), (hAccept, \"text/plain\")] \"\"\n          `shouldRespondWith` \"plain\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"text/plain; charset=utf-8\", \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds in calling handler with a domain on an exposed schema\" $\n        request methodGet \"/another_table\" [(\"Accept-Profile\", \"v2\"), (hAccept, \"text/special\")] \"\"\n          `shouldRespondWith` \"special\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"text/special\", \"Content-Profile\" <:> \"v2\"]\n          }\n\n    context \"calling procs on different schemas\" $ do\n      it \"succeeds in calling the default schema proc\" $\n        request methodGet \"/rpc/get_parents_below?id=6\" [] \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"parent v1-1\"}, {\"id\":2,\"name\":\"parent v1-2\"}]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v1\"]\n          }\n\n      it \"succeeds in calling the v1 schema proc and embedding\" $\n        request methodGet \"/rpc/get_parents_below?id=6&select=id,name,children(id,name)\" [(\"Accept-Profile\", \"v1\")] \"\"\n          `shouldRespondWith`\n          [json| [\n            {\"id\":1,\"name\":\"parent v1-1\",\"children\":[{\"id\":1,\"name\":\"child v1-1\"}]},\n            {\"id\":2,\"name\":\"parent v1-2\",\"children\":[{\"id\":2,\"name\":\"child v1-2\"}]}] |]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v1\"]\n          }\n\n      it \"succeeds in calling the v2 schema proc and embedding\" $\n        request methodGet \"/rpc/get_parents_below?id=6&select=id,name,children(id,name)\" [(\"Accept-Profile\", \"v2\")] \"\"\n          `shouldRespondWith`\n          [json| [\n            {\"id\":3,\"name\":\"parent v2-3\",\"children\":[{\"id\":1,\"name\":\"child v2-3\"}]},\n            {\"id\":4,\"name\":\"parent v2-4\",\"children\":[]}] |]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds in calling the v2 schema proc with POST by using Content-Profile\" $\n        request methodPost \"/rpc/get_parents_below?select=id,name\" [(\"Content-Profile\", \"v2\")]\n          [json|{\"id\": \"6\"}|]\n          `shouldRespondWith`\n          [json| [\n            {\"id\":3,\"name\":\"parent v2-3\"},\n            {\"id\":4,\"name\":\"parent v2-4\"}]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds in calling handler with a domain on another schema\" $\n        request methodGet \"/rpc/get_plain_text\" [(\"Accept-Profile\", \"v2\"), (hAccept, \"text/plain\")] \"\"\n          `shouldRespondWith` \"plain\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"text/plain; charset=utf-8\", \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds in calling handler with a domain on an exposed schema\" $\n        request methodGet \"/rpc/get_special_text\" [(\"Accept-Profile\", \"v2\"), (hAccept, \"text/special\")] \"\"\n          `shouldRespondWith` \"special\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"text/special\", \"Content-Profile\" <:> \"v2\"]\n          }\n\n    context \"Modifying tables on different schemas\" $ do\n      it \"succeeds in patching on the v1 schema and returning its parent\" $\n        request methodPatch \"/children?select=name,parent(name)&id=eq.1\" [(\"Content-Profile\", \"v1\"), (\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"child v1-1 updated\"}|]\n          `shouldRespondWith`\n          [json|[{\"name\":\"child v1-1 updated\", \"parent\": {\"name\": \"parent v1-1\"}}]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v1\"]\n          }\n\n      it \"succeeds in patching on the v2 schema and returning its parent\" $\n        request methodPatch \"/children?select=name,parent(name)&id=eq.1\" [(\"Content-Profile\", \"v2\"), (\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"child v2-1 updated\"}|]\n          `shouldRespondWith`\n          [json|[{\"name\":\"child v2-1 updated\", \"parent\": {\"name\": \"parent v2-3\"}}]|]\n          {\n            matchStatus = 200\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]\n          }\n\n      it \"succeeds on deleting on the v2 schema\" $ do\n        request methodDelete \"/children?id=eq.1\"\n            [(\"Content-Profile\", \"v2\"), (\"Prefer\", \"return=representation\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{\"id\": 1, \"name\": \"child v2-3\", \"parent_id\": 3}]|]\n            { matchHeaders = [\"Content-Profile\" <:> \"v2\"] }\n\n      it \"succeeds on PUT on the v2 schema\" $\n        request methodPut \"/children?id=eq.111\" [(\"Content-Profile\", \"v2\"), (\"Prefer\", \"return=representation\")]\n          [json|[{\"id\": 111, \"name\": \"child v2-111\", \"parent_id\": null}]|]\n          `shouldRespondWith`\n          [json|[{\"id\": 111, \"name\": \"child v2-111\", \"parent_id\": null}]|]\n          { matchStatus  = 201\n          , matchHeaders = [matchContentTypeJson, \"Content-Profile\" <:> \"v2\"]}\n\n    context \"OpenAPI output\" $ do\n      it \"succeeds in reading table definition from default schema v1 if no schema is selected via header\" $ do\n          req <- request methodGet \"/\" [] \"\"\n\n          liftIO $ do\n            simpleHeaders req `shouldSatisfy` matchHeader \"Content-Profile\" \"v1\"\n\n            let def = simpleBody req ^? key \"definitions\" . key \"parents\"\n\n            def `shouldBe` Just\n                [aesonQQ|\n                  {\n                    \"type\" : \"object\",\n                    \"properties\" : {\n                      \"id\" : {\n                        \"description\" : \"Note:\\nThis is a Primary Key.<pk/>\",\n                        \"format\" : \"integer\",\n                        \"type\" : \"integer\"\n                      },\n                      \"name\" : {\n                        \"format\" : \"text\",\n                        \"type\" : \"string\"\n                      }\n                    },\n                    \"required\" : [\n                      \"id\"\n                    ]\n                  }\n                |]\n\n      it \"succeeds in reading table definition from default schema v1 after explicitly passing it in the header\" $ do\n          r <- request methodGet \"/\" [(\"Accept-Profile\", \"v1\")] \"\"\n\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy` matchHeader \"Content-Profile\" \"v1\"\n\n            let def = simpleBody r ^? key \"definitions\" . key \"parents\"\n\n            def `shouldBe` Just\n                [aesonQQ|\n                  {\n                    \"type\" : \"object\",\n                    \"properties\" : {\n                      \"id\" : {\n                        \"description\" : \"Note:\\nThis is a Primary Key.<pk/>\",\n                        \"format\" : \"integer\",\n                        \"type\" : \"integer\"\n                      },\n                      \"name\" : {\n                        \"format\" : \"text\",\n                        \"type\" : \"string\"\n                      }\n                    },\n                    \"required\" : [\n                      \"id\"\n                    ]\n                  }\n                |]\n\n      it \"succeeds in reading table definition from schema v2\" $ do\n          r <- request methodGet \"/\" [(\"Accept-Profile\", \"v2\")] \"\"\n\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy` matchHeader \"Content-Profile\" \"v2\"\n\n            let def = simpleBody r ^? key \"definitions\" . key \"parents\"\n\n            def `shouldBe` Just\n                [aesonQQ|\n                  {\n                    \"type\" : \"object\",\n                    \"properties\" : {\n                      \"id\" : {\n                        \"description\" : \"Note:\\nThis is a Primary Key.<pk/>\",\n                        \"format\" : \"integer\",\n                        \"type\" : \"integer\"\n                      },\n                      \"name\" : {\n                        \"format\" : \"text\",\n                        \"type\" : \"string\"\n                      }\n                    },\n                    \"required\" : [\n                      \"id\"\n                    ]\n                  }\n                |]\n\n      it \"succeeds in reading another_table definition from schema v2\" $ do\n          r <- request methodGet \"/\" [(\"Accept-Profile\", \"v2\")] \"\"\n\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy` matchHeader \"Content-Profile\" \"v2\"\n\n            let def = simpleBody r ^? key \"definitions\" . key \"another_table\"\n\n            def `shouldBe` Just\n                [aesonQQ|\n                  {\n                    \"type\" : \"object\",\n                    \"properties\" : {\n                      \"id\" : {\n                        \"description\" : \"Note:\\nThis is a Primary Key.<pk/>\",\n                        \"format\" : \"integer\",\n                        \"type\" : \"integer\"\n                      },\n                      \"another_value\" : {\n                        \"format\" : \"text\",\n                        \"type\" : \"string\"\n                      }\n                    },\n                    \"required\" : [\n                      \"id\"\n                    ]\n                  }\n                |]\n\n      it \"doesn't find another_table definition in schema v1\" $ do\n        r <- request methodGet \"/\" [(\"Accept-Profile\", \"v1\")] \"\"\n\n        liftIO $ do\n          let def = simpleBody r ^? key \"definitions\" . key \"another_table\"\n          def `shouldBe` Nothing\n\n      it \"fails trying to read definitions from unknown schema\" $\n        request methodGet \"/\" [(\"Accept-Profile\", \"unknown\")] \"\" `shouldRespondWith`\n          [json|{\"message\":\"Invalid schema: unknown\",\"code\":\"PGRST106\",\"details\":null,\"hint\":\"Only the following schemas are exposed: v1, v2, SPECIAL \\\"@/\\\\#~_-\"}|]\n          {\n            matchStatus = 406\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/NullsStripSpec.hs",
    "content": "module Feature.Query.NullsStripSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Stripping null values from JSON response\" $ do\n    let arrayStrip = (\"Accept\", \"application/vnd.pgrst.array+json;nulls=stripped\")\n    let singularStrip = (\"Accept\", \"application/vnd.pgrst.object+json;nulls=stripped\")\n\n    context \"strip nulls from response\" $ do\n      it \"strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped\" $ do\n        request methodGet  \"/organizations?select=*\"\n          [arrayStrip]\n          \"\"\n          `shouldRespondWith`\n          [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}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n        request methodPost  \"/organizations?select=*\"\n          [arrayStrip,(\"Prefer\",\"return=representation\")]\n          [json|{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}|]\n          `shouldRespondWith`\n          [json|[{\"id\":7,\"name\":\"John\",\"manager_id\":6}]|]\n          { matchStatus  = 201\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n        request methodPatch  \"/organizations?id=eq.3&select=*\"\n          [arrayStrip, (\"Prefer\",\"return=representation\")]\n          [json|{\"name\":\"John\",\"referee\":null}|]\n          `shouldRespondWith`\n          [json|[{\"id\":3,\"name\":\"John\",\"auditor\":2,\"manager_id\":3}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n      it \"strips nulls when Accept: application/vnd.pgrst.array;nulls=stripped\" $\n        request methodGet  \"/organizations?select=*\"\n          [(\"Accept\",\"application/vnd.pgrst.array;nulls=stripped\")]\n          \"\"\n          `shouldRespondWith`\n          [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}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n      it \"strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped\" $\n        request methodGet  \"/organizations?limit=1\"\n          [singularStrip]\n          \"\"\n          `shouldRespondWith`\n          [json|{\"id\":1,\"name\":\"Referee Org\",\"manager_id\":1}|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTSingularStrip]\n          }\n\n      it \"throws error when Accept: application/vnd.pgrst.object+json;nulls=stripped and result not singular\" $\n        request methodGet  \"/organizations?select=*\"\n          [singularStrip]\n          \"\"\n          `shouldRespondWith`\n          [json|{\"details\":\"The result contains 6 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n          { matchStatus  = 406\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"strip nulls from response even if explicitly selected\" $ do\n      it \"strips nulls when Accept: application/vnd.pgrst.array+json;nulls=stripped\" $ do\n        request methodGet  \"/organizations?select=id,referee,auditor\"\n          [arrayStrip]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":1},{\"id\":2},{\"id\":3,\"referee\":1,\"auditor\":2},{\"id\":4,\"referee\":1,\"auditor\":2},{\"id\":5,\"referee\":3,\"auditor\":4},{\"id\":6,\"referee\":3,\"auditor\":4}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n        request methodPost  \"/organizations?select=id,referee,auditor\"\n          [arrayStrip,(\"Prefer\",\"return=representation\")]\n          [json|{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}|]\n          `shouldRespondWith`\n          [json|[{\"id\":7}]|]\n          { matchStatus  = 201\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n        request methodPatch  \"/organizations?id=eq.3&select=id,name,referee,auditor\"\n          [arrayStrip, (\"Prefer\",\"return=representation\")]\n          [json|{\"name\":\"John\",\"referee\":null}|]\n          `shouldRespondWith`\n          [json|[{\"id\":3,\"name\":\"John\",\"auditor\":2}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTArrayStrip]\n          }\n\n      it \"strips nulls when Accept: application/vnd.pgrst.object+json;nulls=stripped\" $\n        request methodGet  \"/organizations?select=id,referee,auditor&limit=1\"\n          [singularStrip]\n          \"\"\n          `shouldRespondWith`\n          [json|{\"id\":1}|]\n          { matchStatus  = 200\n          , matchHeaders = [matchCTSingularStrip]\n          }\n\n    context \"doesn't strip nulls\" $ do\n      it \"doesn't strips nulls when Accept: application/vnd.pgrst.array+json\" $\n        request methodGet  \"/organizations?select=id,referee,auditor\"\n          [(\"Accept\", \"application/vnd.pgrst.array+json\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":1,\"referee\":null,\"auditor\":null},{\"id\":2,\"referee\":null,\"auditor\":null},{\"id\":3,\"referee\":1,\"auditor\":2},{\"id\":4,\"referee\":1,\"auditor\":2},{\"id\":5,\"referee\":3,\"auditor\":4},{\"id\":6,\"referee\":3,\"auditor\":4}]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/PgSafeUpdateSpec.hs",
    "content": "module Feature.Query.PgSafeUpdateSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude hiding (get, put)\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Enabling pg-safeupdate\" $ do\n    context \"Full table update\" $ do\n      it \"does not update and throws error if no condition is present\" $\n        request methodPatch \"/safe_update_items\"\n            [(\"Prefer\", \"count=exact\")]\n            [json| {\"name\": \"New name\"} |]\n          `shouldRespondWith`\n            [json|{\n              \"code\": \"21000\",\n              \"details\": null,\n              \"hint\": null,\n              \"message\": \"UPDATE requires a WHERE clause\"\n            }|]\n            { matchStatus  = 400 }\n\n      it \"allows full table update if a filter is present\" $\n        request methodPatch \"/safe_update_items?id=gt.0\" mempty [json| {\"name\": \"updated-item\"} |]\n          `shouldRespondWith`\n          204\n\n    context \"Full table delete\" $ do\n      it \"does not delete and throws error if no condition is present\" $\n        request methodDelete \"/safe_delete_items\" [] mempty\n          `shouldRespondWith`\n            [json|{\n              \"code\": \"21000\",\n              \"details\": null,\n              \"hint\": null,\n              \"message\": \"DELETE requires a WHERE clause\"\n            }|]\n            { matchStatus  = 400 }\n\n      it \"allows full table delete if a filter is present\" $\n        request methodDelete \"/safe_delete_items?id=gt.0\" mempty mempty\n          `shouldRespondWith`\n          204\n\ndisabledSpec :: SpecWith ((), Application)\ndisabledSpec =\n  describe \"Disabling pg-safeupdate\" $ do\n    context \"Full table update\" $ do\n      it \"works if no condition is present\" $\n        request methodPatch \"/unsafe_update_items\" mempty [json| {\"name\": \"updated-item\"} |]\n          `shouldRespondWith`\n          204\n\n    context \"Full table delete\" $ do\n      it \"works if no condition is present\" $\n        request methodDelete \"/unsafe_delete_items\" mempty mempty\n          `shouldRespondWith`\n          204\n"
  },
  {
    "path": "test/spec/Feature/Query/PlanSpec.hs",
    "content": "{-# LANGUAGE MultiWayIf #-}\n\nmodule Feature.Query.PlanSpec where\n\nimport Control.Lens     ((^?))\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (..))\n\nimport           Data.Aeson.Lens\nimport           Data.Aeson.QQ\nimport qualified Data.ByteString.Lazy as LBS\nimport qualified Data.Text            as T\nimport           Network.HTTP.Types\nimport           Test.Hspec           hiding (pendingWith)\nimport           Test.Hspec.Wai\nimport           Test.Hspec.Wai.JSON\n\nimport PostgREST.Config.PgVersion (PgVersion, pgVersion170)\nimport Protolude                  hiding (get)\nimport SpecHelper\n\nspec :: PgVersion -> SpecWith ((), Application)\nspec actualPgVersion = do\n  describe \"read table/view plan\" $ do\n    it \"outputs the total cost for a single filter on a table\" $ do\n      r <- request methodGet \"/projects?id=in.(1,2,3)\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") \"\"\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63)\n\n    it \"outputs the total cost for a single filter on a view\" $ do\n      r <- request methodGet \"/projects_view?id=gt.2\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") \"\"\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 24.28\n\n    it \"outputs blocks info when using the buffers option\" $ do\n      r <- request methodGet \"/projects\" (acceptHdrs \"application/vnd.pgrst.plan+json; options=buffers\") \"\"\n\n      let resBody  = simpleBody r\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=buffers; charset=utf-8\")\n        resBody `shouldSatisfy` (\\t -> T.isInfixOf \"Shared Hit Blocks\" (decodeUtf8 $ LBS.toStrict t))\n\n    it \"outputs the search path when using the settings option\" $ do\n      r <- request methodGet \"/projects\" (acceptHdrs \"application/vnd.pgrst.plan+json; options=settings\") \"\"\n\n      let searchPath  = simpleBody r ^? nth 0 . key \"Settings\"\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=settings; charset=utf-8\")\n        searchPath `shouldBe`\n          Just [aesonQQ|\n            {\n              \"search_path\": \"\\\"test\\\"\"\n            }\n          |]\n\n    it \"outputs WAL info when using the wal option\" $ do\n      r <- request methodGet \"/projects\" (acceptHdrs \"application/vnd.pgrst.plan+json; options=analyze|wal\") \"\"\n\n      let walRecords  = simpleBody r ^? nth 0 . key \"Plan\" . key \"WAL Records\"\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze|wal; charset=utf-8\")\n        walRecords `shouldBe` Just [aesonQQ|0|]\n\n    it \"outputs columns info when using the verbose option\" $ do\n      r <- request methodGet \"/projects\" (acceptHdrs \"application/vnd.pgrst.plan+json; options=verbose\") \"\"\n\n      let cols  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Plans\" . nth 0 . key \"Output\"\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=verbose; charset=utf-8\")\n        cols `shouldBe` Just [aesonQQ| [\"projects.id\", \"projects.name\", \"projects.client_id\"] |]\n\n    it \"outputs the plan for application/json \" $ do\n      r <- request methodGet \"/projects\" (acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=verbose\") \"\"\n\n      let aggCol  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Output\" . nth 2\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=verbose; charset=utf-8\")\n        aggCol `shouldBe`\n          Just [aesonQQ| \"COALESCE(json_agg(ROW(projects.id, projects.name, projects.client_id)), '[]'::json)\" |]\n\n    it \"outputs the plan for application/vnd.pgrst.object \" $ do\n      r <- request methodGet \"/projects_view\" (acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.pgrst.object\\\"; options=verbose\") \"\"\n\n      let aggCol  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Output\" . nth 2\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.pgrst.object+json\\\"; options=verbose; charset=utf-8\")\n        aggCol `shouldBe`\n          Just [aesonQQ| \"COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)\" |]\n\n  describe \"writes plans\" $ do\n    it \"outputs the total cost for an insert\" $ do\n      r <- request methodPost \"/projects\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") [json|{\"id\":100, \"name\": \"Project 100\"}|]\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 0.06\n\n    it \"outputs the total cost for an update\" $ do\n      r <- request methodPatch \"/projects?id=eq.3\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") [json|{\"name\": \"Patched Project\"}|]\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 8.23\n\n    it \"outputs the total cost for a delete\" $ do\n      r <- request methodDelete \"/projects?id=in.(1,2,3)\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") \"\"\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68)\n\n    it \"outputs the total cost for a single upsert\" $ do\n      r <- request methodPut \"/tiobe_pls?name=eq.Go\"\n            (acceptHdrs \"application/vnd.pgrst.plan+json\")\n            [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 3.55\n\n    it \"outputs the total cost for 2 upserts\" $ do\n      r <- request methodPost \"/tiobe_pls\"\n            [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n            [json| [ { \"name\": \"Python\", \"rank\": 19 }, { \"name\": \"Go\", \"rank\": 20} ]|]\n\n      let totalCost  = planCost r\n          resStatus  = simpleStatus r\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 5.53\n\n    it \"outputs the total cost for an upsert with 10 rows\" $ do\n      r <- request methodPost \"/tiobe_pls\"\n            [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n            (getInsertDataForTiobePlsTable 10)\n\n      let totalCost  = planCost r\n          resStatus  = simpleStatus r\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 5.53\n\n    it \"outputs the total cost for an upsert with 100 rows\" $ do\n      r <- request methodPost \"/tiobe_pls\"\n            [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n            (getInsertDataForTiobePlsTable 100)\n\n      let totalCost  = planCost r\n          resStatus  = simpleStatus r\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 5.53\n\n    it \"outputs the total cost for an upsert with 1000 rows\" $ do\n      r <- request methodPost \"/tiobe_pls\"\n            [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n            (getInsertDataForTiobePlsTable 1000)\n\n      let totalCost  = planCost r\n          resStatus  = simpleStatus r\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 5.53\n\n    it \"outputs the plan for application/vnd.pgrst.object\" $ do\n      r <- request methodDelete \"/projects?id=eq.6\"\n        [(\"Prefer\", \"return=representation\"), (\"Accept\", \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.pgrst.object\\\"; options=verbose\")] \"\"\n\n      let aggCol  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Output\" . nth 3\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.pgrst.object+json\\\"; options=verbose; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        aggCol `shouldBe` Just [aesonQQ| \"COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)\" |]\n\n  describe \"function plan\" $ do\n    it \"outputs the total cost for a function call\" $ do\n      r <- request methodGet \"/rpc/getallprojects?id=in.(1,2,3)\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") \"\"\n\n      let totalCost  = planCost r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        totalCost `shouldBe` 68.56\n\n  describe \"text format\" $ do\n    it \"outputs the total cost for a function call\" $ do\n      r <- request methodGet \"/projects?id=in.(1,2,3)\"\n             (acceptHdrs \"application/vnd.pgrst.plan+text\") \"\"\n\n      let resBody    = simpleBody r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+text; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        resBody `shouldSatisfy` (\\t -> LBS.take 9 t == \"Aggregate\")\n\n    it \"outputs in text format by default\" $ do\n      r <- request methodGet \"/projects?id=in.(1,2,3)\"\n             (acceptHdrs \"application/vnd.pgrst.plan\") \"\"\n\n      let resBody    = simpleBody r\n          resHeaders = simpleHeaders r\n          resStatus  = simpleStatus r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+text; for=\\\"application/json\\\"; charset=utf-8\")\n        resHeaders `shouldSatisfy` notZeroContentLength\n        resStatus `shouldBe` Status { statusCode = 200, statusMessage=\"OK\" }\n        resBody `shouldSatisfy` (\\t -> LBS.take 9 t == \"Aggregate\")\n\n  describe \"resource embedding costs\" $ do\n    it \"a one to many doesn't surpass a threshold\" $ do\n      r <- request methodGet \"/clients?select=*,projects(*)&id=eq.1\"\n             [planHdr] \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 33.3)\n\n    it \"a many to one doesn't surpass a threshold\" $ do\n      r <- request methodGet \"/projects?select=*,clients(*)&id=eq.1\"\n             [planHdr] \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 16.5)\n\n    it \"a many to many doesn't surpass a threshold\" $ do\n      r <- request methodGet \"/users?select=*,tasks(*)&id=eq.1\"\n             (acceptHdrs \"application/vnd.pgrst.plan+json\") \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 70.9)\n\n    context \"!inner vs embed not null\" $ do\n      it \"on an o2m, an !inner has a similar cost to not.null\" $ do\n        r1 <- request methodGet \"/clients?select=*,projects!inner(*)&id=eq.1\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r1 `shouldSatisfy` (< 33.3)\n\n        r2 <- request methodGet \"/clients?select=*,projects(*)&projects=not.is.null&id=eq.1\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r2 `shouldSatisfy` (< 33.3)\n\n      it \"on an m2o, an !inner has a similar cost to not.null\" $ do\n        r1 <- request methodGet \"/projects?select=*,clients!inner(*)&id=eq.1\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r1 `shouldSatisfy` (< 16.42)\n\n        r2 <- request methodGet \"/projects?select=*,clients(*)&clients=not.is.null&id=eq.1\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r2 `shouldSatisfy` (< 16.42)\n\n      it \"on an m2m, an !inner has a similar cost to not.null\" $ do\n        r1 <- request methodGet \"/users?select=*,tasks!inner(*)&tasks.id=eq.1\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r1 `shouldSatisfy` (< 20888.83)\n\n        r2 <- request methodGet \"/users?select=*,tasks(*)&tasks.id=eq.1&tasks=not.is.null\"\n               [planHdr] \"\"\n\n        liftIO $ planCost r2 `shouldSatisfy` (< 20888.83)\n\n  describe \"function call costs\" $ do\n    it \"should not exceed cost when calling setof composite proc\" $ do\n      r <- request methodGet \"/rpc/get_projects_below?id=3\"\n             [planHdr] \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 35.4)\n\n    it \"should not exceed cost when calling setof composite proc with empty params\" $ do\n      r <- request methodGet \"/rpc/getallprojects\"\n             [planHdr] \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 71.0)\n\n    it \"should not exceed cost when calling scalar proc\" $ do\n      r <- request methodGet \"/rpc/add_them?a=3&b=4\"\n             [planHdr] \"\"\n\n      liftIO $ planCost r `shouldSatisfy` (< 0.08)\n\n    context \"function inlining\" $ do\n      it \"should inline a zero argument function(the function won't appear in the plan tree)\" $ do\n        r <- request methodGet \"/rpc/getallusers?id=eq.1\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> not $ T.isInfixOf \"getallusers\" (decodeUtf8 $ LBS.toStrict t))\n\n      it \"should inline a function with arguments(the function won't appear in the plan tree)\" $ do\n        r <- request methodGet \"/rpc/getitemrange?min=10&max=15\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> not $ T.isInfixOf \"getitemrange\" (decodeUtf8 $ LBS.toStrict t))\n\n    context \"index usage\" $ do\n      it \"should use an index for a json arrow operator filter\" $ do\n        r <- request methodGet \"/bets?data_json->>contractId=eq.1\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> T.isInfixOf \"Index Cond\" (decodeUtf8 $ LBS.toStrict t))\n\n      it \"should use an index for a jsonb arrow operator filter\" $ do\n        r <- request methodGet \"/bets?data_jsonb->>contractId=eq.1\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> T.isInfixOf \"Index\" (decodeUtf8 $ LBS.toStrict t))\n\n      it \"should use an index for ordering on a json arrow operator\" $ do\n        r <- request methodGet \"/bets?order=data_json->>contractId\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> T.isInfixOf \"Index\" (decodeUtf8 $ LBS.toStrict t))\n\n      it \"should use an index for ordering on a jsonb arrow operator\" $ do\n        r <- request methodGet \"/bets?order=data_jsonb->>contractId\"\n               [(hAccept, \"application/vnd.pgrst.plan\")] \"\"\n\n        let resBody = simpleBody r\n\n        liftIO $ do\n          resBody `shouldSatisfy` (\\t -> T.isInfixOf \"Index\" (decodeUtf8 $ LBS.toStrict t))\n\n  describe \"custom media types\" $ do\n    it \"outputs the plan for a scalar function text/xml\" $ do\n      r <- request methodGet \"/rpc/return_scalar_xml\"\n        (acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"text/xml\\\"; options=verbose\") \"\"\n\n      let aggCol  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Output\" . nth 2\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"text/xml\\\"; options=verbose; charset=utf-8\")\n        aggCol `shouldBe` Just [aesonQQ| \"return_scalar_xml.pgrst_scalar\" |]\n\n    it \"outputs the plan for an aggregate application/vnd.twkb\" $ do\n      r <- request methodGet \"/lines\"\n        (acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.twkb\\\"; options=verbose\") \"\"\n\n      let aggCol  = simpleBody r ^? nth 0 . key \"Plan\" . key \"Output\" . nth 2\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/vnd.twkb\\\"; options=verbose; charset=utf-8\")\n        aggCol `shouldBe`\n          Just [aesonQQ| \"twkb_agg(ROW(lines.id, lines.name, lines.geom)::lines)\" |]\n\n  describe \"plan of upsert with DEFAULT surrogate primary key\" $ do\n    it \"test case sensitive sequence is properly quoted in nextval()\" $ do\n      r <- request methodPost \"/Surr_Gen_Default_Upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation, resolution=merge-duplicates, missing=default\"), (\"Accept\", \"application/vnd.pgrst.plan+json; options=verbose\")]\n        [json| [\n            { \"id\": 1, \"name\": \"updated value\" },\n            { \"name\": \"new value\" }\n        ]|]\n\n      let nextValSnip = simpleBody r ^? nth 0 . key \"Plan\" . key \"Plans\" . nth 0 . key \"Plans\" . nth 0 . key \"Plans\" . nth 0 . key \"Output\"\n          resHeaders = simpleHeaders r\n\n      liftIO $ do\n        resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=verbose; charset=utf-8\")\n        nextValSnip `shouldBe`\n          Just [aesonQQ| [\"jsonb_agg((jsonb_build_object('id', nextval('\\\"Surr_Gen_Default_Upsert_id_seq\\\"'::regclass)) || elem.value))\"] |]\n\n  describe \"count preference plan costs\" $ do\n    context \"tables with count=exact\" $ do\n      it \"shows only 1 count aggregate when no limits/max-rows are set\" $ do\n        _ <- request methodPost \"/tiobe_pls\"\n              [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n              (getInsertDataForTiobePlsTable 1000)\n\n        r <- request methodGet \"/tiobe_pls\"\n          ((\"Prefer\", \"count=exact\") : acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze;\") \"\"\n\n        let resBody = simpleBody r\n            resHeaders = simpleHeaders r\n            totalCost = planCost r\n            aggregateQty = subtract 1 $ length $ T.splitOn \"Aggregate\" (decodeUtf8 $ LBS.toStrict resBody)\n\n        liftIO $ do\n          resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze; charset=utf-8\")\n          totalCost `shouldSatisfy` (< 33.0)\n          aggregateQty `shouldBe` 1\n\n      it \"shows relevant count aggregates when limits are set\" $ do\n        _ <- request methodPost \"/tiobe_pls\"\n              [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n              (getInsertDataForTiobePlsTable 1000)\n\n        r <- request methodGet \"/tiobe_pls?limit=1000\"\n          ((\"Prefer\", \"count=exact\") : acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze;\") \"\"\n\n        let resBody = simpleBody r\n            resHeaders = simpleHeaders r\n            totalCost = planCost r\n            aggregateQty = subtract 1 $ length $ T.splitOn \"Aggregate\" (decodeUtf8 $ LBS.toStrict resBody)\n\n        liftIO $ do\n          resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze; charset=utf-8\")\n          totalCost `shouldSatisfy` (> 49.0)\n          aggregateQty `shouldSatisfy` (> 1)\n\n    context \"functions with count=exact\" $ do\n      it \"shows only 1 count aggregate when no limits/max-rows are set\" $ do\n        _ <- request methodPost \"/tiobe_pls\"\n              [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\"), (\"Prefer\", \"return=representation\")]\n              (getInsertDataForTiobePlsTable 1000)\n\n        r <- request methodGet \"/rpc/get_tiobe_pls\"\n          ((\"Prefer\", \"count=exact\") : acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze;\") \"\"\n\n        let resBody = simpleBody r\n            resHeaders = simpleHeaders r\n            totalCost = planCost r\n            aggregateQty = subtract 1 $ length $ T.splitOn \"Aggregate\" (decodeUtf8 $ LBS.toStrict resBody)\n\n        liftIO $ do\n          resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze; charset=utf-8\")\n          totalCost `shouldSatisfy` (< 38.0)\n          aggregateQty `shouldBe` 1\n\n      it \"shows relevant count aggregates when limits are set\" $ do\n        _ <- request methodPost \"/tiobe_pls\"\n              [(\"Prefer\",\"resolution=merge-duplicates\"), (\"Accept\",\"application/vnd.pgrst.plan+json\")]\n              (getInsertDataForTiobePlsTable 1000)\n\n        r <- request methodGet \"/rpc/get_tiobe_pls?limit=1000\"\n          ((\"Prefer\", \"count=exact\") : acceptHdrs \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze;\") \"\"\n\n        let resBody = simpleBody r\n            resHeaders = simpleHeaders r\n            totalCost = planCost r\n            aggregateQty = subtract 1 $ length $ T.splitOn \"Aggregate\" (decodeUtf8 $ LBS.toStrict resBody)\n\n        liftIO $ do\n          resHeaders `shouldSatisfy` elem (\"Content-Type\", \"application/vnd.pgrst.plan+json; for=\\\"application/json\\\"; options=analyze; charset=utf-8\")\n          totalCost `shouldSatisfy` (> 67.0)\n          aggregateQty `shouldSatisfy` (> 1)\n\ndisabledSpec :: SpecWith ((), Application)\ndisabledSpec =\n  it \"doesn't work if db-plan-enabled=false(the default)\" $ do\n    request methodGet \"/projects?id=in.(1,2,3)\"\n         (acceptHdrs \"application/vnd.pgrst.plan\") \"\"\n      `shouldRespondWith` 406\n\n    request methodGet \"/rpc/getallprojects?id=in.(1,2,3)\"\n      (acceptHdrs \"application/vnd.pgrst.plan\") \"\"\n      `shouldRespondWith` 406\n\n    request methodDelete \"/projects?id=in.(1,2,3)\"\n           (acceptHdrs \"application/vnd.pgrst.plan\") \"\"\n      `shouldRespondWith` 406\n"
  },
  {
    "path": "test/spec/Feature/Query/PostGISSpec.hs",
    "content": "module Feature.Query.PostGISSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"PostGIS features\" $\n  context \"GeoJSON output\" $ do\n    it \"works for a table that has a geometry column\" $\n      request methodGet \"/shops\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json| {\n          \"type\" : \"FeatureCollection\",\n          \"features\" : [\n            {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.10044,42.373695]}, \"properties\": {\"id\": 1, \"address\": \"1369 Cambridge St\"}}\n          , {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.10543,42.366432]}, \"properties\": {\"id\": 2, \"address\": \"757 Massachusetts Ave\"}}\n          , {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.081924,42.36437]}, \"properties\": {\"id\": 3, \"address\": \"605 W Kendall St\"}}\n          ]} |]\n        { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"fails for a table that doesn't have a geometry column\" $\n      request methodGet \"/projects\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json| {\"hint\":null,\"details\":null,\"code\":\"22023\",\"message\":\"geometry column is missing\"} |]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"gives an empty features array on no rows\" $\n      request methodGet \"/shops?id=gt.3\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json| {\n          \"type\" : \"FeatureCollection\",\n          \"features\" : []} |]\n        { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"must include the geometry column when using ?select\" $\n      request methodGet \"/shops?select=id,shop_geom&id=eq.1\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json| {\n          \"type\" : \"FeatureCollection\",\n          \"features\" : [\n            {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.10044,42.373695]}, \"properties\": {\"id\": 1}}\n          ] }|]\n        { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"works with resource embedding\" $\n      request methodGet \"/shops?select=*,shop_bles(*)&id=eq.1\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json| {\n          \"type\": \"FeatureCollection\",\n          \"features\": [\n            {\n              \"type\": \"Feature\",\n              \"geometry\": { \"coordinates\": [ -71.10044, 42.373695 ], \"type\": \"Point\" },\n              \"properties\": {\n                \"address\": \"1369 Cambridge St\", \"id\": 1,\n                \"shop_bles\": [\n                  { \"id\": 1, \"name\": \"Beacon-1\", \"shop_id\": 1 ,\n                    \"coords\": { \"coordinates\": [ -71.10044, 42.373695 ], \"crs\": { \"properties\": { \"name\": \"EPSG:4326\" }, \"type\": \"name\" }, \"type\": \"Point\" },\n                    \"range_area\": {\n                      \"coordinates\": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ],\n                      \"crs\": { \"properties\": { \"name\": \"EPSG:4326\" }, \"type\": \"name\" }, \"type\": \"Polygon\" }\n                  },\n                  { \"coords\": { \"coordinates\": [ -71.10044, 42.373695 ], \"crs\": { \"properties\": { \"name\": \"EPSG:4326\" }, \"type\": \"name\" }, \"type\": \"Point\" },\n                    \"id\": 2, \"name\": \"Beacon-2\", \"shop_id\": 1,\n                    \"range_area\": {\n                      \"coordinates\": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ],\n                      \"crs\": { \"properties\": { \"name\": \"EPSG:4326\" }, \"type\": \"name\" }, \"type\": \"Polygon\" }\n                  }\n                ]\n              }\n            }\n          ]\n        }|]\n        { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"works with RPC\" $\n      request methodGet \"/rpc/get_shop?id=1\"\n        [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n        [json|{\n          \"type\" : \"FeatureCollection\",\n          \"features\" : [\n            {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.10044,42.373695]}, \"properties\": {\"id\": 1, \"address\": \"1369 Cambridge St\"} }\n          ]\n        }|]\n        { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"works with Prefer: return=representation after POST\" $\n      request methodPost \"/shops\"\n        [(\"Accept\", \"application/geo+json\"), (\"Prefer\", \"return=representation\")] [json|\n          {\"id\": 4, \"address\": \"1354 Massachusetts Ave\", \"shop_geom\": \"SRID=4326;POINT(-71.11834 42.373238)\"}\n        |] `shouldRespondWith`\n        [json|{\n          \"type\": \"FeatureCollection\",\n          \"features\": [\n            {\n              \"type\": \"Feature\",\n              \"geometry\": { \"coordinates\": [ -71.11834, 42.373238 ], \"type\": \"Point\" },\n              \"properties\": { \"address\": \"1354 Massachusetts Ave\", \"id\": 4 }\n            }\n          ]\n        }|]\n        { matchStatus  = 201\n        , matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"]\n        }\n\n    it \"works with Prefer: return=representation after PATCH\" $\n      request methodPatch \"/shops?id=eq.3\"\n        [(\"Accept\", \"application/geo+json\"), (\"Prefer\", \"return=representation\")]\n        [json| { \"address\": \"1354 Massachusetts Avenue\"} |]\n        `shouldRespondWith`\n        [json|{\n          \"type\": \"FeatureCollection\",\n          \"features\": [\n            {\n              \"type\": \"Feature\",\n              \"geometry\": { \"coordinates\": [-71.081924,42.36437], \"type\": \"Point\" },\n              \"properties\": { \"address\": \"1354 Massachusetts Avenue\", \"id\": 3 }\n            }\n          ]\n        }|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"]\n        }\n\n    it \"works with Prefer: return=representation after PUT\" $\n      request methodPut \"/shops?id=eq.3\"\n        [(\"Accept\", \"application/geo+json\"), (\"Prefer\", \"return=representation\")]\n        [json| { \"id\": 3, \"address\": \"1354 Massachusetts Avenue\"} |]\n        `shouldRespondWith`\n        [json|{\n          \"type\": \"FeatureCollection\",\n          \"features\": [\n            {\n              \"type\": \"Feature\",\n              \"geometry\": { \"coordinates\": [-71.081924,42.36437], \"type\": \"Point\" },\n              \"properties\": { \"address\": \"1354 Massachusetts Avenue\", \"id\": 3 }\n            }\n          ]\n        }|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"]\n        }\n\n    it \"works with Prefer: return=representation after DELETE\" $\n      request methodDelete \"/shops?id=eq.3\"\n        [(\"Accept\", \"application/geo+json\"), (\"Prefer\", \"return=representation\")] \"\" `shouldRespondWith`\n        [json|{\n          \"type\" : \"FeatureCollection\",\n          \"features\" : [\n            {\"type\": \"Feature\", \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.081924,42.36437]}, \"properties\": {\"id\": 3, \"address\": \"605 W Kendall St\"}}\n          ]\n        }|]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"]\n        }\n\n    context \"multiple geometry columns\" $\n      it \"can select the geometry column to get as a feature with ?select\" $ do\n        request methodGet \"/shop_bles?select=id,name,coords&id=eq.1\"\n          [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n          [json|{\n            \"type\" : \"FeatureCollection\",\n            \"features\" : [\n              {\"type\": \"Feature\",\n               \"geometry\": {\"type\":\"Point\",\"coordinates\":[-71.10044,42.373695]},\n               \"properties\": {\"id\": 1, \"name\": \"Beacon-1\"}\n              }\n            ]}|]\n          { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n        request methodGet \"/shop_bles?select=id,name,range_area&id=eq.1\"\n          [(\"Accept\", \"application/geo+json\")] \"\" `shouldRespondWith`\n          [json|{\n            \"type\" : \"FeatureCollection\",\n            \"features\" : [\n              {\"type\": \"Feature\",\n               \"geometry\": {\"type\":\"Polygon\",\"coordinates\":[[[-71.100452542,42.373870833],[-71.100480705,42.373771262],[-71.100396886,42.373758382],[-71.100370064,42.373858449],[-71.100452542,42.373870833]]]},\n               \"properties\": {\"id\": 1, \"name\": \"Beacon-1\"}}\n            ]}|]\n          { matchHeaders = [\"Content-Type\" <:> \"application/geo+json; charset=utf-8\"] }\n\n    it \"gets the geojson geometry object with the regular application/json output\" $\n      request methodGet \"/shops?id=eq.1\" [] \"\" `shouldRespondWith`\n        [json|[{\n          \"id\":1,\"address\":\"1369 Cambridge St\",\n          \"shop_geom\":{\"type\":\"Point\",\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}},\"coordinates\":[-71.10044,42.373695]}\n          }]|]\n        { matchHeaders = [matchContentTypeJson] }\n"
  },
  {
    "path": "test/spec/Feature/Query/PreferencesSpec.hs",
    "content": "module Feature.Query.PreferencesSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"test prefer headers and preference-applied headers\" $ do\n\n    context \"check behaviour of Prefer: handling=strict\" $ do\n      it \"throws error when handling=strict and invalid prefs are given\" $\n        request methodGet  \"/items\" [(\"Prefer\", \"handling=strict, anything\")] \"\"\n          `shouldRespondWith`\n          [json|{\"details\":\"Invalid preferences: anything\",\"message\":\"Invalid preferences given with handling=strict\",\"code\":\"PGRST122\",\"hint\":null}|]\n          { matchStatus = 400 }\n\n      it \"throw error when handling=strict and invalid prefs are given with multiples in separate prefers\" $\n        request methodGet  \"/items\" [(\"Prefer\", \"handling=strict\"),(\"Prefer\",\"something, else\")] \"\"\n          `shouldRespondWith`\n          [json|{\"details\":\"Invalid preferences: something, else\",\"message\":\"Invalid preferences given with handling=strict\",\"code\":\"PGRST122\",\"hint\":null}|]\n          { matchStatus = 400 }\n\n      it \"throws error with post request\" $\n        request methodPost  \"/organizations?select=*\"\n          [(\"Prefer\",\"return=representation, handling=strict, anything\")]\n          [json|{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}|]\n          `shouldRespondWith`\n          [json|{\"details\":\"Invalid preferences: anything\",\"message\":\"Invalid preferences given with handling=strict\",\"code\":\"PGRST122\",\"hint\":null}|]\n          { matchStatus = 400 }\n\n      it \"throws error with rpc\" $\n        request methodPost \"/rpc/overloaded_unnamed_param\"\n          [(\"Content-Type\", \"application/json\"), (\"Prefer\", \"handling=strict, anything\")]\n          [json|{}|]\n          `shouldRespondWith`\n          [json|{\"details\":\"Invalid preferences: anything\",\"message\":\"Invalid preferences given with handling=strict\",\"code\":\"PGRST122\",\"hint\":null}|]\n          { matchStatus = 400 }\n\n    context \"check behaviour of Prefer: handling=lenient\" $ do\n      it \"does not throw error when handling=lenient and invalid prefs\" $\n        request methodGet  \"/items\" [(\"Prefer\", \"handling=lenient, anything\")] \"\"\n          `shouldRespondWith` 200\n\n      it \"does not throw error when handling=lenient and invalid prefs in multiples prefers\" $\n        request methodGet  \"/items\" [(\"Prefer\", \"handling=lenient\"), (\"Prefer\", \"anything\")] \"\"\n          `shouldRespondWith` 200\n\n      it \"does not throw error with post request\" $\n        request methodPost  \"/organizations?select=*\"\n          [(\"Prefer\",\"return=representation, handling=lenient, anything\")]\n          [json|{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}|]\n          `shouldRespondWith`\n          [json|[{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}]|]\n          { matchStatus  = 201\n          , matchHeaders = [ matchContentTypeJson ]\n          }\n\n      it \"does not throw error with rpc\" $\n        request methodPost \"/rpc/overloaded_unnamed_param\"\n          [(\"Content-Type\", \"application/json\"), (\"Prefer\", \"handling=lenient, anything\")]\n          [json|{}|]\n          `shouldRespondWith`\n          [json| 1 |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n           }\n\n    context \"test Prefer: timezone=America/Los_Angeles\" $ do\n      it \"should change timezone with handling=strict\" $\n        request methodGet \"/timestamps\"\n          [(\"Prefer\", \"handling=strict, timezone=America/Los_Angeles\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"t\":\"2023-10-18T05:37:59.611-07:00\"}, {\"t\":\"2023-10-18T07:37:59.611-07:00\"}, {\"t\":\"2023-10-18T09:37:59.611-07:00\"}]|]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson\n                           , \"Preference-Applied\" <:> \"handling=strict, timezone=America/Los_Angeles\"]}\n\n      it \"should change timezone without handling=strict\" $\n        request methodGet \"/timestamps\"\n          [(\"Prefer\", \"timezone=America/Los_Angeles\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"t\":\"2023-10-18T05:37:59.611-07:00\"}, {\"t\":\"2023-10-18T07:37:59.611-07:00\"}, {\"t\":\"2023-10-18T09:37:59.611-07:00\"}]|]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson\n                           , \"Preference-Applied\" <:> \"timezone=America/Los_Angeles\"] }\n\n    context \"test Prefer: timezone=Invalid/Timezone\" $ do\n      it \"should throw error with handling=strict\" $\n        request methodGet \"/timestamps\"\n          [(\"Prefer\", \"handling=strict, timezone=Invalid/Timezone\")]\n          \"\"\n          `shouldRespondWith`\n          [json|{\"code\":\"PGRST122\",\"details\":\"Invalid preferences: timezone=Invalid/Timezone\",\"hint\":null,\"message\":\"Invalid preferences given with handling=strict\"}|]\n          { matchStatus = 400 }\n\n      it \"should return with default timezone without handling or with handling=lenient\" $ do\n        request methodGet \"/timestamps\"\n          [(\"Prefer\", \"timezone=Invalid/Timezone\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"t\":\"2023-10-18T12:37:59.611+00:00\"}, {\"t\":\"2023-10-18T14:37:59.611+00:00\"}, {\"t\":\"2023-10-18T16:37:59.611+00:00\"}]|]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson]}\n\n        request methodGet \"/timestamps\"\n          [(\"Prefer\", \"handling=lenient, timezone=Invalid/Timezone\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"t\":\"2023-10-18T12:37:59.611+00:00\"}, {\"t\":\"2023-10-18T14:37:59.611+00:00\"}, {\"t\":\"2023-10-18T16:37:59.611+00:00\"}]|]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson\n                           , \"Preference-Applied\" <:> \"handling=lenient\"]}\n\n    context \"test Prefer: max-affected with handling=strict\" $ do\n      it \"should fail if items deleted more than 10\" $\n        request methodDelete \"/items?id=lt.15\"\n          [(\"Prefer\", \"handling=strict, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          [json|{\"code\":\"PGRST124\",\"details\":\"The query affects 14 rows\",\"hint\":null,\"message\":\"Query result exceeds max-affected preference constraint\"}|]\n          { matchStatus = 400 }\n\n      it \"should succeed if items deleted less than 10\" $\n        request methodDelete \"/items?id=lt.10\"\n          [(\"Prefer\", \"handling=strict, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=strict, max-affected=10\"]}\n\n      it \"should fail if items updated more than 0\" $\n        request methodPatch \"/tiobe_pls?name=eq.Java\"\n          [(\"Prefer\", \"handling=strict, max-affected=0\")]\n          [json| [{\"name\":\"Java\", \"rank\":19}] |]\n          `shouldRespondWith`\n          [json|{\"code\":\"PGRST124\",\"details\":\"The query affects 1 rows\",\"hint\":null,\"message\":\"Query result exceeds max-affected preference constraint\"}|]\n          { matchStatus = 400 }\n\n      it \"should succeed if items updated equal 1\" $\n        request methodDelete \"/tiobe_pls?name=eq.Java\"\n          [(\"Prefer\", \"handling=strict, max-affected=1\")]\n          [json| [{\"name\":\"Java\", \"rank\":19}] |]\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=strict, max-affected=1\"]}\n\n    context \"test Prefer: max-affected with handling=lenient\" $ do\n      it \"should not fail\" $\n        request methodDelete \"/items?id=lt.15\"\n          [(\"Prefer\", \"handling=lenient, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=lenient\"]}\n\n      it \"should succeed if items deleted less than 10\" $\n        request methodDelete \"/items?id=lt.10\"\n          [(\"Prefer\", \"handling=lenient, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=lenient\"]}\n\n      it \"should not fail\" $\n        request methodPatch \"/tiobe_pls?name=eq.Java\"\n          [(\"Prefer\", \"handling=lenient, max-affected=0\")]\n          [json| [{\"name\":\"Java\", \"rank\":19}] |]\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=lenient\"]}\n\n      it \"should succeed if items updated equal 1\" $\n        request methodDelete \"/tiobe_pls?name=eq.Java\"\n          [(\"Prefer\", \"handling=lenient, max-affected=1\")]\n          [json| [{\"name\":\"Java\", \"rank\":19}] |]\n          `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [\"Preference-Applied\" <:> \"handling=lenient\"]}\n\n    context \"test Prefer: max-affected with rpc\" $ do\n      it \"should fail with rpc when deleting rows more than prefered with returns setof\" $\n        request methodPost \"/rpc/delete_items_returns_setof\"\n          [(\"Prefer\", \"handling=strict, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST124\",\"details\":\"The query affects 15 rows\",\"hint\":null,\"message\":\"Query result exceeds max-affected preference constraint\"} |]\n          { matchStatus = 400 }\n\n      it \"should fail with rpc when deleting rows more than prefered with returns table\" $\n        request methodPost \"/rpc/delete_items_returns_table\"\n          [(\"Prefer\", \"handling=strict, max-affected=10\")]\n          \"\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST124\",\"details\":\"The query affects 15 rows\",\"hint\":null,\"message\":\"Query result exceeds max-affected preference constraint\"} |]\n          { matchStatus = 400 }\n\n      it \"should succeed with rpc deleting rows less than prefered with returns setof\" $\n        request methodPost \"/rpc/delete_items_returns_setof\"\n          [(\"Prefer\", \"handling=strict, max-affected=20\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4},{\"id\":5},{\"id\":6},{\"id\":7},\n                 {\"id\":8},{\"id\":9},{\"id\":10},{\"id\":11},{\"id\":12},{\"id\":13},\n                 {\"id\":14},{\"id\":15}]|]\n          { matchStatus = 200 }\n\n      it \"should succeed with rpc deleting rows less than prefered with returns table\" $\n        request methodPost \"/rpc/delete_items_returns_table\"\n          [(\"Prefer\", \"handling=strict, max-affected=20\")]\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4},{\"id\":5},{\"id\":6},{\"id\":7},\n                 {\"id\":8},{\"id\":9},{\"id\":10},{\"id\":11},{\"id\":12},{\"id\":13},\n                 {\"id\":14},{\"id\":15}]|]\n          { matchStatus = 200 }\n\n      it \"should fail with rpc when returns void with handling=strict\" $\n        request methodPost \"/rpc/delete_items_returns_void\"\n          [(\"Prefer\", \"handling=strict, max-affected=20\")]\n          \"\"\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST128\",\"details\":null,\"hint\":null,\"message\":\"Function must return SETOF or TABLE when max-affected preference is used with handling=strict\"} |]\n          { matchStatus = 400 }\n"
  },
  {
    "path": "test/spec/Feature/Query/QueryLimitedSpec.hs",
    "content": "module Feature.Query.QueryLimitedSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Requesting many items with server limits(max-rows) enabled\" $ do\n    it \"restricts results\" $\n      get \"/items?order=id\"\n        `shouldRespondWith`\n          [json| [{\"id\":1},{\"id\":2}] |]\n          { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n\n    it \"respects additional client limiting\" $ do\n      request methodGet  \"/items\"\n          (rangeHdrs $ ByteRangeFromTo 0 0)\n          \"\"\n        `shouldRespondWith`\n          [json| [{\"id\":1}] |]\n          { matchHeaders = [\"Content-Range\" <:> \"0-0/*\"] }\n\n    it \"works on all levels\" $\n      get \"/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc\"\n        `shouldRespondWith`\n          [json|[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2}]},{\"id\":2,\"tasks\":[{\"id\":5},{\"id\":6}]}]|]\n          { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n\n    it \"succeeds in getting parent embeds despite the limit, see #647\" $\n      get \"/tasks?select=id,project:projects(id)&id=gt.5\"\n        `shouldRespondWith`\n          [json|[{\"id\":6,\"project\":{\"id\":3}},{\"id\":7,\"project\":{\"id\":4}}]|]\n          { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n\n    it \"can offset the parent embed, being consistent with the other embed types\" $\n      get \"/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1\"\n        `shouldRespondWith`\n          [json|[{\"id\":6,\"project\":null}, {\"id\":7,\"project\":null}]|]\n          { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n\n    context \"count=estimated\" $ do\n      it \"uses the query planner guess when query rows > maxRows\" $\n        request methodHead \"/getallprojects_view\"\n            [(\"Prefer\", \"count=estimated\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-1/2019\" ]\n            }\n\n      it \"gives exact count when query rows <= maxRows\" $\n        request methodHead \"/getallprojects_view?id=lt.3\"\n            [(\"Prefer\", \"count=estimated\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-1/2\" ]\n            }\n\n      it \"only uses the query planner guess if it's indeed greater than the exact count\" $\n        request methodHead \"/get_projects_above_view\"\n            [(\"Prefer\", \"count=estimated\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-1/3\" ]\n            }\n\n    context \"max-rows=2 on mutations\" $ do\n      it \"doesn't affect insertions\" $\n        request methodPost \"/projects?select=id,name\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| [\n              { \"id\": 6, \"name\": \"BeOS\" },\n              { \"id\": 7, \"name\": \"PopOS\" },\n              { \"id\": 8, \"name\": \"HaikuOS\" } ]|]\n          `shouldRespondWith`\n            [json| [\n              { \"id\": 6, \"name\": \"BeOS\" },\n              { \"id\": 7, \"name\": \"PopOS\" },\n              { \"id\": 8, \"name\": \"HaikuOS\" } ]|]\n            { matchStatus  = 201 }\n\n      it \"doesn't affect updates(2 rows would be modified if it did)\" $\n        request methodPatch \"/employees?select=first_name,last_name,occupation\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| [{\"occupation\": \"Barista\"}] |]\n          `shouldRespondWith`\n            [json|[\n                { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"occupation\": \"Barista\" },\n                { \"first_name\": \"Daniel B.\", \"last_name\": \"Lyon\", \"occupation\": \"Barista\" },\n                { \"first_name\": \"Edwin S.\", \"last_name\": \"Smith\", \"occupation\": \"Barista\" } ]|]\n            { matchStatus  = 200 }\n\n      it \"doesn't affect deletions\" $\n        request methodDelete \"/employees?select=first_name,last_name\"\n            [(\"Prefer\", \"return=representation\")]\n            mempty\n          `shouldRespondWith`\n            [json| [\n              { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\" },\n              { \"first_name\": \"Daniel B.\", \"last_name\": \"Lyon\" },\n              { \"first_name\": \"Edwin S.\", \"last_name\": \"Smith\" } ]|]\n            { matchStatus  = 200 }\n\n    context \"max-rows is set and limits are requested\" $ do\n      it \"should work with limit 0\" $\n        get \"/items?limit=0\"\n          `shouldRespondWith`\n            [json| [] |]\n            { matchHeaders = [\"Content-Range\" <:> \"*/*\"] }\n"
  },
  {
    "path": "test/spec/Feature/Query/QuerySpec.hs",
    "content": "module Feature.Query.QuerySpec where\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (simpleHeaders))\n\nimport Network.HTTP.Types\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = do\n\n  describe \"Querying a table with a column called count\" $\n    it \"should not confuse count column with pg_catalog.count aggregate\" $\n      get \"/has_count_column\" `shouldRespondWith` 200\n\n  describe \"Querying a table with a column called t\" $\n    it \"should not conflict with internal postgrest table alias\" $\n      get \"/clashing_column?select=t\" `shouldRespondWith` 200\n\n  describe \"Querying a nonexistent table\" $\n    it \"causes a 404\" $\n      get \"/faketable\"\n      `shouldRespondWith`\n      [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.faketable' in the schema cache\"} |]\n      { matchStatus = 404\n      , matchHeaders = [\"Content-Length\" <:> \"120\"]\n      }\n\n  describe \"Filtering response\" $ do\n    it \"matches with equality\" $\n      get \"/items?id=eq.5\"\n        `shouldRespondWith` [json| [{\"id\":5}] |]\n        { matchHeaders = [\n            \"Content-Range\" <:> \"0-0/*\",\n            \"Content-Length\" <:> \"10\"\n          ] }\n\n    it \"matches with equality using not operator\" $\n      get \"/items?id=not.eq.5&order=id\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4},{\"id\":6},{\"id\":7},{\"id\":8},{\"id\":9},{\"id\":10},{\"id\":11},{\"id\":12},{\"id\":13},{\"id\":14},{\"id\":15}] |]\n        { matchHeaders = [\"Content-Range\" <:> \"0-13/*\"] }\n\n    it \"matches with more than one condition using not operator\" $\n      get \"/simple_pk?k=like.*yx&extra=not.eq.u\" `shouldRespondWith` \"[]\"\n\n    it \"matches with inequality using not operator\" $ do\n      get \"/items?id=not.lt.14&order=id.asc\"\n        `shouldRespondWith` [json| [{\"id\":14},{\"id\":15}] |]\n        { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n      get \"/items?id=not.gt.2&order=id.asc\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":2}] |]\n        { matchHeaders = [\"Content-Range\" <:> \"0-1/*\"] }\n\n    it \"matches items IN\" $\n      get \"/items?id=in.(1,3,5)\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":3},{\"id\":5}] |]\n        { matchHeaders = [\"Content-Range\" <:> \"0-2/*\"] }\n\n    it \"matches items NOT IN using not operator\" $\n      get \"/items?id=not.in.(2,4,6,7,8,9,10,11,12,13,14,15)\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":3},{\"id\":5}] |]\n        { matchHeaders = [\"Content-Range\" <:> \"0-2/*\"] }\n\n    it \"matches nulls using not operator\" $\n      get \"/no_pk?a=not.is.null\" `shouldRespondWith`\n        [json| [{\"a\":\"1\",\"b\":\"0\"},{\"a\":\"2\",\"b\":\"0\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches not_null using is operator\" $\n      get \"/no_pk?a=is.not_null\" `shouldRespondWith`\n        [json| [{\"a\":\"1\",\"b\":\"0\"},{\"a\":\"2\",\"b\":\"0\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches nulls in varchar and numeric fields alike\" $ do\n      get \"/no_pk?a=is.null\" `shouldRespondWith`\n        [json| [{\"a\": null, \"b\": null}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"not.is.not_null is equivalent to is.null\" $ do\n      get \"/no_pk?a=not.is.not_null\" `shouldRespondWith`\n        [json| [{\"a\": null, \"b\": null}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/nullable_integer?a=is.null\" `shouldRespondWith` [json|[{\"a\":null}]|]\n\n    it \"matches with trilean values\" $ do\n      get \"/chores?done=is.true\" `shouldRespondWith`\n        [json| [{\"id\": 1, \"name\": \"take out the garbage\", \"done\": true }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/chores?done=is.false\" `shouldRespondWith`\n        [json| [{\"id\": 2, \"name\": \"do the laundry\", \"done\": false }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/chores?done=is.unknown\" `shouldRespondWith`\n        [json| [{\"id\": 3, \"name\": \"wash the dishes\", \"done\": null }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with null and not_null values in upper or mixed case\" $ do\n      get \"/chores?done=is.NULL\" `shouldRespondWith`\n        [json| [{\"id\": 3, \"name\": \"wash the dishes\", \"done\": null }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/chores?done=is.NoT_NuLl\" `shouldRespondWith`\n        [json| [{\"id\": 1, \"name\": \"take out the garbage\", \"done\": true }\n               ,{\"id\": 2, \"name\": \"do the laundry\", \"done\": false }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with trilean values in upper or mixed case\" $ do\n      get \"/chores?done=is.TRUE\" `shouldRespondWith`\n        [json| [{\"id\": 1, \"name\": \"take out the garbage\", \"done\": true }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/chores?done=is.FAlSe\" `shouldRespondWith`\n        [json| [{\"id\": 2, \"name\": \"do the laundry\", \"done\": false }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n      get \"/chores?done=is.UnKnOwN\" `shouldRespondWith`\n        [json| [{\"id\": 3, \"name\": \"wash the dishes\", \"done\": null }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"fails if 'is' used and there's no null or trilean value\" $ do\n      get \"/chores?done=is.nil\" `shouldRespondWith` 400\n      get \"/chores?done=is.ok\"  `shouldRespondWith` 400\n\n    it \"matches with like\" $ do\n      get \"/simple_pk?k=like.*yx\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"}]|]\n      get \"/simple_pk?k=like.xy*\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"}]|]\n      get \"/simple_pk?k=like.*YY*\" `shouldRespondWith`\n        [json|[{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n\n    it \"matches with like using not operator\" $\n      get \"/simple_pk?k=not.like.*yx\" `shouldRespondWith`\n        [json|[{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n\n    it \"matches with ilike\" $ do\n      get \"/simple_pk?k=ilike.xy*&order=extra.asc\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/simple_pk?k=ilike.*YY*&order=extra.asc\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with ilike using not operator\" $\n      get \"/simple_pk?k=not.ilike.xy*&order=extra.asc\" `shouldRespondWith` \"[]\"\n\n    it \"matches with ~\" $ do\n      get \"/simple_pk?k=match.yx$\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"}]|]\n      get \"/simple_pk?k=match.^xy\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"}]|]\n      get \"/simple_pk?k=match.YY\" `shouldRespondWith`\n        [json|[{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n\n    it \"matches with ~ using not operator\" $\n      get \"/simple_pk?k=not.match.yx$\" `shouldRespondWith`\n        [json|[{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n\n    it \"matches with ~*\" $ do\n      get \"/simple_pk?k=imatch.^xy&order=extra.asc\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/simple_pk?k=imatch..*YY.*&order=extra.asc\" `shouldRespondWith`\n        [json|[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with ~* using not operator\" $\n      get \"/simple_pk?k=not.imatch.^xy&order=extra.asc\" `shouldRespondWith` \"[]\"\n\n    describe \"Full text search operator\" $ do\n      context \"tsvector columns\" $ do\n        it \"finds matches with to_tsquery\" $\n          get \"/tsearch?text_search_vector=fts.impossible\" `shouldRespondWith`\n            [json| [{\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\" }] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can use lexeme boolean operators(&=%26, |=%7C, !) in to_tsquery\" $ do\n          get \"/tsearch?text_search_vector=fts.fun%26possible\" `shouldRespondWith`\n            [json| [ {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=fts.impossible%7Cpossible\" `shouldRespondWith`\n            [json| [\n            {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"},\n            {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=fts.fun%26!possible\" `shouldRespondWith`\n            [json| [ {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with plainto_tsquery\" $\n          get \"/tsearch?text_search_vector=plfts.The%20Fat%20Rats\" `shouldRespondWith`\n            [json| [ {\"text_search_vector\": \"'ate':3 'cat':2 'fat':1 'rat':4\" }] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with websearch_to_tsquery\" $\n            get \"/tsearch?text_search_vector=wfts.The%20Fat%20Rats\" `shouldRespondWith`\n                [json| [ {\"text_search_vector\": \"'ate':3 'cat':2 'fat':1 'rat':4\" }] |]\n                { matchHeaders = [matchContentTypeJson] }\n\n        it \"can use boolean operators(and, or, -) in websearch_to_tsquery\" $ do\n          get \"/tsearch?text_search_vector=wfts.fun%20and%20possible\"\n            `shouldRespondWith`\n              [json| [ {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"}] |]\n              { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=wfts.impossible%20or%20possible\"\n            `shouldRespondWith`\n              [json| [\n                {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"},\n                {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"}]\n                  |]\n              { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=wfts.fun%20and%20-possible\"\n            `shouldRespondWith`\n              [json| [ {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"}] |]\n              { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with different dictionaries\" $ do\n          get \"/tsearch?text_search_vector=fts(french).amusant\" `shouldRespondWith`\n            [json| [{\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" }] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=plfts(french).amusant%20impossible\" `shouldRespondWith`\n            [json| [{\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" }] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n          get \"/tsearch?text_search_vector=wfts(french).amusant%20impossible\"\n              `shouldRespondWith`\n                [json| [{\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" }] |]\n                { matchHeaders = [matchContentTypeJson] }\n\n        it \"can be negated with not operator\" $ do\n          get \"/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun\" `shouldRespondWith`\n            [json| [\n              {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\"},\n              {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=not.fts(english).impossible%7Cfat%7Cfun\" `shouldRespondWith`\n            [json| [\n              {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\"},\n              {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=not.plfts.The%20Fat%20Rats\" `shouldRespondWith`\n            [json| [\n              {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"},\n              {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"},\n              {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\"},\n              {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch?text_search_vector=not.wfts(english).impossible%20or%20fat%20or%20fun\"\n              `shouldRespondWith`\n                [json| [\n                  {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\"},\n                  {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}]|]\n                { matchHeaders = [matchContentTypeJson] }\n\n        context \"Use of the phraseto_tsquery function\" $ do\n          it \"finds matches\" $\n            get \"/tsearch?text_search_vector=phfts.The%20Fat%20Cats\" `shouldRespondWith`\n              [json| [{\"text_search_vector\": \"'ate':3 'cat':2 'fat':1 'rat':4\" }] |]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"finds matches with different dictionaries\" $\n            get \"/tsearch?text_search_vector=phfts(german).Art%20Spass\" `shouldRespondWith`\n              [json| [{\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\" }] |]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"can be negated with not operator\" $\n            get \"/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats\" `shouldRespondWith`\n              [json| [\n                {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\"},\n                {\"text_search_vector\": \"'also':2 'fun':3 'possibl':8\"},\n                {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\"},\n                {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}]|]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"can be used with or query param\" $\n            get \"/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)\" `shouldRespondWith`\n              [json|[\n                {\"text_search_vector\": \"'fun':5 'imposs':9 'kind':3\" },\n                {\"text_search_vector\": \"'amus':5 'fair':7 'impossibl':9 'peu':4\" },\n                {\"text_search_vector\": \"'art':4 'spass':5 'unmog':7\"}\n              ]|] { matchHeaders = [matchContentTypeJson] }\n\n          it \"works with tsvector computed fields\" $\n            get \"/tsearch_to_tsvector?select=text_search_vector&text_search_vector=fts(simple).of\" `shouldRespondWith`\n              [json| [\n                {\"text_search_vector\":\"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6\"}\n              ]|]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"works when the column type is a tsvector domain\" $ do\n            get \"tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).of\" `shouldRespondWith`\n              [json| [\n                {\"text_search_domain\":\"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6\"}\n              ]|]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"works when the column type is a recursive tsvector domain\" $ do\n            get \"tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).of\" `shouldRespondWith`\n              [json| [\n                {\"text_search_rec_domain\":\"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6\"}\n              ]|]\n              { matchHeaders = [matchContentTypeJson] }\n\n      context \"text and json columns\" $ do\n        it \"finds matches with to_tsquery\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=fts.impossible\" `shouldRespondWith`\n            [json| [\n              {\"text_search\": \"It's kind of fun to do the impossible\"},\n              {\"text_search\": \"C'est un peu amusant de faire l'impossible\"}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.impossible\" `shouldRespondWith`\n            [json| [\n              {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can use lexeme boolean operators(&=%26, |=%7C, !) in to_tsquery\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=fts.fun%26possible\" `shouldRespondWith`\n            [json| [{\"text_search\": \"But also fun to do what is possible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.fun%26possible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=fts.impossible%7Cpossible\"  `shouldRespondWith`\n            [json| [\n              {\"text_search\": \"It's kind of fun to do the impossible\"},\n              {\"text_search\": \"But also fun to do what is possible\"},\n              {\"text_search\": \"C'est un peu amusant de faire l'impossible\"}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.impossible%7Cpossible\" `shouldRespondWith`\n            [json| [\n              {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=fts.fun%26!possible\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"It's kind of fun to do the impossible\"}]|]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts.fun%26!possible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with plainto_tsquery\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=plfts.The%20Fat%20Rats\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"Fat cats ate rats\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=plfts.The%20Fat%20Rats\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"Fat cats ate rats\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with websearch_to_tsquery\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=wfts.The%20Fat%20Rats\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"Fat cats ate rats\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.The%20Fat%20Rats\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"Fat cats ate rats\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can use boolean operators(and, or, -) in websearch_to_tsquery\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=wfts.fun%20and%20possible\" `shouldRespondWith`\n            [json| [{\"text_search\": \"But also fun to do what is possible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.fun%20and%20possible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=wfts.impossible%20or%20possible\" `shouldRespondWith`\n            [json| [\n              {\"text_search\": \"It's kind of fun to do the impossible\"},\n              {\"text_search\": \"But also fun to do what is possible\"},\n              {\"text_search\": \"C'est un peu amusant de faire l'impossible\"}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.impossible%20or%20possible\" `shouldRespondWith`\n            [json| [\n              {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=wfts.fun%20and%20-possible\" `shouldRespondWith`\n            [json| [{\"text_search\": \"It's kind of fun to do the impossible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts.fun%20and%20-possible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"finds matches with different dictionaries and uses them as configuration for to_tsvector()\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=fts(french).amusant\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=fts(french).amusant\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=plfts(french).amusant%20impossible\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=plfts(french).amusant%20impossible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=wfts(french).amusant%20impossible\" `shouldRespondWith`\n            [json| [{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=wfts(french).amusant%20impossible\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can be negated with not operator\" $ do\n          get \"/tsearch_to_tsvector?select=text_search&text_search=not.fts.impossible%7Cfat%7Cfun\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.fts.impossible%7Cfat%7Cfun\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=not.fts(english).impossible%7Cfat%7Cfun\"  `shouldRespondWith`\n            [json| [{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.fts(english).impossible%7Cfat%7Cfun\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=not.plfts.The%20Fat%20Rats\"  `shouldRespondWith`\n            [json| [\n              {\"text_search\": \"It's kind of fun to do the impossible\"},\n              {\"text_search\": \"But also fun to do what is possible\"},\n              {\"text_search\": \"C'est un peu amusant de faire l'impossible\"},\n              {\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.plfts.The%20Fat%20Rats\" `shouldRespondWith`\n            [json| [\n              {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}},\n              {\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}]\n            |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=text_search&text_search=not.wfts(english).impossible%20or%20fat%20or%20fun\" `shouldRespondWith`\n            [json| [{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}] |]\n            { matchHeaders = [matchContentTypeJson] }\n          get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.wfts(english).impossible%20or%20fat%20or%20fun\" `shouldRespondWith`\n            [json| [{\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}] |]\n            { matchHeaders = [matchContentTypeJson] }\n\n        context \"Use of the phraseto_tsquery function\" $ do\n          it \"finds matches\" $ do\n            get \"/tsearch_to_tsvector?select=text_search&text_search=phfts.The%20Fat%20Cats\"  `shouldRespondWith`\n              [json| [{\"text_search\": \"Fat cats ate rats\"}] |]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=phfts.The%20Fat%20Cats\" `shouldRespondWith`\n              [json| [{\"jsonb_search\" :{\"text_search\": \"Fat cats ate rats\"}}] |]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"finds matches with different dictionaries and uses them as configuration for to_tsvector()\" $ do\n            get \"/tsearch_to_tsvector?select=text_search&text_search=phfts(german).Art%20Spass\"  `shouldRespondWith`\n              [json| [{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}] |]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=phfts(german).Art%20Spass\" `shouldRespondWith`\n              [json| [{\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}] |]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"can be negated with not operator\" $ do\n            get \"/tsearch_to_tsvector?select=text_search&text_search=not.phfts(english).The%20Fat%20Cats\"  `shouldRespondWith`\n              [json| [\n                {\"text_search\": \"It's kind of fun to do the impossible\"},\n                {\"text_search\": \"But also fun to do what is possible\"},\n                {\"text_search\": \"C'est un peu amusant de faire l'impossible\"},\n                {\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}]\n              |]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/tsearch_to_tsvector?select=jsonb_search&jsonb_search=not.phfts(english).The%20Fat%20Cats\" `shouldRespondWith`\n              [json| [\n                {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n                {\"jsonb_search\" :{\"text_search\": \"But also fun to do what is possible\"}},\n                {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}},\n                {\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}]\n              |]\n              { matchHeaders = [matchContentTypeJson] }\n\n          it \"can be used with or query param\" $ do\n            get \"/tsearch_to_tsvector?select=text_search&or=(text_search.phfts(german).Art%20Spass, text_search.phfts(french).amusant, text_search.fts(english).impossible)\"  `shouldRespondWith`\n              [json| [\n                {\"text_search\": \"It's kind of fun to do the impossible\"},\n                {\"text_search\": \"C'est un peu amusant de faire l'impossible\"},\n                {\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}]\n              |]\n              { matchHeaders = [matchContentTypeJson] }\n            get \"/tsearch_to_tsvector?select=jsonb_search&or=(jsonb_search.phfts(german).Art%20Spass, jsonb_search.phfts(french).amusant, jsonb_search.fts(english).impossible)\" `shouldRespondWith`\n              [json| [\n                {\"jsonb_search\" :{\"text_search\": \"It's kind of fun to do the impossible\"}},\n                {\"jsonb_search\" :{\"text_search\": \"C'est un peu amusant de faire l'impossible\"}},\n                {\"jsonb_search\" :{\"text_search\": \"Es ist eine Art Spaß, das Unmögliche zu machen\"}}]\n              |]\n              { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with computed column\" $\n      get \"/items?always_true=eq.true&order=id.asc\" `shouldRespondWith`\n        [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}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"order by computed column\" $\n      get \"/items?order=anti_id.desc\" `shouldRespondWith`\n        [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}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"cannot access a computed column that is outside of the config schema\" $\n      get \"/items?always_false=is.false\" `shouldRespondWith` 400\n\n    it \"matches filtering nested items\" $\n      get \"/clients?select=id,projects(id,tasks(id,name))&projects.tasks.name=like.Design*\" `shouldRespondWith`\n        [json|[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1,\"name\":\"Design w7\"}]},{\"id\":2,\"tasks\":[{\"id\":3,\"name\":\"Design w10\"}]}]},{\"id\":2,\"projects\":[{\"id\":3,\"tasks\":[{\"id\":5,\"name\":\"Design IOS\"}]},{\"id\":4,\"tasks\":[{\"id\":7,\"name\":\"Design OSX\"}]}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"errs when the embedded resource doesn't exist and an embedded filter is applied to it\" $ do\n      get \"/clients?select=*&non_existent_projects.name=like.*NonExistent*\" `shouldRespondWith`\n        [json|\n          {\"hint\":\"Verify that 'non_existent_projects' is included in the 'select' query parameter.\",\n           \"details\":null,\n           \"code\":\"PGRST108\",\n           \"message\":\"'non_existent_projects' is not an embedded resource in this request\"}|]\n        { matchStatus  = 400\n        , matchHeaders = [\n            \"Content-Length\" <:> \"204\",\n            matchContentTypeJson\n          ]\n        }\n      get \"/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*\" `shouldRespondWith`\n        [json|\n          {\"hint\":\"Verify that 'amiga_projectsss' is included in the 'select' query parameter.\",\n           \"details\":null,\n           \"code\":\"PGRST108\",\n           \"message\":\"'amiga_projectsss' is not an embedded resource in this request\"}|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/clients?select=id,projects(id,tasks(id,name))&projects.tasks2.name=like.Design*\" `shouldRespondWith`\n        [json|\n          {\"hint\":\"Verify that 'tasks2' is included in the 'select' query parameter.\",\n           \"details\":null,\n           \"code\":\"PGRST108\",\n           \"message\":\"'tasks2' is not an embedded resource in this request\"}|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"matches with cs operator\" $\n      get \"/complex_items?select=id&arr_data=cs.{2}\" `shouldRespondWith`\n        [json|[{\"id\":2},{\"id\":3}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with cd operator\" $\n      get \"/complex_items?select=id&arr_data=cd.{1,2,4}\" `shouldRespondWith`\n        [json|[{\"id\":1},{\"id\":2}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with IS DISTINCT FROM\" $\n      get \"/no_pk?select=a&a=isdistinct.2\" `shouldRespondWith`\n        [json|[{\"a\":null},{\"a\":\"1\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"matches with IS DISTINCT FROM using not operator\" $\n      get \"/no_pk?select=a&a=not.isdistinct.2\" `shouldRespondWith`\n        [json|[{\"a\":\"2\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  describe \"Shaping response with select parameter\" $ do\n    it \"selectStar works in absense of parameter\" $\n      get \"/complex_items?id=eq.3\" `shouldRespondWith`\n        [json|[{\"id\":3,\"name\":\"Three\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}},\"arr_data\":[1,2,3],\"field-with_sep\":3}]|]\n\n    it \"dash `-` in column names is accepted\" $\n      get \"/complex_items?id=eq.3&select=id,field-with_sep\" `shouldRespondWith`\n        [json|[{\"id\":3,\"field-with_sep\":3}]|]\n\n    it \"one simple column\" $\n      get \"/complex_items?select=id\" `shouldRespondWith`\n        [json| [{\"id\":1},{\"id\":2},{\"id\":3}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"rename simple column\" $\n      get \"/complex_items?id=eq.1&select=myId:id\" `shouldRespondWith`\n        [json| [{\"myId\":1}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"one simple column with casting (text)\" $\n      get \"/complex_items?select=id::text\" `shouldRespondWith`\n        [json| [{\"id\":\"1\"},{\"id\":\"2\"},{\"id\":\"3\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"rename simple column with casting\" $\n      get \"/complex_items?id=eq.1&select=myId:id::text\" `shouldRespondWith`\n        [json| [{\"myId\":\"1\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"json column\" $\n      get \"/complex_items?id=eq.1&select=settings\" `shouldRespondWith`\n        [json| [{\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"fails on bad casting (wrong cast type)\" $\n      get \"/complex_items?select=id::fakecolumntype\"\n        `shouldRespondWith` [json| {\"hint\":null,\"details\":null,\"code\":\"42704\",\"message\":\"type \\\"fakecolumntype\\\" does not exist\"} |]\n        { matchStatus  = 400\n        , matchHeaders = [\"Content-Length\" <:> \"94\"]\n        }\n\n    it \"can cast types with underscore and numbers\" $\n      get \"/oid_test?select=id,oid_col::int,oid_array_col::_int4\"\n        `shouldRespondWith` [json|\n          [{\"id\":1,\"oid_col\":12345,\"oid_array_col\":[1,2,3,4,5]}]\n        |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting parents and children\" $\n      get \"/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]|]\n        { matchHeaders = [\n            \"Content-Length\" <:> \"141\",\n            matchContentTypeJson\n          ]\n        }\n\n    it \"requesting parent and renaming primary key\" $\n      get \"/projects?select=name,client:clients(clientId:id,name)\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Windows 7\",\"client\":{\"name\": \"Microsoft\", \"clientId\": 1}},\n          {\"name\":\"Windows 10\",\"client\":{\"name\": \"Microsoft\", \"clientId\": 1}},\n          {\"name\":\"IOS\",\"client\":{\"name\": \"Apple\", \"clientId\": 2}},\n          {\"name\":\"OSX\",\"client\":{\"name\": \"Apple\", \"clientId\": 2}},\n          {\"name\":\"Orphan\",\"client\":null}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting parent and specifying/renaming one key of the composite primary key\" $ do\n      get \"/comments?select=*,users_tasks(userId:user_id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"commenter_id\":1,\"user_id\":2,\"task_id\":6,\"content\":\"Needs to be delivered ASAP\",\"users_tasks\":{\"userId\": 2}}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/comments?select=*,users_tasks(taskId:task_id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"commenter_id\":1,\"user_id\":2,\"task_id\":6,\"content\":\"Needs to be delivered ASAP\",\"users_tasks\":{\"taskId\": 6}}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting parents and children while renaming them\" $\n      get \"/projects?id=eq.1&select=myId:id, name, project_client:clients(*), project_tasks:tasks(id, name)\" `shouldRespondWith`\n        [json|[{\"myId\":1,\"name\":\"Windows 7\",\"project_client\":{\"id\":1,\"name\":\"Microsoft\"},\"project_tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting parents and filtering parent columns\" $\n      get \"/projects?id=eq.1&select=id, name, clients(id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1}}]|]\n\n    it \"rows with missing parents are included\" $\n      get \"/projects?id=in.(1,5)&select=id,clients(id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"clients\":{\"id\":1}},{\"id\":5,\"clients\":null}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"rows with no children return [] instead of null\" $\n      get \"/projects?id=in.(5)&select=id,tasks(id)\" `shouldRespondWith`\n        [json|[{\"id\":5,\"tasks\":[]}]|]\n\n    it \"requesting children 2 levels\" $\n      get \"/clients?id=eq.1&select=id,projects(id,tasks(id))\" `shouldRespondWith`\n        [json|[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2}]},{\"id\":2,\"tasks\":[{\"id\":3},{\"id\":4}]}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting many<->many relation\" $\n      get \"/tasks?select=id,users(id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"users\":[{\"id\":1},{\"id\":3}]},{\"id\":2,\"users\":[{\"id\":1}]},{\"id\":3,\"users\":[{\"id\":1}]},{\"id\":4,\"users\":[{\"id\":1}]},{\"id\":5,\"users\":[{\"id\":2},{\"id\":3}]},{\"id\":6,\"users\":[{\"id\":2}]},{\"id\":7,\"users\":[{\"id\":2}]},{\"id\":8,\"users\":[]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting many<->many relation with rename\" $\n      get \"/tasks?id=eq.1&select=id,theUsers:users(id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"theUsers\":[{\"id\":1},{\"id\":3}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting many<->many relation reverse\" $\n      get \"/users?select=id,tasks(id)\" `shouldRespondWith`\n        [json|[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4}]},{\"id\":2,\"tasks\":[{\"id\":5},{\"id\":6},{\"id\":7}]},{\"id\":3,\"tasks\":[{\"id\":1},{\"id\":5}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting many<->many relation using composite key\" $\n      get \"/files?filename=eq.autoexec.bat&project_id=eq.1&select=filename,users_tasks(user_id,task_id)\" `shouldRespondWith`\n        [json|[{\"filename\":\"autoexec.bat\",\"users_tasks\":[{\"user_id\":1,\"task_id\":1},{\"user_id\":3,\"task_id\":1}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting data using many<->many relation defined by composite keys\" $\n      get \"/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files(filename,content)\" `shouldRespondWith`\n        [json|[{\"user_id\":1,\"files\":[{\"filename\":\"autoexec.bat\",\"content\":\"@ECHO OFF\"},{\"filename\":\"command.com\",\"content\":\"#include <unix.h>\"},{\"filename\":\"README.md\",\"content\":\"# make $$$!\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting data using many<->many (composite keys) relation using hint\" $\n      get \"/users_tasks?user_id=eq.1&task_id=eq.1&select=user_id,files!touched_files(filename,content)\" `shouldRespondWith`\n        [json|[{\"user_id\":1,\"files\":[{\"filename\":\"autoexec.bat\",\"content\":\"@ECHO OFF\"},{\"filename\":\"command.com\",\"content\":\"#include <unix.h>\"},{\"filename\":\"README.md\",\"content\":\"# make $$$!\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"requesting children with composite key\" $\n      get \"/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)\" `shouldRespondWith`\n        [json|[{\"user_id\":2,\"task_id\":6,\"comments\":[{\"content\":\"Needs to be delivered ASAP\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    -- https://github.com/PostgREST/postgrest/issues/2070\n    it \"one-to-many embeds without a disambiguation error due to wrongly generated many-to-many relationships\" $\n      get \"/plate?select=*,well(*)\" `shouldRespondWith`\n        [json|[]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    context \"one to one relationships\" $ do\n      it \"works when having a pk as fk\" $ do\n        get \"/students_info?select=address,students(name)\" `shouldRespondWith`\n          [json|[{\"address\":\"Street 1\",\"students\":{\"name\":\"John Doe\"}}, {\"address\":\"Street 2\",\"students\":{\"name\":\"Jane Doe\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/students?select=name,students_info(address)\" `shouldRespondWith`\n          [json|[{\"name\":\"John Doe\",\"students_info\":{\"address\":\"Street 1\"}},{\"name\":\"Jane Doe\",\"students_info\":{\"address\":\"Street 2\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when having a fk with a unique constraint\" $ do\n        get \"/country?select=name,capital(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Afghanistan\",\"capital\":{\"name\":\"Kabul\"}}, {\"name\":\"Algeria\",\"capital\":{\"name\":\"Algiers\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/capital?select=name,country(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Kabul\",\"country\":{\"name\":\"Afghanistan\"}}, {\"name\":\"Algiers\",\"country\":{\"name\":\"Algeria\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when using column as target\" $ do\n        get \"/capital?select=name,country_id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Kabul\",\"country_id\":{\"name\":\"Afghanistan\"}}, {\"name\":\"Algiers\",\"country_id\":{\"name\":\"Algeria\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/capital?select=name,capital_country_id_fkey(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Kabul\",\"capital_country_id_fkey\":{\"name\":\"Afghanistan\"}}, {\"name\":\"Algiers\",\"capital_country_id_fkey\":{\"name\":\"Algeria\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/country?select=name,capital_country_id_fkey(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Afghanistan\",\"capital_country_id_fkey\":{\"name\":\"Kabul\"}}, {\"name\":\"Algeria\",\"capital_country_id_fkey\":{\"name\":\"Algiers\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/country?select=name,id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Afghanistan\",\"id\":{\"name\":\"Kabul\"}}, {\"name\":\"Algeria\",\"id\":{\"name\":\"Algiers\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when using column as hint\" $ do\n        get \"/country?select=name,capital!id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Afghanistan\",\"capital\":{\"name\":\"Kabul\"}}, {\"name\":\"Algeria\",\"capital\":{\"name\":\"Algiers\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/country?select=name,capital!country_id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Afghanistan\",\"capital\":{\"name\":\"Kabul\"}}, {\"name\":\"Algeria\",\"capital\":{\"name\":\"Algiers\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/capital?select=name,country!id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Kabul\",\"country\":{\"name\":\"Afghanistan\"}}, {\"name\":\"Algiers\",\"country\":{\"name\":\"Algeria\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/capital?select=name,country!country_id(name)\" `shouldRespondWith`\n          [json|[{\"name\":\"Kabul\",\"country\":{\"name\":\"Afghanistan\"}}, {\"name\":\"Algiers\",\"country\":{\"name\":\"Algeria\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    describe \"computed columns\" $ do\n      it \"computed column on table\" $\n        get \"/items?id=eq.1&select=id,always_true\" `shouldRespondWith`\n          [json|[{\"id\":1,\"always_true\":true}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"computed column on rpc\" $\n        get \"/rpc/search?id=1&select=id,always_true\" `shouldRespondWith`\n          [json|[{\"id\":1,\"always_true\":true}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"overloaded computed columns on both tables\" $ do\n        get \"/items?id=eq.1&select=id,computed_overload\" `shouldRespondWith`\n          [json|[{\"id\":1,\"computed_overload\":true}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/items2?id=eq.1&select=id,computed_overload\" `shouldRespondWith`\n          [json|[{\"id\":1,\"computed_overload\":true}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"overloaded computed column on rpc\" $\n        get \"/rpc/search?id=1&select=id,computed_overload\" `shouldRespondWith`\n          [json|[{\"id\":1,\"computed_overload\":true}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    describe \"partitioned tables embedding\" $ do\n      it \"can request a table as parent from a partitioned table\" $\n        get \"/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"DeLorean\",\"year\":1981,\"car_brands\":{\"name\":\"DMC\"}},\n             {\"name\":\"Murcielago\",\"year\":2001,\"car_brands\":{\"name\":\"Lamborghini\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request partitioned tables as children from a table\" $\n        get \"/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"DMC\",\"car_models\":[{\"name\":\"DeLorean\",\"year\":1981}]},\n             {\"name\":\"Ferrari\",\"car_models\":[{\"name\":\"F310-B\",\"year\":1997}]},\n             {\"name\":\"Lamborghini\",\"car_models\":[{\"name\":\"Murcielago\",\"year\":2001},{\"name\":\"Veneno\",\"year\":2013}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request tables as children from a partitioned table\" $\n        get \"/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"DeLorean\",\"year\":1981,\"car_racers\":[]},\n             {\"name\":\"F310-B\",\"year\":1997,\"car_racers\":[{\"name\":\"Michael Schumacher\"}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request a partitioned table as parent from a table\" $\n        get \"/car_racers?select=name,car_models(name,year)&order=name.asc\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"Alain Prost\",\"car_models\":null},\n             {\"name\":\"Michael Schumacher\",\"car_models\":{\"name\":\"F310-B\",\"year\":1997}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request partitioned tables as children from a partitioned table\" $\n        get \"/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"DeLorean\",\"year\":1981,\"car_model_sales\":[{\"date\":\"2021-01-14\",\"quantity\":7},{\"date\":\"2021-01-15\",\"quantity\":9}]},\n             {\"name\":\"Murcielago\",\"year\":2001,\"car_model_sales\":[{\"date\":\"2021-02-11\",\"quantity\":1},{\"date\":\"2021-02-12\",\"quantity\":3}]},\n             {\"name\":\"Veneno\",\"year\":2013,\"car_model_sales\":[]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request a partitioned table as parent from a partitioned table\" $ do\n        get \"/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc\" `shouldRespondWith`\n          [json|\n            [{\"date\":\"2021-01-15\",\"quantity\":9,\"car_models\":{\"name\":\"DeLorean\",\"year\":1981}},\n             {\"date\":\"2021-02-11\",\"quantity\":1,\"car_models\":{\"name\":\"Murcielago\",\"year\":2001}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can request many to many relationships between partitioned tables ignoring the intermediate table partitions\" $\n        get \"/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4\" `shouldRespondWith`\n          [json|\n            [{\"name\":\"DeLorean\",\"year\":1981,\"car_dealers\":[{\"name\":\"Springfield Cars S.A.\",\"city\":\"Springfield\"}]},\n             {\"name\":\"F310-B\",\"year\":1997,\"car_dealers\":[]},\n             {\"name\":\"Murcielago\",\"year\":2001,\"car_dealers\":[{\"name\":\"The Best Deals S.A.\",\"city\":\"Franklin\"}]},\n             {\"name\":\"Veneno\",\"year\":2013,\"car_dealers\":[]}] |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"cannot request partitions as children from a partitioned table\" $\n        get \"/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc\" `shouldRespondWith`\n          [json|\n            {\"hint\":\"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.\",\n             \"details\":\"Searched for a foreign key relationship between 'car_models' and 'car_model_sales_202101' in the schema 'test', but no matches were found.\",\n             \"code\":\"PGRST200\",\n             \"message\":\"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache\"} |]\n          { matchStatus  = 400\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      -- we only search for foreign key relationships after checking the\n      -- the existence of first table, #3869\n      it \"table not found error if first table does not exist\" $\n        get \"/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc\" `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.car_model_sales_202101' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"cannot request a partition as parent from a partitioned table\" $\n        get \"/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc\" `shouldRespondWith`\n          [json|\n            {\"hint\":\"Perhaps you meant 'car_models' instead of 'car_models_default'.\",\n             \"details\":\"Searched for a foreign key relationship between 'car_model_sales' and 'car_models_default' in the schema 'test', but no matches were found.\",\n             \"code\":\"PGRST200\",\n             \"message\":\"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache\"} |]\n          { matchStatus  = 400\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"table not found error if first table does not exist\" $\n        get \"/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc\" `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.car_models_default' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    describe \"view embedding\" $ do\n      it \"can detect fk relations through views to tables in the public schema\" $\n        get \"/consumers_view?select=*,orders_view(*)\" `shouldRespondWith` 200\n\n      it \"can detect fk relations through materialized views to tables in the public schema\" $\n        get \"/materialized_projects?select=*,users(*)\" `shouldRespondWith` 200\n\n      it \"can request two parents\" $\n        get \"/articleStars?select=createdAt,article:articles(id),user:users(name)&limit=1\"\n          `shouldRespondWith`\n            [json|[{\"createdAt\":\"2015-12-08T04:22:57.472738\",\"article\":{\"id\": 1},\"user\":{\"name\": \"Angela Martin\"}}]|]\n\n      it \"can detect relations in views from exposed schema that are based on tables in private schema and have columns renames\" $\n        get \"/articles?id=eq.1&select=id,articleStars(users(*))\" `shouldRespondWith`\n          [json|[{\"id\":1,\"articleStars\":[{\"users\":{\"id\":1,\"name\":\"Angela Martin\"}},{\"users\":{\"id\":2,\"name\":\"Michael Scott\"}},{\"users\":{\"id\":3,\"name\":\"Dwight Schrute\"}}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when requesting parents and children on views\" $\n        get \"/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)\" `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when requesting parents and children on views with renamed keys\" $\n        get \"/projects_view_alt?t_id=eq.1&select=t_id, name, clients(*), tasks(id, name)\" `shouldRespondWith`\n          [json|[{\"t_id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"detects parent relations when having many views of a private table\" $ do\n        get \"/books?select=title,author:authors(name)&id=eq.5\" `shouldRespondWith`\n          [json|[ { \"title\": \"Farenheit 451\", \"author\": { \"name\": \"Ray Bradbury\" } } ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/forties_books?select=title,author:authors(name)&limit=1\" `shouldRespondWith`\n          [json|[ { \"title\": \"1984\", \"author\": { \"name\": \"George Orwell\" } } ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/fifties_books?select=title,author:authors(name)&limit=1\" `shouldRespondWith`\n          [json|[ { \"title\": \"The Catcher in the Rye\", \"author\": { \"name\": \"J.D. Salinger\" } } ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/sixties_books?select=title,author:authors(name)&limit=1\" `shouldRespondWith`\n          [json|[ { \"title\": \"To Kill a Mockingbird\", \"author\": { \"name\": \"Harper Lee\" } } ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can detect fk relations through multiple views recursively when all views are in api schema\" $ do\n        get \"/consumers_view_view?select=*,orders_view(*)\" `shouldRespondWith` 200\n\n      it \"works with views that have subselects\" $\n        get \"/authors_books_number?select=*,books(title)&id=eq.1\" `shouldRespondWith`\n          [json|[ {\"id\":1, \"name\":\"George Orwell\",\"num_in_forties\":1,\"num_in_fifties\":0,\"num_in_sixties\":0,\"num_in_all_decades\":1,\n                   \"books\":[{\"title\":\"1984\"}]} ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with views that have case subselects\" $\n        get \"/authors_have_book_in_decade?select=*,books(title)&id=eq.3\" `shouldRespondWith`\n          [json|[ {\"id\":3,\"name\":\"Antoine de Saint-Exupéry\",\"has_book_in_forties\":true,\"has_book_in_fifties\":false,\"has_book_in_sixties\":false,\n                   \"books\":[{\"title\":\"The Little Prince\"}]} ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with views that have subselect in the FROM clause\" $\n        get \"/forties_and_fifties_books?select=title,first_publisher,author:authors(name)&id=eq.1\" `shouldRespondWith`\n          [json|[{\"title\":\"1984\",\"first_publisher\":\"Secker & Warburg\",\"author\":{\"name\":\"George Orwell\"}}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with views that have subselects in a function call\" $\n        get \"/authors_have_book_in_decade2?select=*,books(title)&id=eq.3\"\n          `shouldRespondWith`\n            [json|[ {\"id\":3,\"name\":\"Antoine de Saint-Exupéry\",\"has_book_in_forties\":true,\"has_book_in_fifties\":false,\n                     \"has_book_in_sixties\":false,\"books\":[{\"title\":\"The Little Prince\"}]} ]|]\n\n      it \"works with views that have CTE\" $\n        get \"/odd_years_publications?select=title,publication_year,first_publisher,author:authors(name)&id=in.(1,2,3)\" `shouldRespondWith`\n          [json|[\n            {\"title\":\"1984\",\"publication_year\":1949,\"first_publisher\":\"Secker & Warburg\",\"author\":{\"name\":\"George Orwell\"}},\n            {\"title\":\"The Diary of a Young Girl\",\"publication_year\":1947,\"first_publisher\":\"Contact Publishing\",\"author\":{\"name\":\"Anne Frank\"}},\n            {\"title\":\"The Little Prince\",\"publication_year\":1947,\"first_publisher\":\"Reynal & Hitchcock\",\"author\":{\"name\":\"Antoine de Saint-Exupéry\"}} ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when having a capitalized table name and camelCase fk column\" $\n        get \"/foos?select=*,bars(*)\" `shouldRespondWith` 200\n\n      it \"works when embedding a view with a table that has a long compound pk\" $ do\n        get \"/player_view?select=id,contract(purchase_price)&id=in.(1,3,5,7)\" `shouldRespondWith`\n          [json|\n            [{\"id\":1,\"contract\":[{\"purchase_price\":10}]},\n             {\"id\":3,\"contract\":[{\"purchase_price\":30}]},\n             {\"id\":5,\"contract\":[{\"purchase_price\":50}]},\n             {\"id\":7,\"contract\":[]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/contract?select=tournament,player_view(first_name)&limit=3\" `shouldRespondWith`\n          [json|\n            [{\"tournament\":\"tournament_1\",\"player_view\":{\"first_name\":\"first_name_1\"}},\n             {\"tournament\":\"tournament_2\",\"player_view\":{\"first_name\":\"first_name_2\"}},\n             {\"tournament\":\"tournament_3\",\"player_view\":{\"first_name\":\"first_name_3\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when embedding a view with a view that referes to a table that has a long compound pk\" $ do\n        get \"/player_view?select=id,contract_view(purchase_price)&id=in.(1,3,5,7)\" `shouldRespondWith`\n          [json|\n            [{\"id\":1,\"contract_view\":[{\"purchase_price\":10}]},\n             {\"id\":3,\"contract_view\":[{\"purchase_price\":30}]},\n             {\"id\":5,\"contract_view\":[{\"purchase_price\":50}]},\n             {\"id\":7,\"contract_view\":[]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/contract_view?select=tournament,player_view(first_name)&limit=3\" `shouldRespondWith`\n          [json|\n            [{\"tournament\":\"tournament_1\",\"player_view\":{\"first_name\":\"first_name_1\"}},\n             {\"tournament\":\"tournament_2\",\"player_view\":{\"first_name\":\"first_name_2\"}},\n             {\"tournament\":\"tournament_3\",\"player_view\":{\"first_name\":\"first_name_3\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works when embedding two views that refer to tables with different column ordering\" $\n        get \"/v1?select=v2(*)\" `shouldRespondWith` 200\n\n      it \"can embed a view that has group by\" $\n        get \"/projects_count_grouped_by?select=number_of_projects,client:clients(name)&order=number_of_projects\" `shouldRespondWith`\n          [json|\n            [{\"number_of_projects\":1,\"client\":null},\n             {\"number_of_projects\":2,\"client\":{\"name\":\"Microsoft\"}},\n             {\"number_of_projects\":2,\"client\":{\"name\":\"Apple\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can embed a view that has a subselect containing a select in a where\" $\n        get \"/authors_w_entities?select=name,entities,books(title)&id=eq.1\" `shouldRespondWith`\n          [json| [{\"name\":\"George Orwell\",\"entities\":[3, 4],\"books\":[{\"title\":\"1984\"}]}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with one to one relationships\" $ do\n        get \"/students_view?select=name,students_info(address)\" `shouldRespondWith`\n          [json| [{\"name\":\"John Doe\",\"students_info\":{\"address\":\"Street 1\"}}, {\"name\":\"Jane Doe\",\"students_info\":{\"address\":\"Street 2\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/students_view?select=name,students_info_view(address)\" `shouldRespondWith`\n          [json| [{\"name\":\"John Doe\",\"students_info_view\":{\"address\":\"Street 1\"}}, {\"name\":\"Jane Doe\",\"students_info_view\":{\"address\":\"Street 2\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/students_info_view?select=address,students(name)\" `shouldRespondWith`\n          [json| [{\"address\":\"Street 1\",\"students\":{\"name\":\"John Doe\"}}, {\"address\":\"Street 2\",\"students\":{\"name\":\"Jane Doe\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/students_info_view?select=address,students_view(name)\" `shouldRespondWith`\n          [json| [{\"address\":\"Street 1\",\"students_view\":{\"name\":\"John Doe\"}}, {\"address\":\"Street 2\",\"students_view\":{\"name\":\"Jane Doe\"}}] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n    describe \"aliased embeds\" $ do\n      it \"works with child relation\" $\n        get \"/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3\" `shouldRespondWith`\n          [json|[\n            { \"id\":1,\n              \"zones\": [ {\"id\":1,\"name\":\"zone 1\"}, {\"id\":2,\"name\":\"zone 2\"}],\n              \"stores\": [ {\"id\":3,\"name\":\"store 3\"}, {\"id\":4,\"name\":\"store 4\"}]}\n          ]|] { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with many to many relation\" $\n        get \"/users?select=id,designTasks:tasks(id,name),codeTasks:tasks(id,name)&designTasks.name=like.*Design*&codeTasks.name=like.*Code*\" `shouldRespondWith`\n          [json|[\n             { \"id\":1,\n               \"designTasks\":[ { \"id\":1, \"name\":\"Design w7\" }, { \"id\":3, \"name\":\"Design w10\" } ],\n               \"codeTasks\":[ { \"id\":2, \"name\":\"Code w7\" }, { \"id\":4, \"name\":\"Code w10\" } ] },\n             { \"id\":2,\n               \"designTasks\":[ { \"id\":5, \"name\":\"Design IOS\" }, { \"id\":7, \"name\":\"Design OSX\" } ],\n               \"codeTasks\":[ { \"id\":6, \"name\":\"Code IOS\" } ] },\n             { \"id\":3,\n               \"designTasks\":[ { \"id\":1, \"name\":\"Design w7\" }, { \"id\":5, \"name\":\"Design IOS\" } ],\n               \"codeTasks\":[ ] }\n          ]|] { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with an aliased child plus non aliased child\" $\n        get \"/projects?select=id,name,designTasks:tasks(name,users(id,name))&designTasks.name=like.*Design*&designTasks.users.id=in.(1,2)\" `shouldRespondWith`\n          [json|[\n            {\n              \"id\":1, \"name\":\"Windows 7\",\n              \"designTasks\":[ { \"name\":\"Design w7\", \"users\":[ { \"id\":1, \"name\":\"Angela Martin\" } ] } ] },\n            {\n              \"id\":2, \"name\":\"Windows 10\",\n              \"designTasks\":[ { \"name\":\"Design w10\", \"users\":[ { \"id\":1, \"name\":\"Angela Martin\" } ] } ] },\n            {\n              \"id\":3, \"name\":\"IOS\",\n              \"designTasks\":[ { \"name\":\"Design IOS\", \"users\":[ { \"id\":2, \"name\":\"Michael Scott\" } ] } ] },\n            {\n              \"id\":4, \"name\":\"OSX\",\n              \"designTasks\":[ { \"name\":\"Design OSX\", \"users\":[ { \"id\":2, \"name\":\"Michael Scott\" } ] } ] },\n            {\n              \"id\":5, \"name\":\"Orphan\",\n              \"designTasks\":[ ] }\n          ]|] { matchHeaders = [matchContentTypeJson] }\n\n      it \"works with two aliased children embeds plus and/or\" $\n        get \"/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)\" `shouldRespondWith`\n          [json|[\n            { \"id\":1,\n              \"children\":[\n                {\"id\":1,\"gChildren\":[{\"id\":1}, {\"id\":2}]},\n                {\"id\":2,\"gChildren\":[]}]},\n            { \"id\":2,\n              \"children\":[\n                {\"id\":3,\"gChildren\":[]}]},\n            { \"id\":3,\"children\":[]},\n            { \"id\":4,\"children\":[]}\n          ]|] { matchHeaders = [matchContentTypeJson] }\n\n  describe \"ordering response\" $ do\n    it \"by a column asc\" $\n      get \"/items?id=lte.2&order=id.asc\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":2}] |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-1/*\"]\n        }\n\n\n    it \"by a column desc\" $\n      get \"/items?id=lte.2&order=id.desc\"\n        `shouldRespondWith` [json| [{\"id\":2},{\"id\":1}] |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-1/*\"]\n        }\n\n    it \"by a column with nulls first\" $\n      get \"/no_pk?order=a.nullsfirst\"\n        `shouldRespondWith` [json| [{\"a\":null,\"b\":null},\n                              {\"a\":\"1\",\"b\":\"0\"},\n                              {\"a\":\"2\",\"b\":\"0\"}\n                              ] |]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-2/*\"]\n        }\n\n    it \"by a column asc with nulls last\" $\n      get \"/no_pk?order=a.asc.nullslast\"\n        `shouldRespondWith` [json| [{\"a\":\"1\",\"b\":\"0\"},\n                              {\"a\":\"2\",\"b\":\"0\"},\n                              {\"a\":null,\"b\":null}] |]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-2/*\"]\n        }\n\n    it \"by a column desc with nulls first\" $\n      get \"/no_pk?order=a.desc.nullsfirst\"\n        `shouldRespondWith` [json| [{\"a\":null,\"b\":null},\n                              {\"a\":\"2\",\"b\":\"0\"},\n                              {\"a\":\"1\",\"b\":\"0\"}] |]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-2/*\"]\n        }\n\n    it \"by a column desc with nulls last\" $\n      get \"/no_pk?order=a.desc.nullslast\"\n        `shouldRespondWith` [json| [{\"a\":\"2\",\"b\":\"0\"},\n                              {\"a\":\"1\",\"b\":\"0\"},\n                              {\"a\":null,\"b\":null}] |]\n        { matchStatus = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-2/*\"]\n        }\n\n    it \"by two columns with nulls and direction specified\" $\n      get \"/projects?select=client_id,id,name&order=client_id.desc.nullslast,id.desc\"\n        `shouldRespondWith` [json|\n          [{\"client_id\":2,\"id\":4,\"name\":\"OSX\"},\n           {\"client_id\":2,\"id\":3,\"name\":\"IOS\"},\n           {\"client_id\":1,\"id\":2,\"name\":\"Windows 10\"},\n           {\"client_id\":1,\"id\":1,\"name\":\"Windows 7\"},\n           {\"client_id\":null,\"id\":5,\"name\":\"Orphan\"}]\n        |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-4/*\"]\n        }\n\n    it \"by a column with no direction or nulls specified\" $\n      get \"/items?id=lte.2&order=id\"\n        `shouldRespondWith` [json| [{\"id\":1},{\"id\":2}] |]\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Range\" <:> \"0-1/*\"]\n        }\n\n    it \"without other constraints\" $\n      get \"/items?order=id.asc\" `shouldRespondWith` 200\n\n    it \"ordering embeded entities\" $\n      get \"/projects?id=eq.1&select=id, name, tasks(id, name)&tasks.order=name.asc\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"tasks\":[{\"id\":2,\"name\":\"Code w7\"},{\"id\":1,\"name\":\"Design w7\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"ordering embeded entities with alias\" $\n      get \"/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.order=name.asc\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"the_tasks\":[{\"id\":2,\"name\":\"Code w7\"},{\"id\":1,\"name\":\"Design w7\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"ordering embeded entities, two levels\" $\n      get \"/projects?id=eq.1&select=id, name, tasks(id, name, users(id, name))&tasks.order=name.asc&tasks.users.order=name.desc\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"tasks\":[{\"id\":2,\"name\":\"Code w7\",\"users\":[{\"id\":1,\"name\":\"Angela Martin\"}]},{\"id\":1,\"name\":\"Design w7\",\"users\":[{\"id\":3,\"name\":\"Dwight Schrute\"},{\"id\":1,\"name\":\"Angela Martin\"}]}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"ordering embeded parents does not break things\" $\n      get \"/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"}}]|]\n\n    it \"gives meaningful error message on bad syntax\" $ do\n      get \"/items?order=id.asc.nullslasttt\" `shouldRespondWith`\n        [json|{\"details\":\"unexpected 't' expecting \\\",\\\" or end of input\",\"message\":\"\\\"failed to parse order (id.asc.nullslasttt)\\\" (line 1, column 17)\",\"code\":\"PGRST100\",\"hint\":null}|]\n        { matchStatus  = 400\n        , matchHeaders = [\n            \"Content-Length\" <:> \"169\",\n            matchContentTypeJson\n          ]\n        }\n\n  describe \"Accept headers\" $ do\n    it \"should respond an unknown accept type with 406\" $\n      request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/unknowntype\") \"\"\n        `shouldRespondWith`\n        [json|{\"message\":\"None of these media types are available: text/unknowntype\",\"code\":\"PGRST107\",\"details\":null,\"hint\":null}|]\n        { matchStatus  = 406\n        , matchHeaders = [\n            \"Content-Length\" <:> \"116\",\n            matchContentTypeJson\n          ]\n        }\n\n    it \"should respond correctly to */* in accept header\" $\n      request methodGet \"/simple_pk\"\n              (acceptHdrs \"*/*\") \"\"\n        `shouldRespondWith` 200\n\n    it \"*/* should rescue an unknown type\" $\n      request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/unknowntype, */*\") \"\"\n        `shouldRespondWith` 200\n\n    it \"specific available preference should override */*\" $ do\n      r <- request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/csv, */*\") \"\"\n      liftIO $ do\n        let respHeaders = simpleHeaders r\n        respHeaders `shouldSatisfy` matchHeader\n          \"Content-Type\" \"text/csv; charset=utf-8\"\n\n    it \"honors client preference even when opposite of server preference\" $ do\n      r <- request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/csv, application/json\") \"\"\n      liftIO $ do\n        let respHeaders = simpleHeaders r\n        respHeaders `shouldSatisfy` matchHeader\n          \"Content-Type\" \"text/csv; charset=utf-8\"\n\n    it \"should respond correctly to multiple types in accept header\" $\n      request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/unknowntype, text/csv\") \"\"\n        `shouldRespondWith` 200\n\n    it \"should respond with CSV to 'text/csv' request\" $\n      request methodGet \"/simple_pk\"\n              (acceptHdrs \"text/csv; version=1\") \"\"\n        `shouldRespondWith` \"k,extra\\nxyyx,u\\nxYYx,v\"\n        { matchStatus  = 200\n        , matchHeaders = [\n            \"Content-Type\" <:> \"text/csv; charset=utf-8\",\n            \"Content-Length\" <:> \"21\"\n          ]\n        }\n\n  describe \"Canonical location\" $ do\n    it \"Sets Content-Location with alphabetized params\" $\n      get \"/no_pk?b=eq.1&a=eq.1\"\n        `shouldRespondWith` \"[]\"\n        { matchStatus  = 200\n        , matchHeaders = [\"Content-Location\" <:> \"/no_pk?a=eq.1&b=eq.1\"]\n        }\n\n    it \"Omits question mark when there are no params\" $ do\n      r <- get \"/simple_pk\"\n      liftIO $ do\n        let respHeaders = simpleHeaders r\n        respHeaders `shouldSatisfy` matchHeader\n          \"Content-Location\" \"/simple_pk\"\n\n  describe \"weird requests\" $ do\n    it \"can query as normal\" $ do\n      get \"/Escap3e;\" `shouldRespondWith`\n        [json| [{\"so6meIdColumn\":1},{\"so6meIdColumn\":2},{\"so6meIdColumn\":3},{\"so6meIdColumn\":4},{\"so6meIdColumn\":5}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/ghostBusters\" `shouldRespondWith`\n        [json| [{\"escapeId\":1},{\"escapeId\":3},{\"escapeId\":5}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"fails if an operator is not given\" $\n      get \"/ghostBusters?id=0\" `shouldRespondWith`\n        [json| {\"code\":\"PGRST100\",\"details\":\"unexpected \\\"0\\\" expecting \\\"not\\\" or operator (eq, gt, ...)\",\"hint\":null,\"message\":\"\\\"failed to parse filter (0)\\\" (line 1, column 1)\"} |]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"will embed a collection\" $\n      get \"/Escap3e;?select=ghostBusters(*)\" `shouldRespondWith`\n        [json| [{\"ghostBusters\":[{\"escapeId\":1}]},{\"ghostBusters\":[]},{\"ghostBusters\":[{\"escapeId\":3}]},{\"ghostBusters\":[]},{\"ghostBusters\":[{\"escapeId\":5}]}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"will select and filter a column that has spaces\" $\n      get \"/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*\" `shouldRespondWith`\n        [json|[\n          {\"Just A Server Model\":\" IBM,9113-550 (P5-550)\"},\n          {\"Just A Server Model\":\" IBM,9113-550 (P5-550)\"},\n          {\"Just A Server Model\":\" IBM,9131-52A (P5-52A)\"},\n          {\"Just A Server Model\":\" IBM,9133-55A (P5-55A)\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"will select and filter a quoted column that has PostgREST reserved characters\" $\n      get \"/pgrst_reserved_chars?select=%22:arr-%3Eow::cast%22,%22(inside,parens)%22,%22a.dotted.column%22,%22%20%20col%20%20w%20%20space%20%20%22&%22*id*%22=eq.1\" `shouldRespondWith`\n        [json|[{\":arr->ow::cast\":\" arrow-1 \",\"(inside,parens)\":\" parens-1 \",\"a.dotted.column\":\" dotted-1 \",\"  col  w  space  \":\" space-1\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"will select and filter a column that has dollars in(without double quoting)\" $\n      get \"/do$llar$s?select=a$num$&a$num$=eq.100\" `shouldRespondWith`\n        [json|[{\"a$num$\":100}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  describe \"values with quotes in IN and NOT IN\" $ do\n    it \"succeeds when only quoted values are present\" $ do\n      get \"/w_or_wo_comma_names?name=in.(\\\"Hebdon, John\\\")\" `shouldRespondWith`\n        [json| [{\"name\":\"Hebdon, John\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=in.(\\\"Hebdon, John\\\",\\\"Williams, Mary\\\",\\\"Smith, Joseph\\\")\" `shouldRespondWith`\n        [json| [{\"name\":\"Hebdon, John\"},{\"name\":\"Williams, Mary\"},{\"name\":\"Smith, Joseph\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=not.in.(\\\"Hebdon, John\\\",\\\"Williams, Mary\\\",\\\"Smith, Joseph\\\")&limit=3\" `shouldRespondWith`\n        [json| [ { \"name\": \"David White\" }, { \"name\": \"Larry Thompson\" }, { \"name\": \"Double O Seven(007)\" }] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"succeeds w/ and w/o quoted values\" $ do\n      get \"/w_or_wo_comma_names?name=in.(David White,\\\"Hebdon, John\\\")\" `shouldRespondWith`\n        [json| [{\"name\":\"Hebdon, John\"},{\"name\":\"David White\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=not.in.(\\\"Hebdon, John\\\",Larry Thompson,\\\"Smith, Joseph\\\")&limit=3\" `shouldRespondWith`\n        [json| [ { \"name\": \"Williams, Mary\" }, { \"name\": \"David White\" }, { \"name\": \"Double O Seven(007)\" }] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=in.(\\\"Double O Seven(007)\\\")\" `shouldRespondWith`\n        [json| [{\"name\":\"Double O Seven(007)\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    context \"escaped chars\" $ do\n      it \"accepts escaped double quotes\" $\n        get \"/w_or_wo_comma_names?name=in.(\\\"Double\\\\\\\"Quote\\\\\\\"McGraw\\\\\\\"\\\")\" `shouldRespondWith`\n          [json| [ { \"name\": \"Double\\\"Quote\\\"McGraw\\\"\" } ] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"accepts escaped backslashes\" $ do\n        get \"/w_or_wo_comma_names?name=in.(\\\"\\\\\\\\\\\")\" `shouldRespondWith`\n          [json| [{ \"name\": \"\\\\\" }] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/w_or_wo_comma_names?name=in.(\\\"/\\\\\\\\Slash/\\\\\\\\Beast/\\\\\\\\\\\")\" `shouldRespondWith`\n          [json| [ { \"name\": \"/\\\\Slash/\\\\Beast/\\\\\" } ] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"passes any escaped char as the same char\" $\n        get \"/w_or_wo_comma_names?name=in.(\\\"D\\\\a\\\\vid W\\\\h\\\\ite\\\")\" `shouldRespondWith`\n          [json| [{ \"name\": \"David White\" }] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n  describe \"IN values without quotes\" $ do\n    it \"accepts single double quotes as values\" $ do\n      get \"/w_or_wo_comma_names?name=in.(\\\")\" `shouldRespondWith`\n        [json| [{ \"name\": \"\\\"\" }] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=in.(Double\\\"Quote\\\"McGraw\\\")\" `shouldRespondWith`\n        [json| [ { \"name\": \"Double\\\"Quote\\\"McGraw\\\"\" } ] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"accepts backslashes as values\" $ do\n      get \"/w_or_wo_comma_names?name=in.(\\\\)\" `shouldRespondWith`\n        [json| [{ \"name\": \"\\\\\" }] |]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/w_or_wo_comma_names?name=in.(/\\\\Slash/\\\\Beast/\\\\)\" `shouldRespondWith`\n        [json| [ { \"name\": \"/\\\\Slash/\\\\Beast/\\\\\" } ] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n  describe \"IN and NOT IN empty set\" $ do\n    context \"returns an empty result for IN when no value is present\" $ do\n      it \"works for integer\" $\n        get \"/items_with_different_col_types?int_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for text\" $\n        get \"/items_with_different_col_types?text_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for bool\" $\n        get \"/items_with_different_col_types?bool_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for bytea\" $\n        get \"/items_with_different_col_types?bin_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for char\" $\n        get \"/items_with_different_col_types?char_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for date\" $\n        get \"/items_with_different_col_types?date_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for real\" $\n        get \"/items_with_different_col_types?real_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n      it \"works for time\" $\n        get \"/items_with_different_col_types?time_data=in.()\" `shouldRespondWith`\n          [json| [] |] { matchHeaders = [matchContentTypeJson] }\n\n    it \"returns all results for not.in when no value is present\" $\n      get \"/items_with_different_col_types?int_data=not.in.()&select=int_data\" `shouldRespondWith`\n        [json| [{int_data: 1}] |] { matchHeaders = [matchContentTypeJson] }\n\n    it \"returns an empty result ignoring spaces\" $\n      get \"/items_with_different_col_types?int_data=in.(    )\" `shouldRespondWith`\n        [json| [] |] { matchHeaders = [matchContentTypeJson] }\n\n    it \"only returns an empty result set if the in value is empty\" $\n      get \"/items_with_different_col_types?int_data=in.( ,3,4)\"\n        `shouldRespondWith`\n        [json| {\"hint\":null,\"details\":null,\"code\":\"22P02\",\"message\":\"invalid input syntax for type integer: \\\"\\\"\"} |]\n        { matchStatus = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n  describe \"Embedding when column name = table name\" $ do\n    it \"works with child embeds\" $\n      get \"/being?select=*,descendant(*)&limit=1\" `shouldRespondWith`\n        [json|[{\"being\":1,\"descendant\":[{\"descendant\":1,\"being\":1},{\"descendant\":2,\"being\":1},{\"descendant\":3,\"being\":1}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"works with many to many embeds\" $\n      get \"/being?select=*,part(*)&limit=1\" `shouldRespondWith`\n        [json|[{\"being\":1,\"part\":[{\"part\":1}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  describe \"Foreign table\" $ do\n    it \"can be queried by using regular filters\" $\n      get \"/projects_dump?id=in.(1,2,3)\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}, {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}, {\"id\":3,\"name\":\"IOS\",\"client_id\":2}]|]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"can be queried with select, order and limit\" $\n      get \"/projects_dump?select=id,name&order=id.desc&limit=3\" `shouldRespondWith`\n        [json| [{\"id\":5,\"name\":\"Orphan\"}, {\"id\":4,\"name\":\"OSX\"}, {\"id\":3,\"name\":\"IOS\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n\n  it \"cannot use ltree(in public schema) extension operators if no extra search path added\" $\n    get \"/ltree_sample?path=cd.Top.Science.Astronomy\" `shouldRespondWith` 400\n\n  context \"VIEW that has a source FK based on a UNIQUE key\" $\n    it \"can be embedded\" $\n      get \"/referrals?select=site,link:pages(url)\" `shouldRespondWith`\n        [json| [\n         {\"site\":\"github.com\",     \"link\":{\"url\":\"http://postgrest.org/en/v6.0/api.html\"}},\n         {\"site\":\"hub.docker.com\", \"link\":{\"url\":\"http://postgrest.org/en/v6.0/admin.html\"}}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  it \"shouldn't produce a Content-Profile header since only a single schema is exposed\" $ do\n    r <- get \"/items\"\n    liftIO $ do\n      let respHeaders = simpleHeaders r\n      respHeaders `shouldSatisfy` noProfileHeader\n\n  context \"empty embed\" $ do\n    it \"works on a many-to-one relationship\" $ do\n      get \"/projects?select=id,name,clients()\" `shouldRespondWith`\n        [json| [\n          {\"id\":1,\"name\":\"Windows 7\"},\n          {\"id\":2,\"name\":\"Windows 10\"},\n          {\"id\":3,\"name\":\"IOS\"},\n          {\"id\":4,\"name\":\"OSX\"},\n          {\"id\":5,\"name\":\"Orphan\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/projects?select=id,name,clients!inner()&clients.id=eq.2\" `shouldRespondWith`\n        [json|[\n          {\"id\":3,\"name\":\"IOS\"},\n          {\"id\":4,\"name\":\"OSX\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works on a one-to-many relationship\" $ do\n      get \"/clients?select=id,name,projects()\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Microsoft\"}, {\"id\":2,\"name\":\"Apple\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/clients?select=id,name,projects!inner()&projects.name=eq.IOS\" `shouldRespondWith`\n        [json|[{\"id\":2,\"name\":\"Apple\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works on a many-to-many relationship\" $ do\n      get \"/users?select=*,tasks!inner()\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Angela Martin\"}, {\"id\":2,\"name\":\"Michael Scott\"}, {\"id\":3,\"name\":\"Dwight Schrute\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/users?select=*,tasks!inner()&tasks.id=eq.3\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Angela Martin\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works on nested relationships\" $ do\n      get \"/users?select=*,users_tasks(tasks(projects()))\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Angela Martin\"}, {\"id\":2,\"name\":\"Michael Scott\"}, {\"id\":3,\"name\":\"Dwight Schrute\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/users?select=*,users_tasks!inner(tasks!inner(projects()))&users_tasks.tasks.id=eq.3\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Angela Martin\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/users?select=*,tasks(projects(clients()),users_tasks())\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Angela Martin\"}, {\"id\":2,\"name\":\"Michael Scott\"}, {\"id\":3,\"name\":\"Dwight Schrute\"}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/users?select=*,tasks!inner(projects(clients()),users_tasks(),name)&tasks.id=eq.3\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"Angela Martin\",\"tasks\":[{\"name\": \"Design w10\"}]}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  context \"empty root select\" $\n    it \"gives all columns\" $ do\n      get \"/projects?select=\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Windows 7\",\"client_id\":1},\n          {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1},\n          {\"id\":3,\"name\":\"IOS\",\"client_id\":2},\n          {\"id\":4,\"name\":\"OSX\",\"client_id\":2},\n          {\"id\":5,\"name\":\"Orphan\",\"client_id\":null}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/rpc/getallprojects?select=\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Windows 7\",\"client_id\":1},\n          {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1},\n          {\"id\":3,\"name\":\"IOS\",\"client_id\":2},\n          {\"id\":4,\"name\":\"OSX\",\"client_id\":2},\n          {\"id\":5,\"name\":\"Orphan\",\"client_id\":null}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  context \"any/all quantifiers\" $ do\n    it \"works with the eq operator\" $\n      get \"/projects?id=eq(any).{3,4,5}\" `shouldRespondWith`\n        [json|[\n          {\"id\":3,\"name\":\"IOS\",\"client_id\":2},\n          {\"id\":4,\"name\":\"OSX\",\"client_id\":2},\n          {\"id\":5,\"name\":\"Orphan\",\"client_id\":null}\n        ]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works with the gt/gte operator\" $ do\n      get \"/projects?id=gt(all).{4,3}\" `shouldRespondWith`\n        [json|[{\"id\":5,\"name\":\"Orphan\",\"client_id\":null}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/projects?id=gte(all).{4,3}\" `shouldRespondWith`\n        [json|[{\"id\":4,\"name\":\"OSX\",\"client_id\":2}, {\"id\":5,\"name\":\"Orphan\",\"client_id\":null}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works with the lt/lte operator\" $ do\n      get \"/projects?id=lt(all).{4,3}\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}, {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/projects?id=lte(all).{4,3}\" `shouldRespondWith`\n        [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}, {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}, {\"id\":3,\"name\":\"IOS\",\"client_id\":2}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works with the like/ilike operator\" $ do\n      get \"/articles?body=like(any).{%plan%,%brain%}&select=id\" `shouldRespondWith`\n        [json|[ {\"id\":1}, {\"id\":2} ]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/articles?body=ilike(all).{%plan%,%greatness%}&select=id\" `shouldRespondWith`\n        [json|[ {\"id\":1} ]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    it \"works with the match/imatch operator\" $ do\n      get \"/articles?body=match(any).{stop,thing}&select=id\" `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n        { matchHeaders = [matchContentTypeJson] }\n      get \"/articles?body=imatch(any).{stop,thing}&select=id\" `shouldRespondWith`\n        [json|[{\"id\":1}, {\"id\":2}]|]\n        { matchHeaders = [matchContentTypeJson] }\n\n  describe \"Data representations for customisable value formatting and parsing\" $ do\n    it \"formats a single column\" $\n      get \"/datarep_todos?select=id,label_color&id=lt.4\" `shouldRespondWith`\n        [json| [{\"id\":1,\"label_color\":\"#000000\"},{\"id\":2,\"label_color\":\"#000100\"},{\"id\":3,\"label_color\":\"#01E240\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats two columns with different formatters\" $\n      get \"/datarep_todos?select=id,label_color,due_at&id=lt.4\" `shouldRespondWith`\n        [json| [{\"id\":1,\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\"},{\"id\":2,\"label_color\":\"#000100\",\"due_at\":\"2018-01-03T00:00:00Z\"},{\"id\":3,\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"fails in some reasonable way when selecting fields that don't exist\" $\n      get \"/datarep_todos?select=id,label_color,banana\" `shouldRespondWith`\n        [json| {\"code\":\"42703\",\"details\":null,\"hint\":null,\"message\":\"column datarep_todos.banana does not exist\"} |]\n        { matchStatus  = 400\n        , matchHeaders = [\n            \"Content-Length\" <:> \"98\",\n            matchContentTypeJson\n          ]\n        }\n    it \"formats columns in views including computed columns\" $\n      get \"/datarep_todos_computed?select=id,label_color,dark_color\" `shouldRespondWith`\n        [json| [\n          {\"id\":1, \"label_color\":\"#000000\", \"dark_color\":\"#000000\"},\n          {\"id\":2, \"label_color\":\"#000100\", \"dark_color\":\"#000080\"},\n          {\"id\":3, \"label_color\":\"#01E240\", \"dark_color\":\"#00F120\"},\n          {\"id\":4, \"label_color\":\"\", \"dark_color\":\"\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats and allows rename\" $\n      get \"/datarep_todos?select=id,clr:label_color&id=lt.4\" `shouldRespondWith`\n        [json| [{\"id\":1,\"clr\":\"#000000\"},{\"id\":2,\"clr\":\"#000100\"},{\"id\":3,\"clr\":\"#01E240\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats, renames and allows manual casting on top\" $\n      get \"/datarep_todos?select=id,clr:label_color::text&id=lt.4\" `shouldRespondWith`\n        [json| [{\"id\":1,\"clr\":\"\\\"#000000\\\"\"},{\"id\":2,\"clr\":\"\\\"#000100\\\"\"},{\"id\":3,\"clr\":\"\\\"#01E240\\\"\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats nulls\" $\n      -- due_at is formatted as NULL but label_color NULLs become empty strings-- it's up to the formatting function.\n      get \"/datarep_todos?select=id,label_color,due_at&id=gt.2&id=lt.5\" `shouldRespondWith`\n        [json| [{\"id\":3,\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\"},{\"id\":4,\"label_color\":\"\",\"due_at\":null}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats star select\" $\n      get \"/datarep_todos?select=*&id=lt.4\" `shouldRespondWith`\n        [json| [\n          {\"id\":1,\"name\":\"Report\",\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\",\"icon_image\":\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=\",\"created_at\":1513213350,\"budget\":\"12.50\"},\n           {\"id\":2,\"name\":\"Essay\",\"label_color\":\"#000100\",\"due_at\":\"2018-01-03T00:00:00Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"100000000000000.13\"},\n           {\"id\":3,\"name\":\"Algebra\",\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"0.00\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats implicit star select\" $\n      get \"/datarep_todos?id=lt.4\" `shouldRespondWith`\n        [json| [\n          {\"id\":1,\"name\":\"Report\",\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\",\"icon_image\":\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=\",\"created_at\":1513213350,\"budget\":\"12.50\"},\n          {\"id\":2,\"name\":\"Essay\",\"label_color\":\"#000100\",\"due_at\":\"2018-01-03T00:00:00Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"100000000000000.13\"},\n          {\"id\":3,\"name\":\"Algebra\",\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"0.00\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats star and explicit mix\" $\n      get \"/datarep_todos?select=due_at,*&id=lt.4\" `shouldRespondWith`\n        [json| [\n          {\"due_at\":\"2018-01-02T00:00:00Z\",\"id\":1,\"name\":\"Report\",\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\",\"icon_image\":\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=\",\"created_at\":1513213350,\"budget\":\"12.50\"},\n           {\"due_at\":\"2018-01-03T00:00:00Z\",\"id\":2,\"name\":\"Essay\",\"label_color\":\"#000100\",\"due_at\":\"2018-01-03T00:00:00Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"100000000000000.13\"},\n           {\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"id\":3,\"name\":\"Algebra\",\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"0.00\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats through join\" $\n      get \"/datarep_next_two_todos?select=id,name,first_item:datarep_todos!datarep_next_two_todos_first_item_id_fkey(label_color,due_at)\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"school related\",\"first_item\":{\"label_color\":\"#000100\",\"due_at\":\"2018-01-03T00:00:00Z\"}},{\"id\":2,\"name\":\"do these first\",\"first_item\":{\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\"}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"formats through join with star select\" $\n      get \"/datarep_next_two_todos?select=id,name,second_item:datarep_todos!datarep_next_two_todos_second_item_id_fkey(*)\" `shouldRespondWith`\n        [json| [\n          {\"id\":1,\"name\":\"school related\",\"second_item\":{\"id\":3,\"name\":\"Algebra\",\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"0.00\"}},\n          {\"id\":2,\"name\":\"do these first\",\"second_item\":{\"id\":3,\"name\":\"Algebra\",\"label_color\":\"#01E240\",\"due_at\":\"2018-01-01T14:12:34.123456Z\",\"icon_image\":null,\"created_at\":1513213350,\"budget\":\"0.00\"}}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"uses text parser on value for filter given through query parameters\" $\n      get \"/datarep_todos?select=id,due_at&label_color=eq.000100\" `shouldRespondWith`\n        [json| [{\"id\":2,\"due_at\":\"2018-01-03T00:00:00Z\"}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"in the absense of text parser, does not try to use the JSON parser for query parameters\" $\n      get \"/datarep_todos?select=id,due_at&due_at=eq.Z\" `shouldRespondWith`\n        -- we prove the parser is not used because it'd replace the Z with `+00:00` and a different error message.\n        [json| {\"code\":\"22007\",\"details\":null,\"hint\":null,\"message\":\"invalid input syntax for type timestamp with time zone: \\\"Z\\\"\"} |]\n        { matchStatus  = 400\n        , matchHeaders = [\n            \"Content-Length\" <:> \"117\",\n            matchContentTypeJson\n          ]\n        }\n    it \"uses text parser for filter with 'IN' predicates\" $\n      get \"/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)\" `shouldRespondWith`\n        [json| [\n          {\"id\":2, \"due_at\": \"2018-01-03T00:00:00Z\"},\n          {\"id\":3, \"due_at\": \"2018-01-01T14:12:34.123456Z\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"uses text parser for filter with 'NOT IN' predicates\" $\n      get \"/datarep_todos?select=id,due_at&label_color=not.in.(000000,01E240)\" `shouldRespondWith`\n        [json| [\n          {\"id\":2, \"due_at\": \"2018-01-03T00:00:00Z\"}\n        ] |]\n        { matchHeaders = [matchContentTypeJson] }\n    it \"uses text parser on value for filter across relations\" $\n      get \"/datarep_next_two_todos?select=id,name,datarep_todos!datarep_next_two_todos_first_item_id_fkey(label_color,due_at)&datarep_todos.label_color=neq.000100\" `shouldRespondWith`\n        [json| [{\"id\":1,\"name\":\"school related\",\"datarep_todos\":null},{\"id\":2,\"name\":\"do these first\",\"datarep_todos\":{\"label_color\":\"#000000\",\"due_at\":\"2018-01-02T00:00:00Z\"}}] |]\n        { matchHeaders = [matchContentTypeJson] }\n    -- This is not supported by data reps (would be hard to make it work with high performance). So the test just\n    -- verifies we don't panic or add inappropriate SQL to the filters.\n    it \"fails safely on user trying to use ilike operator on data reps column\" $\n      get \"/datarep_todos?select=id,name&label_color=ilike.#*100\" `shouldRespondWith`\n        [json|\n          {\"code\":\"42883\",\"details\":null,\"hint\":\"No operator matches the given name and argument types. You might need to add explicit type casts.\",\"message\":\"operator does not exist: public.color ~~* unknown\"}\n        |]\n        { matchStatus  = 404\n        , matchHeaders = [\n            \"Content-Length\" <:> \"200\",\n            matchContentTypeJson\n          ]\n        }\n\n  context \"searching for an empty string\" $ do\n    it \"works with an empty eq filter\" $\n      get \"/empty_string?string=eq.&select=id,string\" `shouldRespondWith`\n        [json|\n          [{\"id\":1,\"string\":\"\"}]\n        |]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n  context \"test infinite recursion error 42P17\" $\n    it \"return http status 500\" $\n      get \"/infinite_recursion?select=*\" `shouldRespondWith`\n        [json|{\"code\":\"42P17\",\"message\":\"infinite recursion detected in rules for relation \\\"infinite_recursion\\\"\",\"details\":null,\"hint\":null}|]\n        { matchStatus = 500\n        , matchHeaders = [\"Content-Length\" <:> \"128\"] }\n\n  context \"invalid resource path\" $ do\n    it \"return http status 404\" $\n      get \"/first/second/third?select=*\"\n      `shouldRespondWith`\n      [json| {\"code\":\"PGRST125\",\"details\":null,\"hint\":null,\"message\":\"Invalid path specified in request URL\"} |]\n      { matchStatus = 404\n      , matchHeaders = [\"Content-Length\" <:> \"96\"]}\n"
  },
  {
    "path": "test/spec/Feature/Query/RangeSpec.hs",
    "content": "module Feature.Query.RangeSpec where\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (simpleHeaders, simpleStatus))\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = do\n  describe \"GET /rpc/getitemrange\" $ do\n    context \"without range headers\" $ do\n      context \"with response under server size limit\" $\n        it \"returns whole range with status 200\" $\n           get \"/rpc/getitemrange?min=0&max=15\" `shouldRespondWith` 200\n\n      context \"when I don't want the count\" $ do\n        it \"returns range Content-Range with */* for empty range\" $\n          get \"/rpc/getitemrange?min=2&max=2\"\n            `shouldRespondWith` [json| [] |]\n              { matchHeaders = [ \"Content-Range\" <:> \"*/*\"\n                               , \"Content-Length\" <:> \"2\" ]\n              }\n\n        it \"returns range Content-Range with range/*\" $\n          get \"/rpc/getitemrange?order=id&min=0&max=15\"\n            `shouldRespondWith`\n              [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}] |]\n              { matchHeaders = [\"Content-Range\" <:> \"0-14/*\"] }\n\n      context \"of invalid range\" $ do\n        it \"refuses a range with nonzero start when there are no items\" $\n          request methodGet \"/rpc/getitemrange?offset=1&min=2&max=2\"\n                  [(\"Prefer\", \"count=exact\")] mempty\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 1 was requested, but there are only 0 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [ \"Content-Range\" <:> \"*/0\"\n                             , \"Content-Length\" <:> \"144\"]\n            }\n\n        it \"refuses a range requesting start past last item\" $\n          request methodGet \"/rpc/getitemrange?offset=100&min=0&max=15\"\n                  [(\"Prefer\", \"count=exact\")] mempty\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 100 was requested, but there are only 15 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [\"Content-Range\" <:> \"*/15\"]\n            }\n\n    context \"with range headers\" $ do\n      context \"of acceptable range\" $ do\n        it \"succeeds with partial content\" $ do\n          r <- request methodGet  \"/rpc/getitemrange?min=0&max=15\"\n                       (rangeHdrs $ ByteRangeFromTo 0 1) mempty\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy` matchHeader \"Content-Range\" \"0-1/*\"\n            simpleHeaders r `shouldSatisfy` matchHeader \"Content-Length\" \"22\"\n            simpleStatus r `shouldBe` ok200\n\n        it \"understands open-ended ranges\" $\n          request methodGet \"/rpc/getitemrange?min=0&max=15\"\n                  (rangeHdrs $ ByteRangeFrom 0) mempty\n            `shouldRespondWith` 200\n\n        it \"returns an empty body when there are no results\" $\n          request methodGet \"/rpc/getitemrange?min=2&max=2\"\n                  (rangeHdrs $ ByteRangeFromTo 0 1) mempty\n            `shouldRespondWith` \"[]\"\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n            }\n\n        it \"allows one-item requests\" $ do\n          r <- request methodGet  \"/rpc/getitemrange?min=0&max=15\"\n                       (rangeHdrs $ ByteRangeFromTo 0 0) mempty\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy`\n              matchHeader \"Content-Range\" \"0-0/*\"\n            simpleStatus r `shouldBe` ok200\n\n        it \"handles ranges beyond collection length via truncation\" $ do\n          r <- request methodGet  \"/rpc/getitemrange?min=0&max=15\"\n                       (rangeHdrs $ ByteRangeFromTo 10 100) mempty\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy`\n              matchHeader \"Content-Range\" \"10-14/*\"\n            simpleStatus r `shouldBe` ok200\n\n      context \"of invalid range\" $ do\n        it \"fails with 416 for offside range\" $\n          request methodGet  \"/rpc/getitemrange?min=2&max=2\"\n                  (rangeHdrs $ ByteRangeFromTo 1 0) mempty\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"The lower boundary must be lower than or equal to the upper boundary in the Range header.\",\n                \"hint\":null\n              }|]\n            { matchStatus = 416 }\n\n        it \"refuses a range with nonzero start when there are no items\" $\n          request methodGet \"/rpc/getitemrange?min=2&max=2\"\n                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) mempty\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 1 was requested, but there are only 0 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [ \"Content-Range\" <:> \"*/0\"\n                             , \"Content-Length\" <:> \"144\"]\n            }\n\n        it \"refuses a range requesting start past last item\" $\n          request methodGet \"/rpc/getitemrange?min=0&max=15\"\n                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) mempty\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 100 was requested, but there are only 15 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [\"Content-Range\" <:> \"*/15\"]\n            }\n\n  describe \"GET /items\" $ do\n    context \"without range headers\" $ do\n      context \"with response under server size limit\" $\n        it \"returns whole range with status 200\" $\n          get \"/items\" `shouldRespondWith` 200\n\n      context \"count with an empty body\" $ do\n        it \"returns empty body with Content-Range */0\" $\n          request methodGet \"/items?id=eq.0\"\n            [(\"Prefer\", \"count=exact\")] \"\"\n            `shouldRespondWith`\n              [json|[]|]\n              { matchHeaders = [\"Content-Range\" <:> \"*/0\"] }\n\n      context \"when I don't want the count\" $ do\n        it \"returns range Content-Range with /*\" $\n          request methodGet \"/menagerie\"\n              [(\"Prefer\", \"count=none\")] \"\"\n            `shouldRespondWith`\n              [json|[]|]\n              { matchHeaders = [\"Content-Range\" <:> \"*/*\"] }\n\n        it \"returns range Content-Range with range/*\" $\n          request methodGet \"/items?order=id\"\n                  [(\"Prefer\", \"count=none\")] \"\"\n            `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}] |]\n            { matchHeaders = [\"Content-Range\" <:> \"0-14/*\"] }\n\n        it \"returns range Content-Range with range/* even using other filters\" $\n          request methodGet \"/items?id=eq.1&order=id\"\n                  [(\"Prefer\", \"count=none\")] \"\"\n            `shouldRespondWith` [json| [{\"id\":1}] |]\n            { matchHeaders = [\"Content-Range\" <:> \"0-0/*\"] }\n\n    context \"with limit/offset parameters\" $ do\n      it \"no parameters return everything\" $\n        get \"/items?select=id&order=id.asc\"\n          `shouldRespondWith`\n          [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}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"0-14/*\"]\n          }\n      it \"top level limit with parameter\" $\n        get \"/items?select=id&order=id.asc&limit=3\"\n          `shouldRespondWith` [json|[{\"id\":1},{\"id\":2},{\"id\":3}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"0-2/*\"]\n          }\n      it \"headers override get parameters\" $\n        request methodGet  \"/items?select=id&order=id.asc&limit=3\"\n                     (rangeHdrs $ ByteRangeFromTo 0 1) \"\"\n          `shouldRespondWith` [json|[{\"id\":1},{\"id\":2}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"0-1/*\"]\n          }\n\n      it \"limit works on all levels\" $\n        get \"/clients?select=id,projects(id,tasks(id))&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1\"\n          `shouldRespondWith`\n          [json|[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1}]},{\"id\":2,\"tasks\":[{\"id\":3}]}]}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"0-0/*\"]\n          }\n\n      it \"limit and offset works on first level\" $ do\n        get \"/items?select=id&order=id.asc&limit=3&offset=2\"\n          `shouldRespondWith` [json|[{\"id\":3},{\"id\":4},{\"id\":5}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Range\" <:> \"2-4/*\"]\n          }\n        request methodHead \"/items?select=id&order=id.asc&limit=3&offset=2\"\n            []\n            mempty\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"2-4/*\" ]\n            }\n\n      it \"works alongside order by with nulls order\" $\n         get \"/clients?select=id,projects(id,tasks(id))&order=id.asc.nullslast&limit=1&projects.order=id.asc.nullsfirst&projects.limit=2\"\n           `shouldRespondWith`\n           [json|[{\"id\":1,\"projects\":[{\"id\": 1, \"tasks\": [{\"id\": 1}, {\"id\": 2}]}, {\"id\": 2, \"tasks\": [{\"id\": 3}, {\"id\": 4}]}]}]|]\n           { matchStatus  = 200\n           , matchHeaders = [\"Content-Range\" <:> \"0-0/*\"]\n           }\n\n      context \"succeeds if offset equals 0 as a no-op\" $ do\n        it  \"no items\" $ do\n          get \"/items?offset=0&id=eq.0\"\n            `shouldRespondWith`\n              [json|[]|]\n              { matchHeaders = [\"Content-Range\" <:> \"*/*\"] }\n\n          request methodGet \"/items?offset=0&id=eq.0\"\n            [(\"Prefer\", \"count=exact\")] \"\"\n            `shouldRespondWith`\n              [json|[]|]\n              { matchHeaders = [\"Content-Range\" <:> \"*/0\"] }\n\n        it  \"one or more items\" $\n          get \"/items?select=id&offset=0&order=id\"\n            `shouldRespondWith`\n              [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}]|]\n              { matchHeaders = [\"Content-Range\" <:> \"0-14/*\"] }\n\n      it \"succeeds if offset is negative as a no-op\" $\n        get \"/items?select=id&offset=-4&order=id\"\n          `shouldRespondWith`\n            [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}]|]\n            { matchHeaders = [\"Content-Range\" <:> \"0-14/*\"] }\n\n      it \"succeeds and returns an empty array if limit equals 0\" $\n        get \"/items?select=id&limit=0\"\n          `shouldRespondWith` [json|[]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n            }\n\n      it \"fails if limit is negative\" $\n        get \"/items?select=id&limit=-1\"\n          `shouldRespondWith`\n            [json| {\n              \"message\":\"Requested range not satisfiable\",\n              \"code\":\"PGRST103\",\n              \"details\":\"Limit should be greater than or equal to zero.\",\n              \"hint\":null\n            }|]\n          { matchStatus  = 416\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      context \"of invalid range\" $ do\n        it \"refuses a range with nonzero start when there are no items\" $\n          request methodGet \"/menagerie?offset=1\"\n                  [(\"Prefer\", \"count=exact\")] \"\"\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 1 was requested, but there are only 0 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [ \"Content-Range\" <:> \"*/0\"\n                             , \"Content-Length\" <:>  \"144\"]\n            }\n\n        it \"refuses a range requesting start past last item\" $\n          request methodGet \"/items?offset=100\"\n                  [(\"Prefer\", \"count=exact\")] \"\"\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 100 was requested, but there are only 15 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [\"Content-Range\" <:> \"*/15\"]\n            }\n\n    context \"when count=planned\" $ do\n      it \"obtains a filtered range\" $ do\n        request methodGet \"/items?select=id&id=gt.8\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{\"id\":9}, {\"id\":10}, {\"id\":11}, {\"id\":12}, {\"id\":13}, {\"id\":14}, {\"id\":15}]|]\n            { matchStatus  = 206\n            , matchHeaders = [\"Content-Range\" <:> \"0-6/8\"]\n            }\n\n        request methodGet \"/child_entities?select=id&id=gt.3\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{\"id\":4}, {\"id\":5}, {\"id\":6}]|]\n            { matchStatus  = 206\n            , matchHeaders = [\"Content-Range\" <:> \"0-2/4\"]\n            }\n\n        request methodGet \"/getallprojects_view?select=id&id=lt.3\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[{\"id\":1}, {\"id\":2}]|]\n            { matchStatus  = 206\n            , matchHeaders = [\"Content-Range\" <:> \"0-1/673\"]\n            }\n\n      it \"obtains the full range\" $ do\n        request methodHead \"/items\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-14/15\" ]\n            }\n\n        request methodHead \"/child_entities\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-5/6\" ]\n            }\n\n        request methodHead \"/getallprojects_view\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-4/2019\" ]\n            }\n\n      it \"ignores limit/offset on the planned count\" $ do\n        request methodHead \"/items?limit=2&offset=3\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"3-4/15\" ]\n            }\n\n        request methodHead \"/child_entities?limit=2\"\n           [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-1/6\" ]\n            }\n\n        request methodHead \"/getallprojects_view?limit=2\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 206\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-1/2019\" ]\n            }\n\n      it \"works with two levels\" $\n        request methodHead \"/child_entities?select=*,entities(*)\"\n            [(\"Prefer\", \"count=planned\")]\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson\n                             , \"Content-Range\" <:> \"0-5/6\" ]\n            }\n\n    context \"with range headers\" $ do\n      context \"of acceptable range\" $ do\n        it \"succeeds with partial content\" $ do\n          r <- request methodGet  \"/items\"\n                       (rangeHdrs $ ByteRangeFromTo 0 1) \"\"\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy`\n              matchHeader \"Content-Range\" \"0-1/*\"\n            simpleStatus r `shouldBe` ok200\n\n        it \"understands open-ended ranges\" $\n          request methodGet \"/items\"\n                  (rangeHdrs $ ByteRangeFrom 0) \"\"\n            `shouldRespondWith` 200\n\n        it \"returns an empty body when there are no results\" $\n          request methodGet \"/menagerie\"\n                  (rangeHdrs $ ByteRangeFromTo 0 1) \"\"\n            `shouldRespondWith` \"[]\"\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"*/*\"]\n            }\n\n        it \"allows one-item requests\" $ do\n          r <- request methodGet  \"/items\"\n                       (rangeHdrs $ ByteRangeFromTo 0 0) \"\"\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy`\n              matchHeader \"Content-Range\" \"0-0/*\"\n            simpleStatus r `shouldBe` ok200\n\n        it \"handles ranges beyond collection length via truncation\" $ do\n          r <- request methodGet  \"/items\"\n                       (rangeHdrs $ ByteRangeFromTo 10 100) \"\"\n          liftIO $ do\n            simpleHeaders r `shouldSatisfy`\n              matchHeader \"Content-Range\" \"10-14/*\"\n            simpleStatus r `shouldBe` ok200\n\n      context \"of invalid range\" $ do\n        it \"fails with 416 for offside range\" $\n          request methodGet  \"/items\"\n                  (rangeHdrs $ ByteRangeFromTo 1 0) \"\"\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"The lower boundary must be lower than or equal to the upper boundary in the Range header.\",\n                \"hint\":null\n              }|]\n            { matchStatus = 416 }\n\n        it \"refuses a range with nonzero start when there are no items\" $\n          request methodGet \"/menagerie\"\n                  (rangeHdrsWithCount $ ByteRangeFromTo 1 2) \"\"\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 1 was requested, but there are only 0 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [ \"Content-Range\" <:> \"*/0\"\n                             , \"Content-Length\" <:> \"144\"]\n            }\n\n        it \"refuses a range requesting start past last item\" $\n          request methodGet \"/items\"\n                  (rangeHdrsWithCount $ ByteRangeFromTo 100 199) \"\"\n            `shouldRespondWith`\n              [json| {\n                \"message\":\"Requested range not satisfiable\",\n                \"code\":\"PGRST103\",\n                \"details\":\"An offset of 100 was requested, but there are only 15 rows.\",\n                \"hint\":null\n              }|]\n            { matchStatus  = 416\n            , matchHeaders = [\"Content-Range\" <:> \"*/15\"]\n            }\n"
  },
  {
    "path": "test/spec/Feature/Query/RawOutputTypesSpec.hs",
    "content": "module Feature.Query.RawOutputTypesSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude\nimport SpecHelper (acceptHdrs)\n\nspec :: SpecWith ((), Application)\nspec = describe \"When raw-media-types config variable is missing or left empty\" $ do\n  let firefoxAcceptHdrs = acceptHdrs \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"\n      chromeAcceptHdrs = acceptHdrs \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\"\n  it \"responds json to a GET request with Firefox Accept headers\" $\n    request methodGet \"/items?id=eq.1\" firefoxAcceptHdrs \"\"\n      `shouldRespondWith` [json| [{\"id\":1}] |]\n        { matchHeaders= [\"Content-Type\" <:> \"application/json; charset=utf-8\"] }\n  it \"responds json to a GET request with Chrome Accept headers\" $\n    request methodGet \"/items?id=eq.1\" chromeAcceptHdrs \"\"\n      `shouldRespondWith` [json| [{\"id\":1}] |]\n        { matchHeaders= [\"Content-Type\" <:> \"application/json; charset=utf-8\"] }\n\n  it \"responds json to a GET request to RPC with Firefox Accept headers\" $\n    request methodGet \"/rpc/get_projects_below?id=3\" firefoxAcceptHdrs \"\"\n      `shouldRespondWith` [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}, {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}]|]\n        { matchHeaders= [\"Content-Type\" <:> \"application/json; charset=utf-8\"] }\n  it \"responds json to a GET request to RPC with Chrome Accept headers\" $\n    request methodGet \"/rpc/get_projects_below?id=3\" chromeAcceptHdrs \"\"\n      `shouldRespondWith` [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}, {\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}]|]\n        { matchHeaders= [\"Content-Type\" <:> \"application/json; charset=utf-8\"] }\n"
  },
  {
    "path": "test/spec/Feature/Query/RelatedQueriesSpec.hs",
    "content": "module Feature.Query.RelatedQueriesSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = describe \"related queries\" $ do\n  context \"related orders\" $ do\n    it \"works on a many-to-one relationship\" $ do\n      get \"/projects?select=id,clients(name)&order=clients(name).nullsfirst\" `shouldRespondWith`\n        [json|[\n          {\"id\":5,\"clients\":null},\n          {\"id\":3,\"clients\":{\"name\":\"Apple\"}},\n          {\"id\":4,\"clients\":{\"name\":\"Apple\"}},\n          {\"id\":1,\"clients\":{\"name\":\"Microsoft\"}},\n          {\"id\":2,\"clients\":{\"name\":\"Microsoft\"}} ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/projects?select=id,client:clients(name)&order=client(name).asc\" `shouldRespondWith`\n        [json|[\n          {\"id\":3,\"client\":{\"name\":\"Apple\"}},\n          {\"id\":4,\"client\":{\"name\":\"Apple\"}},\n          {\"id\":1,\"client\":{\"name\":\"Microsoft\"}},\n          {\"id\":2,\"client\":{\"name\":\"Microsoft\"}},\n          {\"id\":5,\"client\":null} ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/videogames?select=id,computed_designers(id)&order=computed_designers(id).desc\" `shouldRespondWith`\n        [json|[\n          {\"id\":3,\"computed_designers\":{\"id\":2}},\n          {\"id\":4,\"computed_designers\":{\"id\":2}},\n          {\"id\":1,\"computed_designers\":{\"id\":1}},\n          {\"id\":2,\"computed_designers\":{\"id\":1}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on a one-to-one relationship and jsonb column\" $ do\n      get \"/trash?select=id,trash_details(id,jsonb_col)&order=trash_details(jsonb_col->key).asc\" `shouldRespondWith`\n        [json|[\n          {\"id\":2,\"trash_details\":{\"id\":2,\"jsonb_col\":{\"key\": 6}}},\n          {\"id\":3,\"trash_details\":{\"id\":3,\"jsonb_col\":{\"key\": 8}}},\n          {\"id\":1,\"trash_details\":{\"id\":1,\"jsonb_col\":{\"key\": 10}}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/trash?select=id,trash_details(id,jsonb_col)&order=trash_details(jsonb_col->key).desc\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"trash_details\":{\"id\":1,\"jsonb_col\":{\"key\": 10}}},\n          {\"id\":3,\"trash_details\":{\"id\":3,\"jsonb_col\":{\"key\": 8}}},\n          {\"id\":2,\"trash_details\":{\"id\":2,\"jsonb_col\":{\"key\": 6}}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on an embedded resource\" $ do\n      get \"/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).desc&limit=1\" `shouldRespondWith`\n        [json| [{\n          \"name\":\"Angela Martin\",\n          \"tasks\":[\n            {\"id\": 3, \"name\":\"Design w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}},\n            {\"id\": 4, \"name\":\"Code w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}},\n            {\"id\": 1, \"name\":\"Design w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}},\n            {\"id\": 2, \"name\":\"Code w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}}\n          ]\n        }]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).desc,name&limit=1\" `shouldRespondWith`\n        [json| [{\n          \"name\":\"Angela Martin\",\n          \"tasks\":[\n            {\"id\": 4, \"name\":\"Code w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}},\n            {\"id\": 3, \"name\":\"Design w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}},\n            {\"id\": 2, \"name\":\"Code w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}},\n            {\"id\": 1, \"name\":\"Design w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}}\n          ]\n        }]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).asc&limit=1\" `shouldRespondWith`\n        [json|[{\n          \"name\":\"Angela Martin\",\n          \"tasks\":[\n            {\"id\":1,\"name\":\"Design w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}},\n            {\"id\":2,\"name\":\"Code w7\",\"projects\":{\"id\":1,\"name\":\"Windows 7\"}},\n            {\"id\":3,\"name\":\"Design w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}},\n            {\"id\":4,\"name\":\"Code w10\",\"projects\":{\"id\":2,\"name\":\"Windows 10\"}}\n          ]\n        }]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"fails when is not a to-one relationship\" $ do\n      get \"/clients?select=*,projects(*)&order=projects(id)\" `shouldRespondWith`\n        [json|{\n          \"code\":\"PGRST118\",\n          \"details\":\"'clients' and 'projects' do not form a many-to-one or one-to-one relationship\",\n          \"hint\":null,\n          \"message\":\"A related order on 'projects' is not possible\"\n        }|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/clients?select=*,pros:projects(*)&order=pros(id)\" `shouldRespondWith`\n        [json|{\n          \"code\":\"PGRST118\",\n          \"details\":\"'clients' and 'pros' do not form a many-to-one or one-to-one relationship\",\n          \"hint\":null,\n          \"message\":\"A related order on 'pros' is not possible\"\n        }|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/designers?select=id,computed_videogames(id)&order=computed_videogames(id).desc\" `shouldRespondWith`\n        [json|{\n          \"code\":\"PGRST118\",\n          \"details\":\"'designers' and 'computed_videogames' do not form a many-to-one or one-to-one relationship\",\n          \"hint\":null,\n          \"message\":\"A related order on 'computed_videogames' is not possible\"\n        }|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"fails when the resource is not embedded\" $\n      get \"/projects?select=id,clients(name)&order=clientsx(name).nullsfirst\" `shouldRespondWith`\n        [json|{\n          \"code\":\"PGRST108\",\n          \"details\":null,\n          \"hint\":\"Verify that 'clientsx' is included in the 'select' query parameter.\",\n          \"message\":\"'clientsx' is not an embedded resource in this request\"\n        }|]\n        { matchStatus  = 400\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n  context \"related conditions through null operator on embed\" $ do\n    it \"works on a many-to-one relationship\" $ do\n      get \"/projects?select=name,clients()&clients=not.is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Windows 7\"},\n          {\"name\":\"Windows 10\"},\n          {\"name\":\"IOS\"},\n          {\"name\":\"OSX\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/projects?select=name,clients()&clients=is.null\" `shouldRespondWith`\n        [json|[{\"name\":\"Orphan\"}]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/projects?select=name,computed_clients()&computed_clients=is.null\" `shouldRespondWith`\n        [json|[{\"name\":\"Orphan\"}]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on a one-to-many relationship\" $ do\n      get \"/entities?select=name,child_entities()&child_entities=not.is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"entity 1\"},\n          {\"name\":\"entity 2\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/entities?select=name,child_entities()&child_entities=is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"entity 3\"},\n          {\"name\":null}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/entities?select=name,childs:child_entities()&childs=is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"entity 3\"},\n          {\"name\":null}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on a many-to-many relationship\" $ do\n      get \"/users?select=name,tasks()&tasks.id=eq.1&tasks=not.is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Angela Martin\"},\n          {\"name\":\"Dwight Schrute\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/users?select=name,tasks()&tasks.id=eq.1&tasks=is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Michael Scott\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on nested embeds\" $ do\n      get \"/entities?select=name,child_entities(name,grandchild_entities())&child_entities.grandchild_entities=not.is.null&child_entities=not.is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"entity 1\",\"child_entities\":[{\"name\":\"child entity 1\"}, {\"name\":\"child entity 2\"}]}]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"can do an or across embeds\" $\n      get \"/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\"},\n          {\"id\":2,\"name\":\"Target\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"doesn't interfere filtering when embedding using the column name\" $\n      get \"/projects?select=name,client_id,client:client_id(name)&client_id=eq.2\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"IOS\",\"client_id\":2,\"client\":{\"name\":\"Apple\"}},\n          {\"name\":\"OSX\",\"client_id\":2,\"client\":{\"name\":\"Apple\"}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"doesn't interfere filtering on column names used for disambiguation\" $\n      get \"/user_friend?select=*,user1(*)&user1=eq.a02fb934-3a4d-469b-a6b6-4fcd88b973cf\" `shouldRespondWith`\n        [json|[]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"doesn't interfere filtering on column names that are the same as the relation name\" $\n      get \"/tournaments?select=*,status(*)&status=eq.3\" `shouldRespondWith`\n        [json|[]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    -- \"?table=not.is.null\" does a \"table IS DISTINCT FROM NULL\" instead of a \"table IS NOT NULL\"\n    -- https://github.com/PostgREST/postgrest/issues/2800#issuecomment-1720315818\n    it \"embeds verifying that the entire target table row is not null\" $ do\n      get \"/table_b?select=name,table_a(name)&table_a=not.is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Test 1\",\"table_a\":{\"name\":\"Not null 1\"}},\n          {\"name\":\"Test 2\",\"table_a\":{\"name\":null}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/table_b?select=name,table_a()&table_a=is.null\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Test 3\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works with count=exact\" $ do\n      request methodGet \"/projects?select=name,clients(name)&clients=not.is.null\"\n        [(\"Prefer\", \"count=exact\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"name\":\"Windows 7\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"Windows 10\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"IOS\", \"clients\":{\"name\":\"Apple\"}},\n          {\"name\":\"OSX\", \"clients\":{\"name\":\"Apple\"}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-3/4\" ]\n        }\n      request methodGet \"/projects?select=name,clients()&clients=is.null\"\n        [(\"Prefer\", \"count=exact\")] \"\"\n       `shouldRespondWith`\n        [json|[{\"name\":\"Orphan\"}]|]\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-0/1\" ]\n        }\n      request methodGet \"/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)\"\n        [(\"Prefer\", \"count=exact\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\"},\n          {\"id\":2,\"name\":\"Target\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-1/2\" ]\n        }\n\n    it \"works with count=planned\" $ do\n      request methodGet \"/projects?select=name,clients(name)&clients=not.is.null\"\n        [(\"Prefer\", \"count=planned\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"name\":\"Windows 7\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"Windows 10\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"IOS\", \"clients\":{\"name\":\"Apple\"}},\n          {\"name\":\"OSX\", \"clients\":{\"name\":\"Apple\"}}\n        ]|]\n        { matchStatus  = 206\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-3/1200\" ]\n        }\n      request methodGet \"/projects?select=name,clients()&clients=is.null\"\n        [(\"Prefer\", \"count=planned\")] \"\"\n       `shouldRespondWith`\n        [json|[{\"name\":\"Orphan\"}]|]\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-0/1\" ]\n        }\n      request methodGet \"/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)\"\n        [(\"Prefer\", \"count=planned\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\"},\n          {\"id\":2,\"name\":\"Target\"}\n        ]|]\n        { matchStatus  = 206\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-1/952\" ]\n        }\n\n    it \"works with count=estimated\" $ do\n      request methodGet \"/projects?select=name,clients(name)&clients=not.is.null\"\n        [(\"Prefer\", \"count=estimated\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"name\":\"Windows 7\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"Windows 10\", \"clients\":{\"name\":\"Microsoft\"}},\n          {\"name\":\"IOS\", \"clients\":{\"name\":\"Apple\"}},\n          {\"name\":\"OSX\", \"clients\":{\"name\":\"Apple\"}}\n        ]|]\n        { matchStatus  = 206\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-3/1200\" ]\n        }\n      request methodGet \"/projects?select=name,clients()&clients=is.null\"\n        [(\"Prefer\", \"count=estimated\")] \"\"\n       `shouldRespondWith`\n        [json|[{\"name\":\"Orphan\"}]|]\n        { matchStatus  = 200\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-0/1\" ]\n        }\n      request methodGet \"/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)\"\n        [(\"Prefer\", \"count=estimated\")] \"\"\n       `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"name\":\"Walmart\"},\n          {\"id\":2,\"name\":\"Target\"}\n        ]|]\n        { matchStatus  = 206\n        , matchHeaders = [ matchContentTypeJson\n                         , \"Content-Range\" <:> \"0-1/952\" ]\n        }\n"
  },
  {
    "path": "test/spec/Feature/Query/RpcSpec.hs",
    "content": "module Feature.Query.RpcSpec where\n\nimport qualified Data.ByteString.Lazy as BL (empty)\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))\n\nimport Network.HTTP.Types\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\nimport Text.Heredoc\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"remote procedure call\" $ do\n    context \"a proc that returns a set\" $ do\n      context \"returns paginated results\" $ do\n        it \"using the Range header\" $\n          request methodGet \"/rpc/getitemrange?min=2&max=4\"\n                  (rangeHdrs (ByteRangeFromTo 1 1)) mempty\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 200\n              , matchHeaders = [\"Content-Range\" <:> \"1-1/*\"]\n              }\n\n        it \"using limit and offset\" $ do\n          post \"/rpc/getitemrange?limit=1&offset=1\" [json| { \"min\": 2, \"max\": 4 } |]\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 200\n              , matchHeaders = [ \"Content-Range\" <:> \"1-1/*\"\n                               , \"Content-Length\" <:> \"10\" ]\n              }\n          get \"/rpc/getitemrange?min=2&max=4&limit=1&offset=1\"\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 200\n              , matchHeaders = [ \"Content-Range\" <:> \"1-1/*\"\n                               , \"Content-Length\" <:> \"10\"]\n              }\n          request methodHead \"/rpc/getitemrange?min=2&max=4&limit=1&offset=1\" mempty mempty\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 200\n              , matchHeaders = [ matchContentTypeJson\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"1-1/*\" ]\n              }\n\n      context \"includes total count if requested\" $ do\n        it \"using the Range header\" $\n          request methodGet \"/rpc/getitemrange?min=2&max=4\"\n                  (rangeHdrsWithCount (ByteRangeFromTo 1 1)) \"\"\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 206 -- it now knows the response is partial\n              , matchHeaders = [\"Content-Range\" <:> \"1-1/2\"]\n              }\n\n        it \"using limit and offset\" $ do\n          request methodPost \"/rpc/getitemrange?limit=1&offset=1\"\n                  [(\"Prefer\", \"count=exact\")]\n                  [json| { \"min\": 2, \"max\": 4 } |]\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 206 -- it now knows the response is partial\n              , matchHeaders = [\"Content-Range\" <:> \"1-1/2\"]\n              }\n          request methodGet \"/rpc/getitemrange?min=2&max=4&limit=1&offset=1\"\n                  [(\"Prefer\", \"count=exact\")] mempty\n             `shouldRespondWith` [json| [{\"id\":4}] |]\n              { matchStatus = 206\n              , matchHeaders = [\"Content-Range\" <:> \"1-1/2\"]\n              }\n          request methodHead \"/rpc/getitemrange?min=2&max=4&limit=1&offset=1\"\n              [(\"Prefer\", \"count=exact\")] mempty\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 206\n              , matchHeaders = [ matchContentTypeJson\n                               , \"Content-Range\" <:> \"1-1/2\" ]\n              }\n\n      it \"includes exact count if requested\" $ do\n        request methodHead \"/rpc/getallprojects\"\n                [(\"Prefer\", \"count=exact\")] \"\"\n           `shouldRespondWith` \"\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-4/5\"]\n            }\n        request methodHead \"/rpc/getallprojects?select=*,clients!inner(*)&clients.id=eq.1\"\n                [(\"Prefer\", \"count=exact\")] \"\"\n           `shouldRespondWith` \"\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-1/2\"]\n            }\n\n      it \"includes exact count of 1 for functions that return a single scalar, domain or composite\" $ do\n        request methodGet \"/rpc/add_them?a=3&b=4\"\n                [(\"Prefer\", \"count=exact\")] \"\"\n           `shouldRespondWith` \"7\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-0/1\"]\n            }\n        request methodGet \"/rpc/ret_domain?val=8\"\n                [(\"Prefer\", \"count=exact\")] \"\"\n           `shouldRespondWith` \"8\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-0/1\"]\n            }\n        request methodGet \"/rpc/ret_point_2d\"\n                [(\"Prefer\", \"count=exact\")] \"\"\n           `shouldRespondWith`\n            [json|{\"x\": 10, \"y\": 5}|]\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-0/1\"]\n            }\n\n      it \"returns proper json\" $ do\n        post \"/rpc/getitemrange\" [json| { \"min\": 2, \"max\": 4 } |] `shouldRespondWith`\n          [json| [ {\"id\": 3}, {\"id\":4} ] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/getitemrange?min=2&max=4\" `shouldRespondWith`\n          [json| [ {\"id\": 3}, {\"id\":4} ] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns CSV\" $ do\n        request methodPost \"/rpc/getitemrange\"\n                (acceptHdrs \"text/csv\")\n                [json| { \"min\": 2, \"max\": 4 } |]\n           `shouldRespondWith` \"id\\n3\\n4\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n            }\n        request methodGet \"/rpc/getitemrange?min=2&max=4\"\n                (acceptHdrs \"text/csv\") \"\"\n           `shouldRespondWith` \"id\\n3\\n4\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n            }\n        request methodHead \"/rpc/getitemrange?min=2&max=4\"\n                (acceptHdrs \"text/csv\") \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 200\n            , matchHeaders = [\"Content-Type\" <:> \"text/csv; charset=utf-8\"]\n            }\n\n      context \"ignores Range header when method is different than GET\" $ do\n        it \"without limit and offset\" $ do\n          request methodPost \"/rpc/getitemrange\"\n                  (rangeHdrsWithCount (ByteRangeFromTo 1 1))\n                  [json| { \"min\": 2, \"max\": 4 } |]\n             `shouldRespondWith` [json| [{\"id\": 3}, {\"id\": 4}] |]\n              { matchStatus = 200\n              , matchHeaders = [\"Content-Range\" <:> \"0-1/2\"]\n              }\n          request methodHead \"/rpc/getitemrange?min=2&max=4\"\n              (rangeHdrsWithCount (ByteRangeFromTo 1 1)) \"\"\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 200\n              , matchHeaders = [ matchContentTypeJson\n                               , \"Content-Range\" <:> \"0-1/2\" ]\n              }\n\n        it \"with limit and offset\" $ do\n          request methodPost \"/rpc/getitemrange?limit=2&offset=1\"\n                  (rangeHdrsWithCount (ByteRangeFromTo 1 1))\n                  [json| { \"min\": 2, \"max\": 5 } |]\n             `shouldRespondWith` [json| [{\"id\": 4}, {\"id\": 5}] |]\n              { matchStatus = 206\n              , matchHeaders = [\"Content-Range\" <:> \"1-2/3\"]\n              }\n          request methodHead \"/rpc/getitemrange?min=2&max=5&limit=2&offset=1\"\n              (rangeHdrsWithCount (ByteRangeFromTo 1 1)) \"\"\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 206\n              , matchHeaders = [ matchContentTypeJson\n                               , \"Content-Range\" <:> \"1-2/3\" ]\n              }\n\n        it \"does not throw an invalid range error\" $ do\n          request methodPost \"/rpc/getitemrange?limit=2&offset=1\"\n                  (rangeHdrsWithCount (ByteRangeFromTo 0 0))\n                  [json| { \"min\": 2, \"max\": 5 } |]\n             `shouldRespondWith` [json| [{\"id\": 4}, {\"id\": 5}] |]\n              { matchStatus = 206\n              , matchHeaders = [\"Content-Range\" <:> \"1-2/3\"]\n              }\n          request methodHead \"/rpc/getitemrange?min=2&max=5&limit=2&offset=1\"\n              (rangeHdrsWithCount (ByteRangeFromTo 0 0)) \"\"\n            `shouldRespondWith`\n              \"\"\n              { matchStatus = 206\n              , matchHeaders = [ matchContentTypeJson\n                               , \"Content-Range\" <:> \"1-2/3\" ]\n              }\n\n    context \"unknown function\" $ do\n      it \"returns 404\" $\n        post \"/rpc/fakefunc\" [json| {} |] `shouldRespondWith` 404\n\n      it \"should fail with 404 on unknown proc name\" $\n        get \"/rpc/fake\" `shouldRespondWith` 404\n\n      it \"should fail with 404 and hint the closest proc on unknown proc name\" $\n        get \"/rpc/sayhell\" `shouldRespondWith`\n          [json| {\n            \"hint\":\"Perhaps you meant to call the function test.sayhello\",\n            \"message\":\"Could not find the function test.sayhell without parameters in the schema cache\",\n            \"code\":\"PGRST202\",\n            \"details\":\"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache.\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [ \"Content-Length\" <:> \"291\"\n                           , matchContentTypeJson ]\n          }\n\n      it \"should fail with 404 on unknown proc args\" $ do\n        get \"/rpc/sayhello\" `shouldRespondWith` 404\n        get \"/rpc/sayhello?any_arg=value\" `shouldRespondWith` 404\n\n      it \"should fail with 404 and hint the closest args on unknown proc args\" $\n        get \"/rpc/sayhello?nam=Peter\" `shouldRespondWith`\n          [json| {\n            \"hint\":\"Perhaps you meant to call the function test.sayhello(name)\",\n            \"message\":\"Could not find the function test.sayhello(nam) in the schema cache\",\n            \"code\":\"PGRST202\",\n            \"details\":\"Searched for the function test.sayhello with parameter nam, but no matches were found in the schema cache.\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"should not ignore unknown args and fail with 404\" $\n        get \"/rpc/add_them?a=1&b=2&smthelse=blabla\" `shouldRespondWith`\n        [json| {\n          \"hint\":\"Perhaps you meant to call the function test.add_them(a, b)\",\n          \"message\":\"Could not find the function test.add_them(a, b, smthelse) in the schema cache\",\n          \"code\":\"PGRST202\",\n          \"details\":\"Searched for the function test.add_them with parameters a, b, smthelse, but no matches were found in the schema cache.\"} |]\n        { matchStatus  = 404\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n      it \"should fail with 404 for overloaded functions with unknown args\" $ do\n        get \"/rpc/overloaded?wrong_arg=value\" `shouldRespondWith`\n          [json| {\n            \"hint\":null,\n            \"message\":\"Could not find the function test.overloaded(wrong_arg) in the schema cache\",\n            \"code\":\"PGRST202\",\n            \"details\":\"Searched for the function test.overloaded with parameter wrong_arg, but no matches were found in the schema cache.\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/rpc/overloaded?a=1&b=2&wrong_arg=value\" `shouldRespondWith`\n          [json| {\n            \"hint\":\"Perhaps you meant to call the function test.overloaded(a, b, c)\",\n            \"message\":\"Could not find the function test.overloaded(a, b, wrong_arg) in the schema cache\",\n            \"code\":\"PGRST202\",\n            \"details\":\"Searched for the function test.overloaded with parameters a, b, wrong_arg, but no matches were found in the schema cache.\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"ambiguous overloaded functions with same parameters' names but different types\" $ do\n      it \"should fail with 300 Multiple Choices without explicit type casts\" $\n        get \"/rpc/overloaded_same_args?arg=value\" `shouldRespondWith`\n          [json| {\n            \"hint\":\"Try renaming the parameters or the function itself in the database so function overloading can be resolved\",\n            \"message\":\"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)\",\n            \"code\":\"PGRST203\",\n            \"details\":null} |]\n          { matchStatus  = 300\n          , matchHeaders = [ \"Content-Length\" <:> \"353\"\n                           , matchContentTypeJson ]\n          }\n\n    it \"works when having uppercase identifiers\" $ do\n      get \"/rpc/quotedFunction?user=mscott&fullName=Michael Scott&SSN=401-32-XXXX\" `shouldRespondWith`\n        [json|{\"user\": \"mscott\", \"fullName\": \"Michael Scott\", \"SSN\": \"401-32-XXXX\"}|]\n        { matchHeaders = [matchContentTypeJson] }\n      post \"/rpc/quotedFunction\"\n        [json|{\"user\": \"dschrute\", \"fullName\": \"Dwight Schrute\", \"SSN\": \"030-18-XXXX\"}|]\n        `shouldRespondWith`\n        [json|{\"user\": \"dschrute\", \"fullName\": \"Dwight Schrute\", \"SSN\": \"030-18-XXXX\"}|]\n        { matchHeaders = [matchContentTypeJson] }\n\n    context \"shaping the response returned by a proc\" $ do\n      it \"returns a project\" $ do\n        post \"/rpc/getproject\" [json| { \"id\": 1} |] `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}]|]\n        get \"/rpc/getproject?id=1\" `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}]|]\n\n      it \"can filter proc results\" $ do\n        post \"/rpc/getallprojects?id=gt.1&id=lt.5&select=id\" [json| {} |] `shouldRespondWith`\n          [json|[{\"id\":2},{\"id\":3},{\"id\":4}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/getallprojects?id=gt.1&id=lt.5&select=id\" `shouldRespondWith`\n          [json|[{\"id\":2},{\"id\":3},{\"id\":4}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can limit proc results\" $ do\n        post \"/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1\" [json| {} |]\n          `shouldRespondWith` [json|[{\"id\":3},{\"id\":4}]|]\n             { matchStatus = 200\n             , matchHeaders = [\"Content-Range\" <:> \"1-2/*\"] }\n        get \"/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1\"\n          `shouldRespondWith` [json|[{\"id\":3},{\"id\":4}]|]\n             { matchStatus = 200\n             , matchHeaders = [\"Content-Range\" <:> \"1-2/*\"] }\n\n      it \"select works on the first level\" $ do\n        post \"/rpc/getproject?select=id,name\" [json| { \"id\": 1} |] `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\"}]|]\n        get \"/rpc/getproject?id=1&select=id,name\" `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\"}]|]\n\n    context \"foreign entities embedding\" $ do\n      it \"can embed if related tables are in the exposed schema\" $ do\n        post \"/rpc/getproject?select=id,name,client:clients(id),tasks(id)\" [json| { \"id\": 1} |] `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1},\"tasks\":[{\"id\":1},{\"id\":2}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/getproject?id=1&select=id,name,client:clients(id),tasks(id)\" `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1},\"tasks\":[{\"id\":1},{\"id\":2}]}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"cannot embed if the related table is not in the exposed schema\" $ do\n        post \"/rpc/single_article?select=*,article_stars(*)\" [json|{ \"id\": 1}|]\n          `shouldRespondWith` 400\n        get \"/rpc/single_article?id=1&select=*,article_stars(*)\"\n          `shouldRespondWith` 400\n\n      it \"can embed if the related tables are in a hidden schema but exposed as views\" $ do\n        post \"/rpc/single_article?select=id,articleStars(userId)\"\n            [json|{ \"id\": 2}|]\n          `shouldRespondWith`\n            [json|{\"id\": 2, \"articleStars\": [{\"userId\": 3}]}|]\n        get \"/rpc/single_article?id=2&select=id,articleStars(userId)\"\n          `shouldRespondWith`\n            [json|{\"id\": 2, \"articleStars\": [{\"userId\": 3}]}|]\n\n      it \"can embed an M2M relationship table\" $ do\n        get \"/rpc/getallusers?select=name,tasks(name)&id=gt.1\"\n          `shouldRespondWith` [json|[\n            {\"name\":\"Michael Scott\", \"tasks\":[{\"name\":\"Design IOS\"}, {\"name\":\"Code IOS\"}, {\"name\":\"Design OSX\"}]},\n            {\"name\":\"Dwight Schrute\",\"tasks\":[{\"name\":\"Design w7\"}, {\"name\":\"Design IOS\"}]}\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n        -- https://github.com/PostgREST/postgrest/issues/2565\n        get \"/rpc/get_yards?select=groups(*)\"\n          `shouldRespondWith` [json|[]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can embed an M2M relationship table that has a parent relationship table\" $\n        get \"/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1\"\n          `shouldRespondWith` [json|[\n            {\"name\":\"Michael Scott\",\"tasks\":[\n              {\"name\":\"Design IOS\",\"project\":{\"name\":\"IOS\"}},\n              {\"name\":\"Code IOS\",\"project\":{\"name\":\"IOS\"}},\n              {\"name\":\"Design OSX\",\"project\":{\"name\":\"OSX\"}}\n            ]},\n            {\"name\":\"Dwight Schrute\",\"tasks\":[\n              {\"name\":\"Design w7\",\"project\":{\"name\":\"Windows 7\"}},\n              {\"name\":\"Design IOS\",\"project\":{\"name\":\"IOS\"}}\n            ]}\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can embed an O2O relationship\" $ do\n        get \"/rpc/allcapitals?select=name,country(name)\"\n          `shouldRespondWith` [json|[\n            {\"name\":\"Kabul\",\"country\":{\"name\":\"Afghanistan\"}},\n            {\"name\":\"Algiers\",\"country\":{\"name\":\"Algeria\"}}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/allcountries?select=name,capital(name)\"\n          `shouldRespondWith` [json|[\n            {\"name\":\"Afghanistan\",\"capital\":{\"name\":\"Kabul\"}},\n            {\"name\":\"Algeria\",\"capital\":{\"name\":\"Algiers\"}}\n          ]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"can embed if rpc returns domain of table type\" $ do\n        post \"/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)\"\n            [json| { \"id\": 1} |]\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1},\"tasks\":[{\"id\":1},{\"id\":2}]}]|]\n        get \"/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)\"\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\",\"client\":{\"id\":1},\"tasks\":[{\"id\":1},{\"id\":2}]}]|]\n\n    context \"a proc that returns an empty rowset\" $\n      it \"returns empty json array\" $ do\n        post \"/rpc/test_empty_rowset\" [json| {} |] `shouldRespondWith`\n          [json| [] |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/test_empty_rowset\" `shouldRespondWith`\n          [json| [] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"proc return types\" $ do\n      context \"returns text\" $ do\n        it \"returns proper json\" $\n          post \"/rpc/sayhello\" [json| { \"name\": \"world\" } |] `shouldRespondWith`\n            [json|\"Hello, world\"|]\n            { matchHeaders = [matchContentTypeJson] }\n\n        it \"can handle unicode\" $\n          post \"/rpc/sayhello\" [json| { \"name\": \"￥\" } |] `shouldRespondWith`\n            [json|\"Hello, ￥\"|]\n            { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns array\" $\n        post \"/rpc/ret_array\" [json|{}|] `shouldRespondWith`\n          [json|[1, 2, 3]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns setof integers\" $\n        post \"/rpc/ret_setof_integers\"\n            [json|{}|]\n          `shouldRespondWith`\n            [json|[1,2,3]|]\n\n      it \"returns enum value\" $\n        post \"/rpc/ret_enum\" [json|{ \"val\": \"foo\" }|] `shouldRespondWith`\n          [json|\"foo\"|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns domain value\" $\n        post \"/rpc/ret_domain\" [json|{ \"val\": \"8\" }|] `shouldRespondWith`\n          [json|8|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns range\" $\n        post \"/rpc/ret_range\" [json|{ \"low\": 10, \"up\": 20 }|] `shouldRespondWith`\n          [json|\"[10,20)\"|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns row of scalars\" $\n        post \"/rpc/ret_scalars\" [json|{}|] `shouldRespondWith`\n          [json|[{\"a\":\"scalars\", \"b\":\"foo\", \"c\":1, \"d\":\"[10,20)\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"returns composite type in exposed schema\" $\n        post \"/rpc/ret_point_2d\"\n            [json|{}|]\n          `shouldRespondWith`\n            [json|{\"x\": 10, \"y\": 5}|]\n\n      it \"cannot return composite type in hidden schema\" $\n        post \"/rpc/ret_point_3d\" [json|{}|] `shouldRespondWith` 401\n\n      it \"returns domain of composite type\" $\n        post \"/rpc/ret_composite_domain\"\n            [json|{}|]\n          `shouldRespondWith`\n            [json|{\"x\": 10, \"y\": 5}|]\n\n      it \"returns single row from table\" $\n        post \"/rpc/single_article?select=id\"\n            [json|{\"id\": 2}|]\n          `shouldRespondWith`\n            [json|{\"id\": 2}|]\n\n      it \"returns 204, no Content-Type header and no content for void\" $\n        post \"/rpc/ret_void\"\n            [json|{}|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength]\n            }\n\n      it \"returns null for an integer with null value\" $\n        post \"/rpc/ret_null\"\n            [json|{}|]\n          `shouldRespondWith`\n            [json|null|]\n\n      it \"returns a record type\" $ do\n        post \"/rpc/returns_record\"\n          \"\"\n         `shouldRespondWith`\n          [json|{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}|]\n        post \"/rpc/returns_record_params\"\n          [json|{\"id\":1, \"name\": \"Windows%\"}|]\n         `shouldRespondWith`\n          [json|{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}|]\n\n      it \"returns a setof record type\" $ do\n        post \"/rpc/returns_setof_record\"\n          \"\"\n         `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1},{\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}]|]\n        post \"/rpc/returns_setof_record_params\"\n          [json|{\"id\":1,\"name\":\"Windows%\"}|]\n         `shouldRespondWith`\n          [json|[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1},{\"id\":2,\"name\":\"Windows 10\",\"client_id\":1}]|]\n\n      context \"different types when overloaded\" $ do\n        it \"returns composite type\" $\n          post \"/rpc/ret_point_overloaded\"\n              [json|{\"x\": 1, \"y\": 2}|]\n            `shouldRespondWith`\n              [json|{\"x\": 1, \"y\": 2}|]\n\n    context \"proc argument types\" $ do\n      it \"accepts a variety of arguments (Postgres >= 10)\" $\n        post \"/rpc/varied_arguments\"\n            [json| {\n              \"double\": 3.1,\n              \"varchar\": \"hello\",\n              \"boolean\": true,\n              \"date\": \"20190101\",\n              \"money\": 0,\n              \"enum\": \"foo\",\n              \"arr\": [\"a\", \"b\", \"c\"],\n              \"integer\": 43,\n              \"json\": {\"some key\": \"some value\"},\n              \"jsonb\": {\"another key\": [1, 2, \"3\"]}\n            } |]\n          `shouldRespondWith`\n            [json| {\n              \"double\": 3.1,\n              \"varchar\": \"hello\",\n              \"boolean\": true,\n              \"date\": \"2019-01-01\",\n              \"money\": \"$0.00\",\n              \"enum\": \"foo\",\n              \"arr\": [\"a\", \"b\", \"c\"],\n              \"integer\": 43,\n              \"json\": {\"some key\": \"some value\"},\n              \"jsonb\": {\"another key\": [1, 2, \"3\"]}\n            } |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      it \"accepts a variety of arguments with GET\" $\n        -- without JSON / JSONB here, because passing those via query string is useless - they just become a \"json string\" all the time\n        get \"/rpc/varied_arguments?double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43\"\n          `shouldRespondWith`\n              [json| {\n                \"double\": 3.1,\n                \"varchar\": \"hello\",\n                \"boolean\": true,\n                \"date\": \"2019-01-01\",\n                \"money\": \"$0.00\",\n                \"enum\": \"foo\",\n                \"arr\": [\"a\", \"b\", \"c\"],\n                \"integer\": 43,\n                \"json\": {},\n                \"jsonb\": {}\n              } |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      it \"accepts a variety of arguments from an html form\" $\n        request methodPost \"/rpc/varied_arguments\"\n            [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n            \"double=3.1&varchar=hello&boolean=true&date=20190101&money=0&enum=foo&arr=%7Ba,b,c%7D&integer=43\"\n          `shouldRespondWith`\n              [json| {\n                \"double\": 3.1,\n                \"varchar\": \"hello\",\n                \"boolean\": true,\n                \"date\": \"2019-01-01\",\n                \"money\": \"$0.00\",\n                \"enum\": \"foo\",\n                \"arr\": [\"a\", \"b\", \"c\"],\n                \"integer\": 43,\n                \"json\": {},\n                \"jsonb\": {}\n              } |]\n            { matchHeaders = [matchContentTypeJson] }\n\n      it \"parses embedded JSON arguments as JSON\" $\n        post \"/rpc/json_argument\"\n            [json| { \"arg\": { \"key\": 3 } } |]\n          `shouldRespondWith`\n            [json|\"object\"|]\n            { matchHeaders = [matchContentTypeJson] }\n\n      it \"parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)\" $\n        post \"/rpc/json_argument\"\n            [json| { \"arg\": \"{ \\\"key\\\": 3 }\" } |]\n          `shouldRespondWith`\n            [json|\"string\"|]\n            { matchHeaders = [matchContentTypeJson] }\n\n    context \"improper input\" $ do\n      it \"rejects unknown content type even if payload is good\" $ do\n        request methodPost \"/rpc/sayhello\"\n          (acceptHdrs \"audio/mpeg3\") [json| { \"name\": \"world\" } |]\n            `shouldRespondWith` 406\n        request methodGet \"/rpc/sayhello?name=world\"\n          (acceptHdrs \"audio/mpeg3\") \"\"\n            `shouldRespondWith` 406\n      it \"rejects malformed json payload\" $ do\n        p <- request methodPost \"/rpc/sayhello\"\n          (acceptHdrs \"application/json\") \"sdfsdf\"\n        liftIO $ do\n          simpleStatus p `shouldBe` badRequest400\n          isErrorFormat (simpleBody p) `shouldBe` True\n      it \"treats simple plpgsql raise as invalid input\" $ do\n        p <- post \"/rpc/problem\" \"{}\"\n        liftIO $ do\n          simpleStatus p `shouldBe` badRequest400\n          isErrorFormat (simpleBody p) `shouldBe` True\n      it \"treats plpgsql assert as internal server error\" $ do\n        p <- post \"/rpc/assert\" \"{}\"\n        liftIO $ do\n          simpleStatus p `shouldBe` internalServerError500\n          isErrorFormat (simpleBody p) `shouldBe` True\n\n    context \"unsupported method\" $ do\n      it \"DELETE fails\" $\n        request methodDelete \"/rpc/sayhello\" [] \"\"\n          `shouldRespondWith`\n          [json|{\"message\":\"Cannot use the DELETE method on RPC\",\"code\":\"PGRST101\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 405\n          , matchHeaders = [ \"Content-Length\" <:> \"94\"\n                           , matchContentTypeJson ]\n          }\n      it \"PATCH fails\" $\n        request methodPatch \"/rpc/sayhello\" [] \"\"\n          `shouldRespondWith` 405\n\n    it \"executes the proc exactly once per request\" $ do\n      -- callcounter is persistent even with rollback, because it uses a sequence\n      -- reset counter first to make test repeatable\n      request methodPost \"/rpc/reset_sequence\"\n          [(\"Prefer\", \"tx=commit\")]\n          [json|{\"name\": \"callcounter_count\", \"value\": 1}|]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , matchHeaderAbsent hContentLength]\n          }\n\n      -- now the test\n      post \"/rpc/callcounter\"\n          [json|{}|]\n        `shouldRespondWith`\n          [json|1|]\n\n      post \"/rpc/callcounter\"\n          [json|{}|]\n        `shouldRespondWith`\n          [json|2|]\n\n    context \"a proc that receives no parameters\" $ do\n      it \"interprets empty string as empty json object on a post request\" $\n        post \"/rpc/noparamsproc\" BL.empty `shouldRespondWith`\n          [json| \"Return value of no parameters procedure.\" |]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"interprets empty string as a function with no args on a get request\" $\n        get \"/rpc/noparamsproc\" `shouldRespondWith`\n          [json| \"Return value of no parameters procedure.\" |]\n          { matchHeaders = [matchContentTypeJson] }\n\n    it \"returns proper output when having the same return col name as the proc name\" $ do\n      post \"/rpc/test\" [json|{}|] `shouldRespondWith`\n        [json|[{\"test\":\"hello\",\"value\":1}]|] { matchHeaders = [matchContentTypeJson] }\n      get \"/rpc/test\" `shouldRespondWith`\n        [json|[{\"test\":\"hello\",\"value\":1}]|] { matchHeaders = [matchContentTypeJson] }\n\n    context \"procs with OUT/INOUT params\" $ do\n      it \"returns an object result when there is a single OUT param\" $ do\n        get \"/rpc/single_out_param?num=5\"\n          `shouldRespondWith`\n            [json|{\"num_plus_one\":6}|]\n\n        get \"/rpc/single_json_out_param?a=1&b=two\"\n          `shouldRespondWith`\n            [json|{\"my_json\": {\"a\": 1, \"b\": \"two\"}}|]\n\n      it \"returns an object result when there is a single INOUT param\" $\n        get \"/rpc/single_inout_param?num=2\"\n          `shouldRespondWith`\n            [json|{\"num\":3}|]\n\n      it \"returns an object result when there are many OUT params\" $\n        get \"/rpc/many_out_params\"\n          `shouldRespondWith`\n            [json|{\"my_json\":{\"a\": 1, \"b\": \"two\"},\"num\":3,\"str\":\"four\"}|]\n\n      it \"returns an object result when there are many INOUT params\" $\n        get \"/rpc/many_inout_params?num=1&str=two&b=false\"\n          `shouldRespondWith`\n            [json|{\"num\":1,\"str\":\"two\",\"b\":false}|]\n\n    context \"procs with TABLE return\" $ do\n      it \"returns an object result when there is a single-column TABLE return type\" $\n        get \"/rpc/single_column_table_return\"\n          `shouldRespondWith`\n            [json|[{\"a\": \"A\"}]|]\n\n      it \"returns an object result when there is a multi-column TABLE return type\" $\n        get \"/rpc/multi_column_table_return\"\n          `shouldRespondWith`\n            [json|[{\"a\": \"A\", \"b\": \"B\"}]|]\n\n    context \"procs with VARIADIC params\" $ do\n      it \"works with POST (Postgres >= 10)\" $\n        post \"/rpc/variadic_param\"\n            [json| { \"v\": [\"hi\", \"hello\", \"there\"] } |]\n          `shouldRespondWith`\n            [json|[\"hi\", \"hello\", \"there\"]|]\n\n      context \"works with GET and repeated params\" $ do\n        it \"n=0 (through DEFAULT)\" $\n          get \"/rpc/variadic_param\"\n            `shouldRespondWith`\n              [json|[]|]\n\n        it \"n=1\" $\n          get \"/rpc/variadic_param?v=hi\"\n            `shouldRespondWith`\n              [json|[\"hi\"]|]\n\n        it \"n>1\" $\n          get \"/rpc/variadic_param?v=hi&v=there\"\n            `shouldRespondWith`\n              [json|[\"hi\", \"there\"]|]\n\n      context \"works with POST and repeated params from html form\" $ do\n        it \"n=0 (through DEFAULT)\" $\n          request methodPost \"/rpc/variadic_param\"\n              [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n              \"\"\n            `shouldRespondWith`\n              [json|[]|]\n\n        it \"n=1\" $\n          request methodPost \"/rpc/variadic_param\"\n              [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n              \"v=hi\"\n            `shouldRespondWith`\n              [json|[\"hi\"]|]\n\n        it \"n>1\" $\n          request methodPost \"/rpc/variadic_param\"\n              [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n              \"v=hi&v=there\"\n            `shouldRespondWith`\n              [json|[\"hi\", \"there\"]|]\n\n    it \"returns last value for repeated params without VARIADIC\" $\n      get \"/rpc/sayhello?name=ignored&name=world\"\n        `shouldRespondWith`\n          [json|\"Hello, world\"|]\n\n    it \"returns last value for repeated non-variadic params in function with other VARIADIC arguments\" $\n      get \"/rpc/sayhello_variadic?name=ignored&name=world&v=unused\"\n        `shouldRespondWith`\n          [json|\"Hello, world\"|]\n\n    it \"can handle procs with args that have a DEFAULT value\" $ do\n      get \"/rpc/many_inout_params?num=1&str=two\"\n        `shouldRespondWith`\n          [json| {\"num\":1,\"str\":\"two\",\"b\":true}|]\n      get \"/rpc/three_defaults?b=4\"\n        `shouldRespondWith`\n          [json|8|]\n\n    it \"can map a RAISE error code and message to a http status\" $\n      get \"/rpc/raise_pt402\"\n        `shouldRespondWith` [json|{ \"hint\": \"Upgrade your plan\", \"details\": \"Quota exceeded\", \"code\": \"PT402\", \"message\": \"Payment Required\" }|]\n        { matchStatus  = 402\n        , matchHeaders = [ \"Content-Length\" <:> \"99\"\n                         , matchContentTypeJson ]\n        }\n\n    it \"defaults to status 500 if RAISE code is PT not followed by a number\" $\n      get \"/rpc/raise_bad_pt\"\n        `shouldRespondWith`\n        [json|{\"hint\": null, \"details\": null, \"code\": \"PT40A\", \"message\": \"Wrong\"}|]\n        { matchStatus  = 500\n        , matchHeaders = [ \"Content-Length\" <:> \"61\"\n                         , matchContentTypeJson ]\n        }\n\n    context \"should work with an overloaded function\" $ do\n      it \"overloaded()\" $\n        get \"/rpc/overloaded\"\n          `shouldRespondWith`\n            [json|[1,2,3]|]\n\n      it \"overloaded(int, int)\" $\n        get \"/rpc/overloaded?a=1&b=2\" `shouldRespondWith` [str|3|]\n\n      it \"overloaded(text, text, text)\" $\n        get \"/rpc/overloaded?a=1&b=2&c=3\" `shouldRespondWith` [json|\"123\"|]\n\n      it \"overloaded_html_form()\" $\n        request methodPost \"/rpc/overloaded_html_form\"\n            [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n            \"\"\n          `shouldRespondWith`\n            [json|[1,2,3]|]\n\n      it \"overloaded_html_form(int, int)\" $\n        request methodPost \"/rpc/overloaded_html_form\"\n            [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n            \"a=1&b=2\"\n          `shouldRespondWith`\n            [str|3|]\n\n      it \"overloaded_html_form(text, text, text)\" $\n        request methodPost \"/rpc/overloaded_html_form\"\n            [(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n            \"a=1&b=2&c=3\"\n          `shouldRespondWith`\n            [json|\"123\"|]\n\n      -- https://github.com/PostgREST/postgrest/issues/1672\n      context \"embedding overloaded functions with the same signature except for the last param with a default value\" $ do\n        it \"overloaded_default(text default)\" $ do\n          request methodPost \"/rpc/overloaded_default?select=id,name,users(name)\"\n              [(\"Content-Type\", \"application/json\")]\n              [json|{}|]\n            `shouldRespondWith`\n              [json|[{\"id\": 2, \"name\": \"Code w7\", \"users\": [{\"name\": \"Angela Martin\"}]}] |]\n\n        it \"overloaded_default(int)\" $\n          request methodPost \"/rpc/overloaded_default\"\n              [(\"Content-Type\", \"application/json\")]\n              [json|{\"must_param\":1}|]\n            `shouldRespondWith`\n              [json|{\"val\":1}|]\n\n        it \"overloaded_default(int, text default)\" $ do\n          request methodPost \"/rpc/overloaded_default?select=id,name,users(name)\"\n              [(\"Content-Type\", \"application/json\")]\n              [json|{\"a\":4}|]\n            `shouldRespondWith`\n              [json|[{\"id\": 5, \"name\": \"Design IOS\", \"users\": [{\"name\": \"Michael Scott\"}, {\"name\": \"Dwight Schrute\"}]}] |]\n\n        it \"overloaded_default(int, int)\" $\n          request methodPost \"/rpc/overloaded_default\"\n              [(\"Content-Type\", \"application/json\")]\n              [json|{\"a\":2,\"must_param\":4}|]\n            `shouldRespondWith`\n              [json|{\"a\":2,\"val\":4}|]\n\n    context \"only for POST rpc\" $ do\n      it \"gives a parse filter error if GET style proc args are specified\" $\n        post \"/rpc/sayhello?name=John\" [json|{name: \"John\"}|] `shouldRespondWith` 400\n\n      it \"ignores json keys not included in ?columns\" $\n        post \"/rpc/sayhello?columns=name\"\n          [json|{\"name\": \"John\", \"smth\": \"here\", \"other\": \"stuff\", \"fake_id\": 13}|]\n          `shouldRespondWith`\n          [json|\"Hello, John\"|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"only takes the first object in case of array of objects payload\" $\n        post \"/rpc/add_them\"\n          [json|[\n            {\"a\": 1, \"b\": 2},\n            {\"a\": 4, \"b\": 6},\n            {\"a\": 100, \"b\": 200} ]|]\n          `shouldRespondWith` \"3\"\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"HTTP request env vars\" $ do\n      it \"custom header is set\" $\n        request methodPost \"/rpc/get_guc_value\"\n                  [(\"Custom-Header\", \"test\")]\n            [json| { \"prefix\": \"request.headers\", \"name\": \"custom-header\" } |]\n            `shouldRespondWith`\n            [json|\"test\"|]\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n      it \"standard header is set\" $\n        request methodPost \"/rpc/get_guc_value\"\n                  [(\"Origin\", \"http://example.com\")]\n            [json| { \"prefix\": \"request.headers\", \"name\": \"origin\" } |]\n            `shouldRespondWith`\n            [json|\"http://example.com\"|]\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n      it \"current role is available as GUC claim\" $\n        request methodPost \"/rpc/get_guc_value\" []\n            [json| { \"prefix\": \"request.jwt.claims\", \"name\": \"role\" } |]\n            `shouldRespondWith`\n            [json|\"postgrest_test_anonymous\"|]\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n      it \"single cookie ends up as claims\" $\n        request methodPost \"/rpc/get_guc_value\" [(\"Cookie\",\"acookie=cookievalue\")]\n            [json| {\"prefix\": \"request.cookies\", \"name\":\"acookie\"} |]\n            `shouldRespondWith`\n            [json|\"cookievalue\"|]\n            { matchStatus = 200\n            , matchHeaders = []\n            }\n      it \"multiple cookies end up as claims\" $\n        request methodPost \"/rpc/get_guc_value\" [(\"Cookie\",\"acookie=cookievalue;secondcookie=anothervalue\")]\n            [json| {\"prefix\": \"request.cookies\", \"name\":\"secondcookie\"} |]\n            `shouldRespondWith`\n            [json|\"anothervalue\"|]\n            { matchStatus = 200\n            , matchHeaders = []\n            }\n      it \"app settings available\" $\n        request methodPost \"/rpc/get_guc_value\" []\n          [json| { \"name\": \"app.settings.app_host\" } |]\n            `shouldRespondWith`\n            [json|\"localhost\"|]\n            { matchStatus  = 200\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n      it \"gets the Authorization value\" $\n        request methodPost \"/rpc/get_guc_value\" [authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"]\n            [json| {\"prefix\": \"request.headers\", \"name\":\"authorization\"} |]\n            `shouldRespondWith`\n            [json|\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA\"|]\n            { matchStatus = 200\n            , matchHeaders = []\n            }\n      it \"gets the http method\" $\n        request methodPost \"/rpc/get_guc_value\" []\n          [json| {\"name\":\"request.method\"} |]\n            `shouldRespondWith`\n            [json|\"POST\"|]\n            { matchStatus = 200\n            , matchHeaders = []\n            }\n      it \"gets the http path\" $\n        request methodPost \"/rpc/get_guc_value\" []\n          [json| {\"name\":\"request.path\"} |]\n            `shouldRespondWith`\n            [json|\"/rpc/get_guc_value\"|]\n            { matchStatus = 200\n            , matchHeaders = []\n            }\n\n    context \"only for GET rpc\" $ do\n      it \"should fail on mutating procs\" $ do\n        get \"/rpc/callcounter\" `shouldRespondWith` 405\n        get \"/rpc/setprojects?id_l=1&id_h=5&name=FreeBSD\" `shouldRespondWith` 405\n\n      it \"should filter a proc that has arg name = filter name\" $\n        get \"/rpc/get_projects_below?id=5&id=gt.2&select=id\" `shouldRespondWith`\n          [json|[{ \"id\": 3 }, { \"id\": 4 }]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"should work with filters that have the not operator\" $ do\n        get \"/rpc/get_projects_below?id=5&id=not.gt.2&select=id\" `shouldRespondWith`\n          [json|[{ \"id\": 1 }, { \"id\": 2 }]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_projects_below?id=5&id=not.in.(1,3)&select=id\" `shouldRespondWith`\n          [json|[{ \"id\": 2 }, { \"id\": 4 }]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"should work with filters that use the fts operator on tsvector columns\" $ do\n        get \"/rpc/get_tsearch?text_search_vector=fts(english).impossible\" `shouldRespondWith`\n          [json|[{\"text_search_vector\":\"'fun':5 'imposs':9 'kind':3\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch?text_search_vector=plfts.impossible\" `shouldRespondWith`\n          [json|[{\"text_search_vector\":\"'fun':5 'imposs':9 'kind':3\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat\" `shouldRespondWith`\n          [json|[{\"text_search_vector\":\"'amus':5 'fair':7 'impossibl':9 'peu':4\"},{\"text_search_vector\":\"'art':4 'spass':5 'unmog':7\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch?text_search_vector=wfts.impossible\" `shouldRespondWith`\n          [json|[{\"text_search_vector\":\"'fun':5 'imposs':9 'kind':3\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n      it \"should work with filters that use the fts operator on text and json columns\" $ do\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search&text_search=fts(english).impossible\" `shouldRespondWith`\n          [json|[\n            {\"text_search\":\"It's kind of fun to do the impossible\"},\n            {\"text_search\":\"C'est un peu amusant de faire l'impossible\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search&text_search=plfts.impossible\" `shouldRespondWith`\n          [json|[\n            {\"text_search\":\"It's kind of fun to do the impossible\"},\n            {\"text_search\":\"C'est un peu amusant de faire l'impossible\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search&text_search=not.fts(english).fun%7Crat\" `shouldRespondWith`\n          [json|[\n            {\"text_search\":\"C'est un peu amusant de faire l'impossible\"},\n            {\"text_search\":\"Es ist eine Art Spaß, das Unmögliche zu machen\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search&text_search=wfts.impossible\" `shouldRespondWith`\n          [json|[\n            {\"text_search\":\"It's kind of fun to do the impossible\"},\n            {\"text_search\":\"C'est un peu amusant de faire l'impossible\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"should work with filters that use the fts operator when the column type is a tsvector domain\" $\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).impossible\" `shouldRespondWith`\n          [json|[\n            {\"text_search_domain\":\"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6\"},\n            {\"text_search_domain\":\"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"should work with filters that use the fts operator when the column type is a recursive tsvector domain\" $\n        get \"/rpc/get_tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).impossible\" `shouldRespondWith`\n          [json|[\n            {\"text_search_rec_domain\":\"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6\"},\n            {\"text_search_rec_domain\":\"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3\"}]\n          |]\n          { matchHeaders = [matchContentTypeJson] }\n\n      it \"should work with the phraseto_tsquery function\" $\n        get \"/rpc/get_tsearch?text_search_vector=phfts(english).impossible\" `shouldRespondWith`\n          [json|[{\"text_search_vector\":\"'fun':5 'imposs':9 'kind':3\"}]|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    it \"should work with an argument of custom type in public schema\" $\n        get \"/rpc/test_arg?my_arg=something\" `shouldRespondWith`\n          [json|\"foobar\"|]\n          { matchHeaders = [matchContentTypeJson] }\n\n    context \"GUC headers on function calls\" $ do\n      it \"succeeds setting the headers\" $ do\n        get \"/rpc/get_projects_and_guc_headers?id=eq.2&select=id\"\n          `shouldRespondWith` [json|[{\"id\": 2}]|]\n          {matchHeaders = [\n              matchContentTypeJson,\n              \"X-Test\"   <:> \"key1=val1; someValue; key2=val2\",\n              \"X-Test-2\" <:> \"key1=val1\"]}\n        get \"/rpc/get_int_and_guc_headers?num=1\"\n          `shouldRespondWith` [json|1|]\n          {matchHeaders = [\n              matchContentTypeJson,\n              \"X-Test\"   <:> \"key1=val1; someValue; key2=val2\",\n              \"X-Test-2\" <:> \"key1=val1\"]}\n        post \"/rpc/get_int_and_guc_headers\" [json|{\"num\": 1}|]\n          `shouldRespondWith` [json|1|]\n          {matchHeaders = [\n              matchContentTypeJson,\n              \"X-Test\"   <:> \"key1=val1; someValue; key2=val2\",\n              \"X-Test-2\" <:> \"key1=val1\"]}\n\n      it \"fails when setting headers with wrong json structure\" $ do\n        get \"/rpc/bad_guc_headers_1\"\n          `shouldRespondWith`\n          [json|{\"message\":\"response.headers guc must be a JSON array composed of objects with a single key and a string value\",\"code\":\"PGRST111\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 500\n          , matchHeaders = [ \"Content-Length\" <:> \"157\"\n                           , matchContentTypeJson ]\n          }\n        get \"/rpc/bad_guc_headers_2\"\n          `shouldRespondWith`\n          [json|{\"message\":\"response.headers guc must be a JSON array composed of objects with a single key and a string value\",\"code\":\"PGRST111\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 500\n          , matchHeaders = [ matchContentTypeJson ]\n          }\n        get \"/rpc/bad_guc_headers_3\"\n          `shouldRespondWith`\n          [json|{\"message\":\"response.headers guc must be a JSON array composed of objects with a single key and a string value\",\"code\":\"PGRST111\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 500\n          , matchHeaders = [ matchContentTypeJson ]\n          }\n        post \"/rpc/bad_guc_headers_1\" [json|{}|]\n          `shouldRespondWith`\n          [json|{\"message\":\"response.headers guc must be a JSON array composed of objects with a single key and a string value\",\"code\":\"PGRST111\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 500\n          , matchHeaders = [ matchContentTypeJson ]\n          }\n\n      it \"can set the same http header twice\" $\n        get \"/rpc/set_cookie_twice\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Set-Cookie\" <:> \"sessionid=38afes7a8; HttpOnly; Path=/\"\n                             , \"Set-Cookie\" <:> \"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly\" ]\n            }\n\n      it \"can override the Location header on a trigger\" $\n        post \"/stuff\"\n            [json|[{\"id\": 2, \"name\": \"stuff 2\"}]|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Location\" <:> \"/stuff?id=eq.2&overriden=true\" ]\n            }\n\n      -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535\n      -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests.\n      -- These tests are to ensure no blank headers are added.\n      context \"Blank headers bug\" $ do\n        it \"shouldn't add blank headers on POST\" $ do\n          r <- request methodPost \"/loc_test\" [] [json|{\"id\": \"1\", \"c\": \"c1\"}|]\n          liftIO $ do\n            let respHeaders = simpleHeaders r\n            respHeaders `shouldSatisfy` noBlankHeader\n\n        it \"shouldn't add blank headers on PATCH\" $ do\n          r <- request methodPatch \"/loc_test?id=eq.1\" [] [json|{\"c\": \"c2\"}|]\n          liftIO $ do\n            let respHeaders = simpleHeaders r\n            respHeaders `shouldSatisfy` noBlankHeader\n\n        it \"shouldn't add blank headers on GET\" $ do\n          r <- request methodGet \"/loc_test\" [] \"\"\n          liftIO $ do\n            let respHeaders = simpleHeaders r\n            respHeaders `shouldSatisfy` noBlankHeader\n\n        it \"shouldn't add blank headers on DELETE\" $ do\n          r <- request methodDelete \"/loc_test?id=eq.1\" [] \"\"\n          liftIO $ do\n            let respHeaders = simpleHeaders r\n            respHeaders `shouldSatisfy` noBlankHeader\n\n      context \"GUC status override\" $ do\n        it \"can override the status on RPC\" $\n          get \"/rpc/send_body_status_403\"\n            `shouldRespondWith`\n            [json|{\"message\" : \"invalid user or password\"}|]\n            { matchStatus  = 403\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        it \"can override the status through trigger\" $\n          patch \"/stuff?id=eq.1\"\n              [json|[{\"name\": \"updated stuff 1\"}]|]\n            `shouldRespondWith`\n              205\n\n        it \"fails when setting invalid status guc\" $\n          get \"/rpc/send_bad_status\"\n            `shouldRespondWith`\n            [json|{\"message\":\"response.status guc must be a valid status code\",\"code\":\"PGRST112\",\"details\":null,\"hint\":null}|]\n            { matchStatus  = 500\n            , matchHeaders = [ \"Content-Length\" <:> \"106\"\n                             , matchContentTypeJson ]\n            }\n\n      context \"single unnamed param\" $ do\n        it \"can insert json directly with unnamed parameter\" $\n          post \"/rpc/unnamed_json_param\"\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n            `shouldRespondWith`\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n\n        it \"rejects json body when single param has a name\" $\n          post \"/rpc/named_json_param\"\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n            `shouldRespondWith`\n              [json|{\n                \"code\":\"PGRST202\",\n                \"message\":\"Could not find the function test.named_json_param(A, B, C) in the schema cache\",\n                \"details\":\"Searched for the function test.named_json_param with parameters A, B, C or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.\",\n                \"hint\":null\n              }|]\n              { matchStatus = 404 }\n\n        it \"can insert text directly\" $ do\n          request methodPost \"/rpc/unnamed_text_param\"\n            [(\"Content-Type\", \"text/plain\"), (\"Accept\", \"text/plain\")]\n            [str|unnamed text arg|]\n            `shouldRespondWith`\n            [str|unnamed text arg|]\n\n        it \"can insert xml directly\" $ do\n          request methodPost \"/rpc/unnamed_xml_param\"\n            [(\"Content-Type\", \"text/xml\"), (\"Accept\", \"text/xml\")]\n            [str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]\n            `shouldRespondWith`\n            [str|<note><from>John</from><to>Jane</to><message>Remember me</message></note>|]\n\n        it \"can insert bytea directly\" $ do\n          let file = readFixtureFile \"image.png\"\n          r <- request methodPost \"/rpc/unnamed_bytea_param\"\n            [(\"Content-Type\", \"application/octet-stream\"), (\"Accept\", \"application/octet-stream\")]\n            file\n          liftIO $ do\n            let respBody = simpleBody r\n            respBody `shouldBe` file\n\n        it \"will err when no function with single unnamed json parameter exists and application/json is specified\" $\n          request methodPost \"/rpc/unnamed_int_param\" [(\"Content-Type\", \"application/json\")]\n              [json|{\"x\": 1, \"y\": 2}|]\n            `shouldRespondWith`\n              [json|{\n                \"hint\": \"Perhaps you meant to call the function test.unnamed_text_param\",\n                \"message\": \"Could not find the function test.unnamed_int_param(x, y) in the schema cache\",\n                \"code\":\"PGRST202\",\n                \"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.\"\n              }|]\n              { matchStatus  = 404\n              , matchHeaders = [ matchContentTypeJson ]\n              }\n\n        it \"will err when no function with single unnamed text parameter exists and text/plain is specified\" $\n          request methodPost \"/rpc/unnamed_int_param\"\n              [(\"Content-Type\", \"text/plain\")]\n              [str|a simple text|]\n            `shouldRespondWith`\n              [json|{\n                \"hint\": null,\n                \"message\": \"Could not find the function test.unnamed_int_param in the schema cache\",\n                \"code\":\"PGRST202\",\n                \"details\":\"Searched for the function test.unnamed_int_param with a single unnamed text parameter, but no matches were found in the schema cache.\"\n              }|]\n              { matchStatus  = 404\n              , matchHeaders = [ matchContentTypeJson ]\n              }\n\n        it \"will err when no function with single unnamed text parameter exists and text/xml is specified\" $\n          request methodPost \"/rpc/unnamed_int_param\"\n              [(\"Content-Type\", \"text/xml\")]\n              [str|a simple text|]\n            `shouldRespondWith`\n              [json|{\n                \"hint\": null,\n                \"message\": \"Could not find the function test.unnamed_int_param in the schema cache\",\n                \"code\":\"PGRST202\",\n                \"details\":\"Searched for the function test.unnamed_int_param with a single unnamed xml parameter, but no matches were found in the schema cache.\"\n              }|]\n              { matchStatus  = 404\n              , matchHeaders = [ matchContentTypeJson ]\n              }\n\n        it \"will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified\" $\n          request methodPost \"/rpc/unnamed_int_param\"\n              [(\"Content-Type\", \"application/octet-stream\")]\n              (readFixtureFile \"image.png\")\n          `shouldRespondWith`\n            [json|{\n              \"hint\": null,\n              \"message\": \"Could not find the function test.unnamed_int_param in the schema cache\",\n              \"code\":\"PGRST202\",\n              \"details\":\"Searched for the function test.unnamed_int_param with a single unnamed bytea parameter, but no matches were found in the schema cache.\"\n            }|]\n            { matchStatus  = 404\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        it \"should be able to resolve when a single unnamed json parameter exists and other overloaded functions are found\" $ do\n          request methodPost \"/rpc/overloaded_unnamed_param\" [(\"Content-Type\", \"application/json\")]\n              [json|{}|]\n            `shouldRespondWith`\n              [json| 1 |]\n              { matchStatus  = 200\n              , matchHeaders = [matchContentTypeJson]\n              }\n          request methodPost \"/rpc/overloaded_unnamed_param\" [(\"Content-Type\", \"application/json\")]\n              [json|{\"x\": 1, \"y\": 2}|]\n            `shouldRespondWith`\n              [json| 3 |]\n              { matchStatus  = 200\n              , matchHeaders = [matchContentTypeJson]\n              }\n\n        it \"should be able to fallback to the single unnamed parameter function when other overloaded functions are not found\" $ do\n          request methodPost \"/rpc/overloaded_unnamed_param\"\n              [(\"Content-Type\", \"application/json\")]\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n            `shouldRespondWith`\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n          request methodPost \"/rpc/overloaded_unnamed_param\"\n              [(\"Content-Type\", \"text/plain\"), (\"Accept\", \"text/plain\")]\n              [str|unnamed text arg|]\n            `shouldRespondWith`\n              [str|unnamed text arg|]\n          let file = readFixtureFile \"image.png\"\n          r <- request methodPost \"/rpc/overloaded_unnamed_param\"\n            [(\"Content-Type\", \"application/octet-stream\"), (\"Accept\", \"application/octet-stream\")]\n            file\n          liftIO $ do\n            let respBody = simpleBody r\n            respBody `shouldBe` file\n\n        it \"should call the function with no parameters and not fallback to the single unnamed parameter function when using GET with Content-Type headers\" $ do\n          request methodGet \"/rpc/overloaded_unnamed_param\" [(\"Content-Type\", \"text/plain\")] \"\"\n            `shouldRespondWith`\n              [json| 1|]\n              { matchStatus  = 200 }\n          request methodGet \"/rpc/overloaded_unnamed_param\" [(\"Content-Type\", \"application/octet-stream\")] \"\"\n            `shouldRespondWith`\n              [json| 1|]\n              { matchStatus  = 200 }\n\n        it \"should fail to fallback to any single unnamed parameter function when using an unsupported Content-Type header\" $ do\n          request methodPost \"/rpc/overloaded_unnamed_param\"\n              [(\"Content-Type\", \"text/csv\")]\n              \"a,b\\n1,2\\n4,6\\n100,200\"\n            `shouldRespondWith`\n              [json| {\n                \"hint\":\"Perhaps you meant to call the function test.overloaded_unnamed_param(x, y)\",\n                \"message\":\"Could not find the function test.overloaded_unnamed_param(a, b) in the schema cache\",\n                \"code\":\"PGRST202\",\n                \"details\":\"Searched for the function test.overloaded_unnamed_param with parameters a, b, but no matches were found in the schema cache.\"\n              }|]\n              { matchStatus  = 404\n              , matchHeaders = [matchContentTypeJson]\n              }\n\n        it \"should fail with multiple choices when two fallback functions with single unnamed json and jsonb parameters exist\" $ do\n          request methodPost \"/rpc/overloaded_unnamed_json_jsonb_param\" [(\"Content-Type\", \"application/json\")]\n              [json|{\"A\": 1, \"B\": 2, \"C\": 3}|]\n            `shouldRespondWith`\n              [json| {\n                \"hint\":\"Try renaming the parameters or the function itself in the database so function overloading can be resolved\",\n                \"message\":\"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)\",\n                \"code\":\"PGRST203\",\n                \"details\":null\n              }|]\n              { matchStatus  = 300\n              , matchHeaders = [matchContentTypeJson]\n              }\n\n        it \"should fail on /rpc/unnamed_xml_param when posting invalid xml\" $ do\n          request methodPost \"/rpc/unnamed_xml_param\"\n            [(\"Content-Type\", \"text/xml\"), (\"Accept\", \"text/xml\")]\n            [str|<|]\n            `shouldRespondWith`\n              [json| {\n                \"hint\":null,\n                \"message\":\"invalid XML content\",\n                \"code\":\"2200N\",\n                \"details\":\"line 1: StartTag: invalid element name\\n<\\n ^\"\n              }|]\n              { matchStatus  = 400\n              , matchHeaders = [ \"Content-Length\" <:> \"118\"\n                               , matchContentTypeJson ]\n              }\n\n    -- https://github.com/PostgREST/postgrest/issues/1586#issuecomment-696345442\n    context \"a proc with bit and char parameters\" $ do\n      it \"modifies the param type from character to character varying\" $ do\n        get \"/rpc/char_param_select?char_=abcdefg&char_arr={abc,abcdefg}\" `shouldRespondWith`\n          [json| [{ \"char_\": \"abcdefg\", \"char_arr\": [ \"abc\", \"abcdefg\" ] }] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n        post \"/rpc/char_param_insert\" [json| { \"char_\": \"abcdefg\", \"char_arr\": \"{abc,abcdefg}\" } |]\n           `shouldRespondWith`\n             [json| {\"code\":\"22001\",\"details\":null,\"hint\":null,\"message\":\"value too long for type character(5)\"} |]\n           { matchStatus = 400\n           , matchHeaders = [ \"Content-Length\" <:> \"92\"\n                            , matchContentTypeJson ]\n           }\n\n      it \"modifies the param type from bit to bit varying\" $ do\n        get \"/rpc/bit_param_select?bit_=101010&bit_arr={101,101010}\" `shouldRespondWith`\n          [json| [{ \"bit_\": \"101010\", \"bit_arr\": [ \"101\", \"101010\" ] }] |]\n          { matchHeaders = [matchContentTypeJson] }\n\n        post \"/rpc/bit_param_insert\" [json| { \"bit_\": \"101010\", \"bit_arr\": \"{101,101010}\" } |]\n           `shouldRespondWith`\n             [json| {\"code\":\"22026\",\"details\":null,\"hint\":null,\"message\":\"bit string length 6 does not match type bit(5)\"} |]\n           { matchStatus = 400\n           , matchHeaders = [ \"Content-Length\" <:> \"102\"\n                            , matchContentTypeJson ]\n           }\n\n    context \"get message and details from raise sqlstate\" $ do\n      it \"gets message and details from raise sqlstate PGRST\" $ do\n        r <- request methodGet \"/rpc/raise_sqlstate_test1\"\n                [] \"\"\n\n        let resStatus  = simpleStatus r\n            resHeaders = simpleHeaders r\n            resBody    = simpleBody r\n\n        liftIO $ do\n          resStatus `shouldBe` Status { statusCode = 332, statusMessage = \"My Custom Status\" }\n          resHeaders `shouldSatisfy` elem (\"X-Header\", \"str\")\n          resBody `shouldBe` [json|{\"code\":\"123\",\"message\":\"ABC\",\"details\":\"DEF\",\"hint\":\"XYZ\"}|]\n\n        get \"/rpc/raise_sqlstate_test2\" `shouldRespondWith`\n          [json|{\"code\":\"123\",\"message\":\"ABC\",\"details\":null,\"hint\":null}|]\n          { matchStatus = 332\n          , matchHeaders = [\"X-Header\" <:> \"str\"] }\n\n      it \"get message and details from PGRST raise and checks standard status message\" $ do\n        r <- request methodGet \"/rpc/raise_sqlstate_test3\"\n                [] \"\"\n\n        let resStatus  = simpleStatus r\n            resHeaders = simpleHeaders r\n            resBody    = simpleBody r\n\n        liftIO $ do\n          resStatus `shouldBe` Status { statusCode = 404, statusMessage = \"Not Found\" }\n          resHeaders `shouldSatisfy` elem (\"X-Header\", \"str\")\n          resBody `shouldBe` [json|{\"code\":\"123\",\"message\":\"ABC\",\"details\":null,\"hint\":null}|]\n\n\n      it \"get message and details from PGRST raise and checks custom status message\" $ do\n        r <- request methodGet \"/rpc/raise_sqlstate_test4\"\n                [] \"\"\n\n        let resStatus  = simpleStatus r\n            resHeaders = simpleHeaders r\n            resBody    = simpleBody r\n\n        liftIO $ do\n          resStatus `shouldBe` Status { statusCode = 404, statusMessage = \"My Not Found\" }\n          resHeaders `shouldSatisfy` elem (\"X-Header\", \"str\")\n          resBody `shouldBe` [json|{\"code\":\"123\",\"message\":\"ABC\",\"details\":null,\"hint\":null}|]\n\n      it \"returns error for invalid JSON in the MESSAGE option of the RAISE statement\" $\n        get \"/rpc/raise_sqlstate_invalid_json_message\" `shouldRespondWith`\n          [json|{\n            \"code\":\"PGRST121\",\n            \"message\":\"Could not parse JSON in the \\\"RAISE SQLSTATE 'PGRST'\\\" error\",\n            \"details\":\"Invalid JSON value for MESSAGE: 'INVALID'\",\n            \"hint\":\"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'.\"}|]\n          { matchStatus = 500\n          , matchHeaders = [ \"Content-Length\" <:> \"263\"\n                           , matchContentTypeJson ]\n           }\n\n      it \"returns error for invalid JSON in the DETAIL option of the RAISE statement\" $\n        get \"/rpc/raise_sqlstate_invalid_json_details\" `shouldRespondWith`\n          [json|{\n            \"code\":\"PGRST121\",\n            \"message\":\"Could not parse JSON in the \\\"RAISE SQLSTATE 'PGRST'\\\" error\",\n            \"details\":\"Invalid JSON value for DETAIL: 'INVALID'\",\n            \"hint\":\"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'.\"}|]\n          { matchStatus = 500 }\n\n      it \"returns error for missing DETAIL option in the RAISE statement\" $\n        get \"/rpc/raise_sqlstate_missing_details\" `shouldRespondWith`\n          [json|{\n            \"code\":\"PGRST121\",\n            \"message\":\"Could not parse JSON in the \\\"RAISE SQLSTATE 'PGRST'\\\" error\",\n            \"details\":\"DETAIL is missing in the RAISE statement\",\n            \"hint\":\"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'.\"}|]\n          { matchStatus = 500 }\n\n    -- here JWT has the role: postgrest_test_superuser\n    context \"test function temp_file_limit\" $\n      let auth = authHeaderJWT \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3Rfc3VwZXJ1c2VyIiwiaWQiOiJqZG9lIn0.LQ-qx0ArBnfkwQQhIHKF5cS-lzl0gnTPI8NLoPbL5Fg\" in\n      it \"should return http status 500\" $\n        request methodGet \"/rpc/temp_file_limit\" [auth] \"\" `shouldRespondWith`\n          [json|{\"code\":\"53400\",\"message\":\"temporary file size exceeds temp_file_limit (1kB)\",\"details\":null,\"hint\":null}|]\n          { matchStatus = 500\n          , matchHeaders = [ \"Content-Length\" <:> \"105\"\n                           , matchContentTypeJson ]\n          }\n\n    context \"test table valued function with filter\" $ do\n      it \"works with filter on unselected columns\" $\n        request methodGet \"/rpc/getallprojects?select=id,client_id&name=like.OSX\"\n          [] \"\"\n          `shouldRespondWith`\n          [json| [{\"id\":4,\"client_id\":2}] |]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"works with filter on unselected columns with null embed\" $\n        request methodGet \"/rpc/getallprojects?select=id,clients(id)&clients.name=not.is.null\"\n          [] \"\"\n          `shouldRespondWith`\n          [json| [{\"id\":1,\"clients\":{\"id\": 1}}, {\"id\":2,\"clients\":{\"id\": 1}}, {\"id\":3,\"clients\":{\"id\": 2}}, {\"id\":4,\"clients\":{\"id\": 2}}, {\"id\":5,\"clients\":null}] |]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n      it \"works with logical filter on unselected columns\" $\n        request methodGet \"/rpc/getallprojects?select=id,client_id&or=(name.like.OSX,name.like.IOS)\"\n          [] \"\"\n          `shouldRespondWith`\n          [json| [{\"id\":3,\"client_id\":2}, {\"id\":4,\"client_id\":2}] |]\n          { matchStatus = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"schema cache duplicate definitions when two entries in pg_description have the same OID\" $\n      it \"doesn't err with 300 Multiple Choices\" $\n        request methodGet \"/rpc/collision_test_func?id=1\"\n          [] \"\"\n          `shouldRespondWith`\n          [json| 1 |]\n          { matchStatus = 200 }\n"
  },
  {
    "path": "test/spec/Feature/Query/ServerTimingSpec.hs",
    "content": "module Feature.Query.ServerTimingSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Show Duration on Server-Timing header\" $ do\n\n    context \"responds with Server-Timing header\" $ do\n      it \"works with get request\" $ do\n        request methodGet  \"/organizations?id=eq.6\"\n          []\n          \"\"\n          `shouldRespondWith`\n          [json|[{\"id\":6,\"name\":\"Oscorp\",\"referee\":3,\"auditor\":4,\"manager_id\":6}]|]\n          { matchStatus  = 200\n          , matchHeaders = matchContentTypeJson : map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n          }\n\n      it \"works with post request\" $\n        request methodPost  \"/organizations?select=*\"\n          [(\"Prefer\",\"return=representation\")]\n          [json|{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}|]\n          `shouldRespondWith`\n          [json|[{\"id\":7,\"name\":\"John\",\"referee\":null,\"auditor\":null,\"manager_id\":6}]|]\n          { matchStatus  = 201\n          , matchHeaders = matchContentTypeJson : map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n          }\n\n      it \"works with patch request\" $\n        request methodPatch \"/no_pk?b=eq.0\" mempty\n            [json| { b: \"1\" } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = matchHeaderAbsent hContentType : map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n            }\n\n      it \"works with put request\" $\n        request methodPut \"/tiobe_pls?name=eq.Python\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| [ { \"name\": \"Python\", \"rank\": 19 } ]|]\n          `shouldRespondWith`\n            [json| [ { \"name\": \"Python\", \"rank\": 19 } ]|]\n            { matchStatus  = 200\n            , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n            }\n\n      it \"works with delete request\" $\n        request methodDelete \"/items?id=eq.1\"\n            []\n            \"\"\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = matchHeaderAbsent hContentType : map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n            }\n\n      it \"works with rpc call\" $\n        request methodPost \"/rpc/ret_point_overloaded\"\n          []\n          [json|{\"x\": 1, \"y\": 2}|]\n          `shouldRespondWith`\n          [json|{\"x\": 1, \"y\": 2}|]\n          { matchStatus  = 200\n          , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n          }\n\n      it \"works with root spec\" $\n        request methodHead \"/\"\n          []\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus  = 200\n          , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"plan\", \"transaction\", \"response\"]\n          }\n\n      it \"works with OPTIONS method\" $ do\n        request methodOptions \"/organizations\"\n          []\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus  = 200\n          , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"response\"]\n          }\n        request methodOptions \"/rpc/getallprojects\"\n          []\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus  = 200\n          , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"response\"]\n          }\n        request methodOptions \"/\"\n          []\n          \"\"\n          `shouldRespondWith`\n          \"\"\n          { matchStatus  = 200\n          , matchHeaders = map matchServerTimingHasTiming [\"jwt\", \"parse\", \"response\"]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/SingularSpec.hs",
    "content": "module Feature.Query.SingularSpec where\n\nimport Network.Wai      (Application)\nimport Network.Wai.Test (SResponse (..))\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Requesting singular json object\" $ do\n    let singular = (\"Accept\", \"application/vnd.pgrst.object+json\")\n\n    context \"with GET request\" $ do\n      it \"fails for zero rows\" $\n        request methodGet  \"/items?id=gt.0&id=lt.0\" [singular] \"\"\n          `shouldRespondWith` 406\n\n      it \"will select an existing object\" $ do\n        request methodGet \"/items?id=eq.5\" [singular] \"\"\n          `shouldRespondWith`\n            [json|{\"id\":5}|]\n            { matchHeaders = [matchContentTypeSingular] }\n        -- also test without the +json suffix\n        request methodGet \"/items?id=eq.5\"\n            [(\"Accept\", \"application/vnd.pgrst.object\")] \"\"\n          `shouldRespondWith`\n            [json|{\"id\":5}|]\n            { matchHeaders = [matchContentTypeSingular] }\n\n      it \"can combine multiple prefer values\" $\n        request methodGet \"/items?id=eq.5\" [singular, (\"Prefer\",\"count=none\")] \"\"\n          `shouldRespondWith`\n            [json|{\"id\":5}|]\n            { matchHeaders = [matchContentTypeSingular] }\n\n      it \"can shape plurality singular object routes\" $\n        request methodGet \"/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)\" [singular] \"\"\n          `shouldRespondWith`\n            [json|{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}|]\n            { matchHeaders = [matchContentTypeSingular] }\n\n    context \"when updating rows\" $ do\n      it \"works for one row with return=rep\" $ do\n        request methodPatch \"/addresses?id=eq.1\"\n            [(\"Prefer\", \"return=representation\"), singular]\n            [json| { address: \"B Street\" } |]\n          `shouldRespondWith`\n            [json|{\"id\":1,\"address\":\"B Street\"}|]\n            { matchHeaders = [matchContentTypeSingular] }\n\n      it \"works for one row with return=minimal\" $\n        request methodPatch \"/addresses?id=eq.1\"\n            [(\"Prefer\", \"return=minimal\"), singular]\n            [json| { address: \"C Street\" } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n\n      it \"raises an error for multiple rows\" $ do\n        request methodPatch \"/addresses\"\n            [(\"Prefer\", \"tx=commit\"), singular]\n            [json| { address: \"zzz\" } |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 4 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should not be updated, either\n        get \"/addresses?id=eq.1\"\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"address\":\"address 1\"}]|]\n\n      it \"raises an error for multiple rows with return=rep\" $ do\n        request methodPatch \"/addresses\"\n            [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=representation\"), singular]\n            [json| { address: \"zzz\" } |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 4 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should not be updated, either\n        get \"/addresses?id=eq.1\"\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"address\":\"address 1\"}]|]\n\n      it \"raises an error for zero rows\" $\n        request methodPatch \"/items?id=gt.0&id=lt.0\"\n                [singular] [json|{\"id\":1}|]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                  { matchStatus  = 406\n                  , matchHeaders = [matchContentTypeJson]\n                  }\n\n      it \"raises an error for zero rows with return=rep\" $\n        request methodPatch \"/items?id=gt.0&id=lt.0\"\n                [(\"Prefer\", \"return=representation\"), singular] [json|{\"id\":1}|]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                  { matchStatus  = 406\n                  , matchHeaders = [matchContentTypeJson]\n                  }\n\n    context \"when creating rows\" $ do\n      it \"works for one row with return=rep\" $ do\n        request methodPost \"/addresses\"\n            [(\"Prefer\", \"return=representation\"), singular]\n            [json| [ { id: 102, address: \"xxx\" } ] |]\n          `shouldRespondWith`\n            [json|{\"id\":102,\"address\":\"xxx\"}|]\n            { matchStatus  = 201\n            , matchHeaders = [matchContentTypeSingular]\n            }\n\n      it \"works for one row with return=minimal\" $ do\n        request methodPost \"/addresses\"\n            [(\"Prefer\", \"return=minimal\"), singular]\n            [json| [ { id: 103, address: \"xxx\" } ] |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 201\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , \"Content-Range\" <:> \"*/*\"\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n\n      it \"raises an error when attempting to create multiple entities\" $ do\n        request methodPost \"/addresses\"\n            [(\"Prefer\", \"tx=commit\"), singular]\n            [json| [ { id: 200, address: \"xxx\" }, { id: 201, address: \"yyy\" } ] |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 2 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should not exist, either\n        get \"/addresses?id=eq.200\"\n          `shouldRespondWith`\n            \"[]\"\n\n      it \"raises an error when attempting to create multiple entities with return=rep\" $ do\n        request methodPost \"/addresses\"\n            [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=representation\"), singular]\n            [json| [ { id: 202, address: \"xxx\" }, { id: 203, address: \"yyy\" } ] |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 2 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should not exist, either\n        get \"/addresses?id=eq.202\"\n          `shouldRespondWith`\n            \"[]\"\n\n      it \"raises an error regardless of return=minimal\" $ do\n        request methodPost \"/addresses\"\n            [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=minimal\"), singular]\n            [json| [ { id: 204, address: \"xxx\" }, { id: 205, address: \"yyy\" } ] |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 2 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should not exist, either\n        get \"/addresses?id=eq.204\"\n          `shouldRespondWith`\n            \"[]\"\n\n      it \"raises an error when creating zero entities\" $\n        request methodPost \"/addresses\"\n                [singular]\n                [json| [ ] |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                  { matchStatus  = 406\n                  , matchHeaders = [matchContentTypeJson]\n                  }\n\n      it \"raises an error when creating zero entities with return=rep\" $\n        request methodPost \"/addresses\"\n                [(\"Prefer\", \"return=representation\"), singular]\n                [json| [ ] |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                  { matchStatus  = 406\n                  , matchHeaders = [matchContentTypeJson]\n                  }\n\n    context \"when deleting rows\" $ do\n      it \"works for one row with return=rep\" $ do\n        p <- request methodDelete\n          \"/items?id=eq.11\"\n          [(\"Prefer\", \"return=representation\"), singular] \"\"\n        liftIO $ simpleBody p `shouldBe` [json|{\"id\":11}|]\n\n      it \"works for one row with return=minimal\" $ do\n        p <- request methodDelete\n          \"/items?id=eq.12\"\n          [(\"Prefer\", \"return=minimal\"), singular] \"\"\n        liftIO $ simpleBody p `shouldBe` \"\"\n\n      it \"raises an error when attempting to delete multiple entities\" $ do\n        request methodDelete \"/items?id=gt.0&id=lt.6\"\n            [(\"Prefer\", \"tx=commit\"), singular]\n            \"\"\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 5 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should still exist\n        get \"/items?id=gt.0&id=lt.6&order=id\"\n          `shouldRespondWith`\n            [json| [{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4},{\"id\":5}] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-4/*\"]\n            }\n\n      it \"raises an error when attempting to delete multiple entities with return=rep\" $ do\n        request methodDelete \"/items?id=gt.5&id=lt.11\"\n            [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=representation\"), singular] \"\"\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 5 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- the rows should still exist\n        get \"/items?id=gt.5&id=lt.11\"\n          `shouldRespondWith` [json| [{\"id\":6},{\"id\":7},{\"id\":8},{\"id\":9},{\"id\":10}] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Range\" <:> \"0-4/*\"]\n            }\n\n      it \"raises an error when deleting zero entities\" $\n        request methodDelete \"/items?id=lt.0\"\n                [singular] \"\"\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                { matchStatus  = 406\n                , matchHeaders = [matchContentTypeJson]\n                }\n\n      it \"raises an error when deleting zero entities with return=rep\" $\n        request methodDelete \"/items?id=lt.0\"\n                [(\"Prefer\", \"return=representation\"), singular] \"\"\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                { matchStatus  = 406\n                , matchHeaders = [matchContentTypeJson]\n                }\n\n    context \"when calling a stored proc\" $ do\n      it \"fails for zero rows\" $\n        request methodPost \"/rpc/getproject\"\n                [singular] [json|{ \"id\": 9999999}|]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 0 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                { matchStatus  = 406\n                , matchHeaders = [matchContentTypeJson]\n                }\n\n      -- this one may be controversial, should vnd.pgrst.object include\n      -- the likes of 2 and \"hello?\"\n      it \"succeeds for scalar result\" $\n        request methodPost \"/rpc/sayhello\"\n          [singular] [json|{ \"name\": \"world\"}|]\n          `shouldRespondWith` 200\n\n      it \"returns a single object for json proc\" $\n        request methodPost \"/rpc/getproject\"\n            [singular] [json|{ \"id\": 1}|]\n          `shouldRespondWith`\n            [json|{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1}|]\n            { matchHeaders = [matchContentTypeSingular] }\n\n      it \"fails for multiple rows\" $\n        request methodPost \"/rpc/getallprojects\"\n                [singular] \"{}\"\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 5 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n                { matchStatus  = 406\n                , matchHeaders = [matchContentTypeJson]\n                }\n\n      it \"fails for multiple rows with rolled back changes\" $ do\n        post \"/rpc/getproject?select=id,name\"\n            [json| {\"id\": 1} |]\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\"}]|]\n\n        request methodPost \"/rpc/setprojects\"\n            [(\"Prefer\", \"tx=commit\"), singular]\n            [json| {\"id_l\": 1, \"id_h\": 2, \"name\": \"changed\"} |]\n          `shouldRespondWith`\n            [json|{\"details\":\"The result contains 2 rows\",\"message\":\"Cannot coerce the result to a single JSON object\",\"code\":\"PGRST116\",\"hint\":null}|]\n            { matchStatus  = 406\n            , matchHeaders = [ matchContentTypeJson ]\n            }\n\n        -- should rollback function\n        post \"/rpc/getproject?select=id,name\"\n            [json| {\"id\": 1} |]\n          `shouldRespondWith`\n            [json|[{\"id\":1,\"name\":\"Windows 7\"}]|]\n"
  },
  {
    "path": "test/spec/Feature/Query/SpreadQueriesSpec.hs",
    "content": "module Feature.Query.SpreadQueriesSpec where\n\nimport Network.Wai (Application)\n\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"spread embeds\" $ do\n    it \"works on a many-to-one relationship\" $ do\n      get \"/projects?select=id,...clients(client_name:name)\" `shouldRespondWith`\n        [json|[\n          {\"id\":1,\"client_name\":\"Microsoft\"},\n          {\"id\":2,\"client_name\":\"Microsoft\"},\n          {\"id\":3,\"client_name\":\"Apple\"},\n          {\"id\":4,\"client_name\":\"Apple\"},\n          {\"id\":5,\"client_name\":null}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/grandchild_entities?select=name,...child_entities(parent_name:name,...entities(grandparent_name:name))&limit=3\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"grandchild entity 1\",\"parent_name\":\"child entity 1\",\"grandparent_name\":\"entity 1\"},\n          {\"name\":\"grandchild entity 2\",\"parent_name\":\"child entity 1\",\"grandparent_name\":\"entity 1\"},\n          {\"name\":\"grandchild entity 3\",\"parent_name\":\"child entity 2\",\"grandparent_name\":\"entity 1\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/videogames?select=name,...computed_designers(designer_name:name)\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Civilization I\",\"designer_name\":\"Sid Meier\"},\n          {\"name\":\"Civilization II\",\"designer_name\":\"Sid Meier\"},\n          {\"name\":\"Final Fantasy I\",\"designer_name\":\"Hironobu Sakaguchi\"},\n          {\"name\":\"Final Fantasy II\",\"designer_name\":\"Hironobu Sakaguchi\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works inside a normal embed\" $\n      get \"/grandchild_entities?select=name,child_entity:child_entities(name,...entities(parent_name:name))&limit=1\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"grandchild entity 1\",\"child_entity\":{\"name\":\"child entity 1\",\"parent_name\":\"entity 1\"}}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"works on a one-to-one relationship\" $\n      get \"/country?select=name,...capital(capital:name)\" `shouldRespondWith`\n        [json|[\n          {\"name\":\"Afghanistan\",\"capital\":\"Kabul\"},\n          {\"name\":\"Algeria\",\"capital\":\"Algiers\"}\n        ]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    it \"can include or exclude attributes of the junction on a m2m\" $ do\n      get \"/users?select=*,tasks:users_tasks(*,...tasks(*))&limit=1\" `shouldRespondWith`\n        [json|[{\n          \"id\":1,\"name\":\"Angela Martin\",\n          \"tasks\": [\n            {\"user_id\":1,\"task_id\":1,\"id\":1,\"name\":\"Design w7\",\"project_id\":1},\n            {\"user_id\":1,\"task_id\":2,\"id\":2,\"name\":\"Code w7\",\"project_id\":1},\n            {\"user_id\":1,\"task_id\":3,\"id\":3,\"name\":\"Design w10\",\"project_id\":2},\n            {\"user_id\":1,\"task_id\":4,\"id\":4,\"name\":\"Code w10\",\"project_id\":2}\n          ]\n        }]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n      get \"/users?select=*,tasks:users_tasks(...tasks(*))&limit=1\" `shouldRespondWith`\n        [json|[{\n          \"id\":1,\"name\":\"Angela Martin\",\n          \"tasks\":[\n            {\"id\":1,\"name\":\"Design w7\",\"project_id\":1},\n            {\"id\":2,\"name\":\"Code w7\",\"project_id\":1},\n            {\"id\":3,\"name\":\"Design w10\",\"project_id\":2},\n            {\"id\":4,\"name\":\"Code w10\",\"project_id\":2}\n          ]\n        }]|]\n        { matchStatus  = 200\n        , matchHeaders = [matchContentTypeJson]\n        }\n\n    context \"one-to-many relationships\" $ do\n      it \"should spread a column as a json array\" $ do\n        get \"/factories?select=factory:name,...processes(name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B1\", \"Process B2\"]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"]},\n            {\"factory\":\"Factory D\",\"name\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/factories?select=factory:name,...processes(processes:name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"processes\":[\"Process A1\", \"Process A2\"]},\n            {\"factory\":\"Factory B\",\"processes\":[\"Process B1\", \"Process B2\"]},\n            {\"factory\":\"Factory C\",\"processes\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"]},\n            {\"factory\":\"Factory D\",\"processes\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should spread many columns as json arrays\" $ do\n        get \"/factories?select=factory:name,...processes(name,category_id)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"],\"category_id\":[1, 2]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B1\", \"Process B2\"],\"category_id\":[1, 1]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"category_id\":[2, 2, 2, 2]},\n            {\"factory\":\"Factory D\",\"name\":[],\"category_id\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/factories?select=factory:name,...processes(processes:name,categories:category_id)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"processes\":[\"Process A1\", \"Process A2\"],\"categories\":[1, 2]},\n            {\"factory\":\"Factory B\",\"processes\":[\"Process B1\", \"Process B2\"],\"categories\":[1, 1]},\n            {\"factory\":\"Factory C\",\"processes\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"categories\":[2, 2, 2, 2]},\n            {\"factory\":\"Factory D\",\"processes\":[],\"categories\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should return an empty array when no elements are found\" $\n        get \"/factories?select=factory:name,...processes(processes:name)&processes=is.null\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory D\",\"processes\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should return a single null element array, not an empty one, when the row exists but the value happens to be null\" $\n        get \"/managers?select=name,...organizations(organizations:name,referees:referee)&id=eq.1\" `shouldRespondWith`\n          [json|[\n            {\"name\":\"Referee Manager\",\"organizations\":[\"Referee Org\"],\"referees\":[null]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should work when selecting all columns\" $\n        get \"/factories?select=factory:name,...processes(*)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"id\":[1, 2],\"name\":[\"Process A1\", \"Process A2\"],\"factory_id\":[1, 1],\"category_id\":[1, 2]},\n            {\"factory\":\"Factory B\",\"id\":[3, 4],\"name\":[\"Process B1\", \"Process B2\"],\"factory_id\":[2, 2],\"category_id\":[1, 1]},\n            {\"factory\":\"Factory C\",\"id\":[5, 6, 7, 8],\"name\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"factory_id\":[3, 3, 3, 3],\"category_id\":[2, 2, 2, 2]},\n            {\"factory\":\"Factory D\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should select spread columns from a nested one-to-one relationship\" $\n        get \"/factories?select=factory:name,...processes(process:name,...process_costs(process_costs:cost))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"process_costs\":[150.00, 200.00]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"process_costs\":[180.00, 70.00]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process YY\", \"Process XX\"],\"process_costs\":[40.00, 70.00, 40.00, null]},\n            {\"factory\":\"Factory D\",\"process\":[],\"process_costs\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should select spread columns from a nested many-to-one relationship\" $\n        get \"/factories?select=factory:name,...processes(process:name,...process_categories(categories:name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"categories\":[\"Batch\", \"Mass\"]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B2\", \"Process B1\"],\"categories\":[\"Batch\", \"Batch\"]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process YY\", \"Process XX\", \"Process C2\", \"Process C1\"],\"categories\":[\"Mass\", \"Mass\", \"Mass\", \"Mass\"]},\n            {\"factory\":\"Factory D\",\"process\":[],\"categories\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should select spread columns from a nested one-to-many relationship\" $\n        get \"/factories?select=factory:name,...processes(process:name,...process_supervisor(supervisor_ids:supervisor_id))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisor_ids\":[[1], [2]]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"supervisor_ids\":[[3, 4], [1, 2]]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"supervisor_ids\":[[3], [3], [], []]},\n            {\"factory\":\"Factory D\",\"process\":[],\"supervisor_ids\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should select spread columns from a nested many-to-many relationship\" $ do\n        get \"/factories?select=factory:name,...processes(process:name,...supervisors(supervisors:name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisors\":[[\"Mary\"], [\"John\"]]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"supervisors\":[[\"Peter\", \"Sarah\"], [\"Mary\", \"John\"]]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"supervisors\":[[\"Peter\"], [\"Peter\"], [], []]},\n            {\"factory\":\"Factory D\",\"process\":[],\"supervisors\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread one-to-one relationship as an array of objects\" $ do\n        get \"/factories?select=factory:name,...processes(process:name,process_costs(cost))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"process_costs\":[{\"cost\": 150.00}, {\"cost\": 200.00}]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"process_costs\":[{\"cost\": 180.00}, {\"cost\": 70.00}]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process YY\", \"Process XX\"],\"process_costs\":[{\"cost\": 40.00}, {\"cost\": 70.00}, {\"cost\": 40.00}, null]},\n            {\"factory\":\"Factory D\",\"process\":[],\"process_costs\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread many-to-one relationship as an array of objects\" $\n        get \"/factories?select=factory:name,...processes(process:name,process_categories(name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"process_categories\":[{\"name\": \"Batch\"}, {\"name\": \"Mass\"}]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B2\", \"Process B1\"],\"process_categories\":[{\"name\": \"Batch\"}, {\"name\": \"Batch\"}]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process YY\", \"Process XX\", \"Process C2\", \"Process C1\"],\"process_categories\":[{\"name\": \"Mass\"}, {\"name\": \"Mass\"}, {\"name\": \"Mass\"}, {\"name\": \"Mass\"}]},\n            {\"factory\":\"Factory D\",\"process\":[],\"process_categories\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread one-to-many relationship as an array of arrays\" $\n        get \"/factories?select=factory:name,...processes(process:name,process_supervisor(supervisor_id))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"process_supervisor\":[[{\"supervisor_id\": 1}], [{\"supervisor_id\": 2}]]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"process_supervisor\":[[{\"supervisor_id\": 3}, {\"supervisor_id\": 4}], [{\"supervisor_id\": 1}, {\"supervisor_id\": 2}]]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"process_supervisor\":[[{\"supervisor_id\": 3}], [{\"supervisor_id\": 3}], [], []]},\n            {\"factory\":\"Factory D\",\"process\":[],\"process_supervisor\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread many-to-many relationship as an array of arrays\" $\n        get \"/factories?select=factory:name,...processes(process:name,supervisors(name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisors\":[[{\"name\": \"Mary\"}], [{\"name\": \"John\"}]]},\n            {\"factory\":\"Factory B\",\"process\":[\"Process B1\", \"Process B2\"],\"supervisors\":[[{\"name\": \"Peter\"}, {\"name\": \"Sarah\"}], [{\"name\": \"Mary\"}, {\"name\": \"John\"}]]},\n            {\"factory\":\"Factory C\",\"process\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"],\"supervisors\":[[{\"name\": \"Peter\"}], [{\"name\": \"Peter\"}], [], []]},\n            {\"factory\":\"Factory D\",\"process\":[],\"supervisors\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should work when selecting all columns in a nested to-one resource\" $\n        get \"/factories?select=factory:name,...processes(*,...process_costs(*))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"id\":[1, 2],\"name\":[\"Process A1\", \"Process A2\"],\"factory_id\":[1, 1],\"category_id\":[1, 2],\"process_id\":[1, 2],\"cost\":[150.00, 200.00]},\n            {\"factory\":\"Factory B\",\"id\":[3, 4],\"name\":[\"Process B1\", \"Process B2\"],\"factory_id\":[2, 2],\"category_id\":[1, 1],\"process_id\":[3, 4],\"cost\":[180.00, 70.00]},\n            {\"factory\":\"Factory C\",\"id\":[5, 6, 8, 7],\"name\":[\"Process C1\", \"Process C2\", \"Process YY\", \"Process XX\"],\"factory_id\":[3, 3, 3, 3],\"category_id\":[2, 2, 2, 2],\"process_id\":[5, 6, 8, null],\"cost\":[40.00, 70.00, 40.00, null]},\n            {\"factory\":\"Factory D\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[],\"process_id\":[],\"cost\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"works when column filters are specified\" $\n        get \"/factories?select=factory:name,...processes(*)&processes.name=not.like.*1&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"id\":[2],\"name\":[\"Process A2\"],\"factory_id\":[1],\"category_id\":[2]},\n            {\"factory\":\"Factory B\",\"id\":[4],\"name\":[\"Process B2\"],\"factory_id\":[2],\"category_id\":[1]},\n            {\"factory\":\"Factory C\",\"id\":[6, 7, 8],\"name\":[\"Process C2\", \"Process XX\", \"Process YY\"],\"factory_id\":[3, 3, 3],\"category_id\":[2, 2, 2]},\n            {\"factory\":\"Factory D\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"works with inner joins or not.is.null filters\" $ do\n        get \"/factories?select=factory:name,...processes!inner(name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B1\", \"Process B2\"]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/factories?select=factory:name,...processes(name)&processes=not.is.null&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B1\", \"Process B2\"]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process C1\", \"Process C2\", \"Process XX\", \"Process YY\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the spread relationship ordering columns\" $ do\n        get \"/factories?select=factory:name,...processes(*)&processes.order=category_id.asc,name.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"id\":[1, 2],\"name\":[\"Process A1\", \"Process A2\"],\"factory_id\":[1, 1],\"category_id\":[1, 2]},\n            {\"factory\":\"Factory B\",\"id\":[4, 3],\"name\":[\"Process B2\", \"Process B1\"],\"factory_id\":[2, 2],\"category_id\":[1, 1]},\n            {\"factory\":\"Factory C\",\"id\":[8, 7, 6, 5],\"name\":[\"Process YY\", \"Process XX\", \"Process C2\", \"Process C1\"],\"factory_id\":[3, 3, 3, 3],\"category_id\":[2, 2, 2, 2]},\n            {\"factory\":\"Factory D\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/factories?select=factory:name,...factory_buildings(*)&factory_buildings.order=inspections->pending.asc.nullsfirst,inspections->ins.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"id\":[2, 1],\"code\":[\"A002\", \"A001\"],\"size\":[200, 150],\"type\":[\"A\", \"A\"],\"factory_id\":[1, 1],\"inspections\":[{\"ins\": \"2025A\", \"pending\": true}, {\"ins\": \"2024C\", \"pending\": true}]},\n            {\"factory\":\"Factory B\",\"id\":[4, 3],\"code\":[\"B002\", \"B001\"],\"size\":[120, 50],\"type\":[\"C\", \"B\"],\"factory_id\":[2, 2],\"inspections\":[{\"ins\": \"2023A\"}, {\"ins\": \"2025A\", \"pending\": true}]},\n            {\"factory\":\"Factory C\",\"id\":[5],\"code\":[\"C001\"],\"size\":[240],\"type\":[\"B\"],\"factory_id\":[3],\"inspections\":[{\"ins\": \"2022B\"}]},\n            {\"factory\":\"Factory D\",\"id\":[6],\"code\":[\"D001\"],\"size\":[310],\"type\":[\"A\"],\"factory_id\":[4],\"inspections\":[{\"ins\": \"2024C\", \"pending\": true}]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the spread relationship ordering columns even if they aren't selected\" $\n        get \"/factories?select=factory:name,...processes(name)&processes.order=category_id.asc,name.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B2\", \"Process B1\"]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process YY\", \"Process XX\", \"Process C2\", \"Process C1\"]},\n            {\"factory\":\"Factory D\",\"name\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the related ordering columns in the spread relationship\" $\n        get \"/factories?select=factory:name,...processes(name,...process_costs(cost))&processes.order=process_costs(cost)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"factory\":\"Factory A\",\"name\":[\"Process A1\", \"Process A2\"],\"cost\":[150.00, 200.00]},\n            {\"factory\":\"Factory B\",\"name\":[\"Process B2\", \"Process B1\"],\"cost\":[70.00, 180.00]},\n            {\"factory\":\"Factory C\",\"name\":[\"Process C1\", \"Process YY\", \"Process C2\", \"Process XX\"],\"cost\":[40.00, 40.00, 70.00, null]},\n            {\"factory\":\"Factory D\",\"name\":[],\"cost\":[]}\n           ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"many-to-many relationships\" $ do\n      it \"should spread a column as a json array\" $ do\n        get \"/operators?select=operator:name,...processes(name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"name\":[\"Process C2\", \"Process XX\"]},\n            {\"operator\":\"Anne\",\"name\":[\"Process A1\", \"Process A2\", \"Process B2\"]},\n            {\"operator\":\"Jeff\",\"name\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"]},\n            {\"operator\":\"Liz\",\"name\":[]},\n            {\"operator\":\"Louis\",\"name\":[\"Process A1\", \"Process A2\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/operators?select=operator:name,...processes(processes:name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"processes\":[\"Process C2\", \"Process XX\"]},\n            {\"operator\":\"Anne\",\"processes\":[\"Process A1\", \"Process A2\", \"Process B2\"]},\n            {\"operator\":\"Jeff\",\"processes\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"]},\n            {\"operator\":\"Liz\",\"processes\":[]},\n            {\"operator\":\"Louis\",\"processes\":[\"Process A1\", \"Process A2\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should spread many columns as json arrays\" $ do\n        get \"/operators?select=operator:name,...processes(name,category_id)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"name\":[\"Process C2\", \"Process XX\"],\"category_id\":[2, 2]},\n            {\"operator\":\"Anne\",\"name\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"category_id\":[1, 2, 1]},\n            {\"operator\":\"Jeff\",\"name\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"category_id\":[2, 1, 1, 2]},\n            {\"operator\":\"Liz\",\"name\":[],\"category_id\":[]},\n            {\"operator\":\"Louis\",\"name\":[\"Process A1\", \"Process A2\"],\"category_id\":[1, 2]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/operators?select=operator:name,...processes(processes:name,categories:category_id)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"processes\":[\"Process C2\", \"Process XX\"],\"categories\":[2, 2]},\n            {\"operator\":\"Anne\",\"processes\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"categories\":[1, 2, 1]},\n            {\"operator\":\"Jeff\",\"processes\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"categories\":[2, 1, 1, 2]},\n            {\"operator\":\"Liz\",\"processes\":[],\"categories\":[]},\n            {\"operator\":\"Louis\",\"processes\":[\"Process A1\", \"Process A2\"],\"categories\":[1, 2]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should return an empty array when no elements are found\" $\n        get \"/operators?select=operator:name,...processes(processes:name)&processes=is.null\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Liz\",\"processes\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should return a single null element array, not an empty one, when the row exists but the value happens to be null\" $\n        get \"/operators?select=name,...processes(process:name,...process_costs(cost)))&id=eq.5&processes.id=eq.7\" `shouldRespondWith`\n          [json|[\n            {\"name\":\"Alfred\",\"process\":[\"Process XX\"],\"cost\":[null]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should work when selecting all columns\" $\n        get \"/operators?select=operator:name,...processes(*)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"id\":[6, 7],\"name\":[\"Process C2\", \"Process XX\"],\"factory_id\":[3, 3],\"category_id\":[2, 2]},\n            {\"operator\":\"Anne\",\"id\":[1, 2, 4],\"name\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"factory_id\":[1, 1, 2],\"category_id\":[1, 2, 1]},\n            {\"operator\":\"Jeff\",\"id\":[2, 3, 4, 6],\"name\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"factory_id\":[1, 2, 2, 3],\"category_id\":[2, 1, 1, 2]},\n            {\"operator\":\"Liz\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]},\n            {\"operator\":\"Louis\",\"id\":[1, 2],\"name\":[\"Process A1\", \"Process A2\"],\"factory_id\":[1, 1],\"category_id\":[1, 2]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show spread columns from a nested one-to-one relationship\" $\n        get \"/operators?select=operator:name,...processes(process:name,...process_costs(process_costs:cost))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"process_costs\":[70.00, null]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"process_costs\":[150.00, 200.00, 70.00]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"process_costs\":[200.00, 180.00, 70.00, 70.00]},\n            {\"operator\":\"Liz\",\"process\":[],\"process_costs\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"process_costs\":[150.00, 200.00]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show spread columns from a nested many-to-one relationship\" $\n        get \"/operators?select=operator:name,...processes(process:name,...process_categories(categories:name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"categories\":[\"Mass\", \"Mass\"]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"categories\":[\"Batch\", \"Mass\", \"Batch\"]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"categories\":[\"Mass\", \"Batch\", \"Batch\", \"Mass\"]},\n            {\"operator\":\"Liz\",\"process\":[],\"categories\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"categories\":[\"Batch\", \"Mass\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show spread columns from a nested one-to-many relationship\" $\n        get \"/operators?select=operator:name,...processes(process:name,...process_supervisor(supervisor_ids:supervisor_id))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"supervisor_ids\":[[3], []]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"supervisor_ids\":[[1], [2], [1, 2]]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"supervisor_ids\":[[2], [3, 4], [1, 2], [3]]},\n            {\"operator\":\"Liz\",\"process\":[],\"supervisor_ids\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisor_ids\":[[1], [2]]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show spread columns from a nested many-to-many relationship\" $ do\n        get \"/operators?select=operator:name,...processes(process:name,...supervisors(supervisors:name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"supervisors\":[[\"Peter\"], []]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"supervisors\":[[\"Mary\"], [\"John\"], [\"Mary\", \"John\"]]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"supervisors\":[[\"John\"], [\"Peter\", \"Sarah\"], [\"Mary\", \"John\"], [\"Peter\"]]},\n            {\"operator\":\"Liz\",\"process\":[],\"supervisors\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisors\":[[\"Mary\"], [\"John\"]]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread one-to-one relationship as an array of objects\" $ do\n        get \"/operators?select=operator:name,...processes(process:name,process_costs(cost))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"process_costs\":[{\"cost\": 70.00}, null]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"process_costs\":[{\"cost\": 150.00}, {\"cost\": 200.00}, {\"cost\": 70.00}]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"process_costs\":[{\"cost\": 200.00}, {\"cost\": 180.00}, {\"cost\": 70.00}, {\"cost\": 70.00}]},\n            {\"operator\":\"Liz\",\"process\":[],\"process_costs\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"process_costs\":[{\"cost\": 150.00}, {\"cost\": 200.00}]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread many-to-one relationship as an array of objects\" $\n        get \"/operators?select=operator:name,...processes(process:name,process_categories(name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"process_categories\":[{\"name\": \"Mass\"}, {\"name\": \"Mass\"}]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"process_categories\":[{\"name\": \"Batch\"}, {\"name\": \"Mass\"}, {\"name\": \"Batch\"}]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"process_categories\":[{\"name\": \"Mass\"}, {\"name\": \"Batch\"}, {\"name\": \"Batch\"}, {\"name\": \"Mass\"}]},\n            {\"operator\":\"Liz\",\"process\":[],\"process_categories\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"process_categories\":[{\"name\": \"Batch\"}, {\"name\": \"Mass\"}]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread one-to-many relationship as an array of arrays\" $\n        get \"/operators?select=operator:name,...processes(process:name,process_supervisor(supervisor_id))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"process_supervisor\":[[{\"supervisor_id\": 3}], []]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"process_supervisor\":[[{\"supervisor_id\": 1}], [{\"supervisor_id\": 2}], [{\"supervisor_id\": 1}, {\"supervisor_id\": 2}]]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"process_supervisor\":[[{\"supervisor_id\": 2}], [{\"supervisor_id\": 3}, {\"supervisor_id\": 4}], [{\"supervisor_id\": 1}, {\"supervisor_id\": 2}], [{\"supervisor_id\": 3}]]},\n            {\"operator\":\"Liz\",\"process\":[],\"process_supervisor\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"process_supervisor\":[[{\"supervisor_id\": 1}], [{\"supervisor_id\": 2}]]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should show a nested non-spread many-to-many relationship as an array of arrays\" $\n        get \"/operators?select=operator:name,...processes(process:name,supervisors(name))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"process\":[\"Process C2\", \"Process XX\"],\"supervisors\":[[{\"name\": \"Peter\"}], []]},\n            {\"operator\":\"Anne\",\"process\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"supervisors\":[[{\"name\": \"Mary\"}], [{\"name\": \"John\"}], [{\"name\": \"Mary\"}, {\"name\": \"John\"}]]},\n            {\"operator\":\"Jeff\",\"process\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"supervisors\":[[{\"name\": \"John\"}], [{\"name\": \"Peter\"}, {\"name\": \"Sarah\"}], [{\"name\": \"Mary\"}, {\"name\": \"John\"}], [{\"name\": \"Peter\"}]]},\n            {\"operator\":\"Liz\",\"process\":[],\"supervisors\":[]},\n            {\"operator\":\"Louis\",\"process\":[\"Process A1\", \"Process A2\"],\"supervisors\":[[{\"name\": \"Mary\"}], [{\"name\": \"John\"}]]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"should work when selecting all columns in a nested to-one resource\" $\n        get \"/operators?select=operator:name,...processes(*,...process_costs(*))&order=name\" `shouldRespondWith`\n          [json|[\n            {\"operator\":\"Alfred\",\"id\":[6, 7],\"name\":[\"Process C2\", \"Process XX\"],\"factory_id\":[3, 3],\"category_id\":[2, 2],\"process_id\":[6, null],\"cost\":[70.00, null]},\n            {\"operator\":\"Anne\",\"id\":[1, 2, 4],\"name\":[\"Process A1\", \"Process A2\", \"Process B2\"],\"factory_id\":[1, 1, 2],\"category_id\":[1, 2, 1],\"process_id\":[1, 2, 4],\"cost\":[150.00, 200.00, 70.00]},\n            {\"operator\":\"Jeff\",\"id\":[2, 3, 4, 6],\"name\":[\"Process A2\", \"Process B1\", \"Process B2\", \"Process C2\"],\"factory_id\":[1, 2, 2, 3],\"category_id\":[2, 1, 1, 2],\"process_id\":[2, 3, 4, 6],\"cost\":[200.00, 180.00, 70.00, 70.00]},\n            {\"operator\":\"Liz\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[],\"process_id\":[],\"cost\":[]},\n            {\"operator\":\"Louis\",\"id\":[1, 2],\"name\":[\"Process A1\", \"Process A2\"],\"factory_id\":[1, 1],\"category_id\":[1, 2],\"process_id\":[1, 2],\"cost\":[150.00, 200.00]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"works when column filters are specified\" $\n        get \"/supervisors?select=supervisor:name,...processes(*)&processes.name=not.like.*1&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"Jane\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]},\n            {\"supervisor\":\"John\",\"id\":[2, 4],\"name\":[\"Process A2\", \"Process B2\"],\"factory_id\":[1, 2],\"category_id\":[2, 1]},\n            {\"supervisor\":\"Mary\",\"id\":[4],\"name\":[\"Process B2\"],\"factory_id\":[2],\"category_id\":[1]},\n            {\"supervisor\":\"Peter\",\"id\":[6],\"name\":[\"Process C2\"],\"factory_id\":[3],\"category_id\":[2]},\n            {\"supervisor\":\"Sarah\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"works with inner joins or not.is.null filters\" $ do\n        get \"/supervisors?select=supervisor:name,...processes!inner(name)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"John\",\"name\":[\"Process A2\", \"Process B2\"]},\n            {\"supervisor\":\"Mary\",\"name\":[\"Process A1\", \"Process B2\"]},\n            {\"supervisor\":\"Peter\",\"name\":[\"Process B1\", \"Process C1\", \"Process C2\"]},\n            {\"supervisor\":\"Sarah\",\"name\":[\"Process B1\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/supervisors?select=supervisor:name,...processes(name)&processes=not.is.null&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"John\",\"name\":[\"Process A2\", \"Process B2\"]},\n            {\"supervisor\":\"Mary\",\"name\":[\"Process A1\", \"Process B2\"]},\n            {\"supervisor\":\"Peter\",\"name\":[\"Process B1\", \"Process C1\", \"Process C2\"]},\n            {\"supervisor\":\"Sarah\",\"name\":[\"Process B1\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the spread relationship ordering columns\" $ do\n        get \"/supervisors?select=supervisor:name,...processes(*)&processes.order=category_id.asc,name.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"Jane\",\"id\":[],\"name\":[],\"factory_id\":[],\"category_id\":[]},\n            {\"supervisor\":\"John\",\"id\":[4, 2],\"name\":[\"Process B2\", \"Process A2\"],\"factory_id\":[2, 1],\"category_id\":[1, 2]},\n            {\"supervisor\":\"Mary\",\"id\":[4, 1],\"name\":[\"Process B2\", \"Process A1\"],\"factory_id\":[2, 1],\"category_id\":[1, 1]},\n            {\"supervisor\":\"Peter\",\"id\":[3, 6, 5],\"name\":[\"Process B1\", \"Process C2\", \"Process C1\"],\"factory_id\":[2, 3, 3],\"category_id\":[1, 2, 2]},\n            {\"supervisor\":\"Sarah\",\"id\":[3],\"name\":[\"Process B1\"],\"factory_id\":[2],\"category_id\":[1]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n        get \"/processes?select=process:name,...operators(*)&operators.order=status->afk.asc.nullsfirst,status->id.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"process\":\"Process A1\",\"id\":[2, 1],\"name\":[\"Louis\", \"Anne\"],\"status\":[{\"id\": \"012345\"}, {\"id\": \"543210\", \"afk\": true}]},\n            {\"process\":\"Process A2\",\"id\":[2, 3, 1],\"name\":[\"Louis\", \"Jeff\", \"Anne\"],\"status\":[{\"id\": \"012345\"}, {\"id\": \"666666\", \"afk\": true}, {\"id\": \"543210\", \"afk\": true}]},\n            {\"process\":\"Process B1\",\"id\":[3],\"name\":[\"Jeff\"],\"status\":[{\"id\": \"666666\", \"afk\": true}]},\n            {\"process\":\"Process B2\",\"id\":[3, 1],\"name\":[\"Jeff\", \"Anne\"],\"status\":[{\"id\": \"666666\", \"afk\": true}, {\"id\": \"543210\", \"afk\": true}]},\n            {\"process\":\"Process C1\",\"id\":[],\"name\":[],\"status\":[]},\n            {\"process\":\"Process C2\",\"id\":[5, 3],\"name\":[\"Alfred\", \"Jeff\"],\"status\":[{\"id\": \"000000\"}, {\"id\": \"666666\", \"afk\": true}]},\n            {\"process\":\"Process XX\",\"id\":[5],\"name\":[\"Alfred\"],\"status\":[{\"id\": \"000000\"}]},\n            {\"process\":\"Process YY\",\"id\":[],\"name\":[],\"status\":[]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the spread relationship ordering columns even if they aren't selected\" $\n        get \"/supervisors?select=supervisor:name,...processes(name)&processes.order=category_id.asc,name.desc&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"Jane\",\"name\":[]},\n            {\"supervisor\":\"John\",\"name\":[\"Process B2\", \"Process A2\"]},\n            {\"supervisor\":\"Mary\",\"name\":[\"Process B2\", \"Process A1\"]},\n            {\"supervisor\":\"Peter\",\"name\":[\"Process B1\", \"Process C2\", \"Process C1\"]},\n            {\"supervisor\":\"Sarah\",\"name\":[\"Process B1\"]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n      it \"orders all the resulting arrays according to the related ordering columns in the spread relationship\" $\n        get \"/supervisors?select=supervisor:name,...processes(name,...process_costs(cost))&processes.order=process_costs(cost)&order=name\" `shouldRespondWith`\n          [json|[\n            {\"supervisor\":\"Jane\",\"name\":[],\"cost\":[]},\n            {\"supervisor\":\"John\",\"name\":[\"Process B2\", \"Process A2\"],\"cost\":[70.00, 200.00]},\n            {\"supervisor\":\"Mary\",\"name\":[\"Process B2\", \"Process A1\"],\"cost\":[70.00, 150.00]},\n            {\"supervisor\":\"Peter\",\"name\":[\"Process C1\", \"Process C2\", \"Process B1\"],\"cost\":[40.00, 70.00, 180.00]},\n            {\"supervisor\":\"Sarah\",\"name\":[\"Process B1\"],\"cost\":[180.00]}\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"empty spreads embeds\" $\n      it \"should work return the same as empty embeddings\" $ do\n        get \"/actors?select=*,...films()\"\n          `shouldRespondWith`\n          [json| [{\"id\":1,\"name\":\"john\"}, {\"id\":2,\"name\":\"mary\"}] |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n        get \"/grandchild_entities?select=name,...child_entities(parent_name:name,...entities())\"\n          `shouldRespondWith`\n          [json|\n            [{\"name\":\"grandchild entity 1\",\"parent_name\":\"child entity 1\"},\n             {\"name\":\"grandchild entity 2\",\"parent_name\":\"child entity 1\"},\n             {\"name\":\"grandchild entity 3\",\"parent_name\":\"child entity 2\"},\n             {\"name\":\"(grandchild,entity,4)\",\"parent_name\":\"child entity 2\"},\n             {\"name\":\"(grandchild,entity,5)\",\"parent_name\":\"child entity 2\"}]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n\n        get \"/factories?select=factory:name,...processes()\"\n          `shouldRespondWith`\n          [json|\n            [{\"factory\":\"Factory A\"},\n             {\"factory\":\"Factory B\"},\n             {\"factory\":\"Factory C\"},\n             {\"factory\":\"Factory D\"}]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/UnicodeSpec.hs",
    "content": "module Feature.Query.UnicodeSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"Reading and writing to unicode schema and table names\" $\n    it \"Can read and write values\" $ do\n      get \"/%D9%85%D9%88%D8%A7%D8%B1%D8%AF\"\n        `shouldRespondWith` \"[]\"\n\n      request methodPost \"/%D9%85%D9%88%D8%A7%D8%B1%D8%AF\"\n          [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"return=representation\")]\n          [json| { \"هویت\": 1 } |]\n        `shouldRespondWith`\n          [json| [{ \"هویت\": 1 }] |]\n          { matchStatus = 201 }\n\n      get \"/%D9%85%D9%88%D8%A7%D8%B1%D8%AF\"\n        `shouldRespondWith`\n          [json| [{ \"هویت\": 1 }] |]\n\n      request methodDelete \"/%D9%85%D9%88%D8%A7%D8%B1%D8%AF\"\n          [(\"Prefer\", \"tx=commit\")]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [matchHeaderAbsent hContentType]\n          }\n"
  },
  {
    "path": "test/spec/Feature/Query/UpdateSpec.hs",
    "content": "module Feature.Query.UpdateSpec where\n\nimport Network.Wai (Application)\nimport Test.Hspec  hiding (pendingWith)\n\nimport Network.HTTP.Types\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec = do\n  describe \"Patching record\" $ do\n    context \"to unknown uri\" $\n      it \"indicates no table found by returning 404\" $\n        request methodPatch \"/fake\" []\n          [json| { \"real\": false } |]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.fake' in the schema cache\"} |]\n          { matchStatus = 404\n          , matchHeaders = [\"Content-Length\" <:> \"115\"]\n          }\n\n\n    context \"on an empty table\" $\n      it \"succeeds with status code 204\" $\n        request methodPatch \"/empty_table\" []\n            [json| { \"extra\":20 } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204,\n              matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength ]\n            }\n\n    context \"with invalid json payload\" $\n      it \"fails with 400 and error\" $\n        request methodPatch \"/simple_pk\" [] \"}{ x = 2\"\n          `shouldRespondWith`\n          [json|{\"message\":\"Empty or invalid json\",\"code\":\"PGRST102\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 400,\n            matchHeaders = [ matchContentTypeJson\n                           , \"Content-Length\" <:> \"80\"]\n          }\n\n    context \"with no payload\" $\n      it \"fails with 400 and error\" $\n        request methodPatch \"/items\" [] \"\"\n          `shouldRespondWith`\n          [json|{\"message\":\"Empty or invalid json\",\"code\":\"PGRST102\",\"details\":null,\"hint\":null}|]\n          { matchStatus  = 400,\n            matchHeaders = [matchContentTypeJson]\n          }\n\n    context \"insignificant whitespace\" $ do\n      it \"ignores it and successfuly updates with json payload\" $ do\n        request methodPatch \"/items?id=eq.1\"\n                     [(\"Prefer\", \"return=representation\")]\n                     \"\\t \\n \\r { \\\"id\\\": 99 } \\t \\n \\r \"\n          `shouldRespondWith` [json|[{\"id\":99}]|]\n          { matchStatus  = 200\n          , matchHeaders = [\"Content-Length\" <:> \"11\"]\n          }\n\n    context \"in a nonempty table\" $ do\n      it \"can update a single item\" $ do\n        patch \"/items?id=eq.2\"\n            [json| { \"id\":42 } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"0-0/*\" ]\n            }\n\n      it \"returns empty array when no rows updated and return=rep\" $\n        request methodPatch \"/items?id=eq.999999\"\n          [(\"Prefer\", \"return=representation\")] [json| { \"id\":999999 } |]\n          `shouldRespondWith` \"[]\"\n          {\n            matchStatus  = 200,\n            matchHeaders = [ \"Preference-Applied\" <:> \"return=representation\"\n                           , \"Content-Length\" <:> \"2\"]\n          }\n\n      it \"returns status code 200 when no rows updated\" $\n        request methodPatch \"/items?id=eq.99999999\" []\n          [json| { \"id\": 42 } |]\n            `shouldRespondWith` 204\n\n      it \"returns updated object as array when return=rep\" $\n        request methodPatch \"/items?id=eq.2\"\n          [(\"Prefer\", \"return=representation\")] [json| { \"id\":2 } |]\n          `shouldRespondWith` [json|[{\"id\":2}]|]\n          { matchStatus  = 200,\n            matchHeaders = [\"Content-Range\" <:> \"0-0/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"can update multiple items\" $ do\n        get \"/no_pk?select=a&b=eq.1\"\n          `shouldRespondWith`\n            [json|[]|]\n\n        request methodPatch \"/no_pk?b=eq.0\"\n            [(\"Prefer\", \"tx=commit\")]\n            [json| { b: \"1\" } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"0-1/*\"\n                             , \"Preference-Applied\" <:> \"tx=commit\" ]\n            }\n\n        -- check it really got updated\n        get \"/no_pk?select=a&b=eq.1\"\n          `shouldRespondWith`\n            [json|[ { a: \"1\" }, { a: \"2\" } ]|]\n\n        -- put value back for other tests\n        request methodPatch \"/no_pk?b=eq.1\"\n            [(\"Prefer\", \"tx=commit\")]\n            [json| { b: \"0\" } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength ]\n            }\n\n      it \"can set a column to NULL\" $ do\n        request methodPatch \"/no_pk?a=eq.1\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { b: null } |]\n          `shouldRespondWith`\n            [json| [{ a: \"1\", b: null }] |]\n\n      context \"filtering by a computed column\" $ do\n        it \"is successful\" $\n          request methodPatch\n            \"/items?is_first=eq.true\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 100 } |]\n            `shouldRespondWith` [json| [{ id: 100 }] |]\n            { matchStatus  = 200,\n              matchHeaders = [matchContentTypeJson\n                             ,\"Content-Range\" <:> \"0-0/*\"\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n        it \"returns empty array when no rows updated and return=rep\" $\n          request methodPatch\n            \"/items?always_true=eq.false\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 100 } |]\n            `shouldRespondWith` \"[]\"\n            { matchStatus  = 200,\n              matchHeaders = [\"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      context \"with representation requested\" $ do\n        it \"can provide a representation\" $ do\n          _ <- post \"/items\"\n            [json| { id: 1 } |]\n          request methodPatch\n            \"/items?id=eq.1\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 99 } |]\n            `shouldRespondWith` [json| [{id:99}] |]\n            { matchHeaders = [matchContentTypeJson\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n          -- put value back for other tests\n          void $ request methodPatch \"/items?id=eq.99\" [] [json| { \"id\":1 } |]\n\n        it \"can return computed columns\" $\n          request methodPatch\n            \"/items?id=eq.1&select=id,always_true\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 1 } |]\n            `shouldRespondWith` [json| [{ id: 1, always_true: true }] |]\n            { matchHeaders = [matchContentTypeJson\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n        it \"can select overloaded computed columns\" $ do\n          request methodPatch\n            \"/items?id=eq.1&select=id,computed_overload\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 1 } |]\n            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]\n            { matchHeaders = [matchContentTypeJson\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n          request methodPatch\n            \"/items2?id=eq.1&select=id,computed_overload\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { id: 1 } |]\n            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]\n            { matchHeaders = [matchContentTypeJson\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      it \"ignores ?select= when return not set or return=minimal\" $ do\n        request methodPatch \"/items?id=eq.1&select=id\"\n           [(\"Prefer\", \"return=minimal\")]\n           [json| { id:1 } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"0-0/*\"\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n        request methodPatch \"/items?id=eq.1&select=id\"\n            [(\"Prefer\", \"return=minimal\")]\n            [json| { id:1 } |]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"0-0/*\"\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n\n      context \"when patching with an empty body\" $ do\n        it \"makes no updates and returns 204 without return= and without ?select=\" $ do\n          request methodPatch \"/items\"\n              []\n              [json| {} |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n          request methodPatch \"/items\"\n              []\n              [json| [] |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n          request methodPatch \"/items\"\n              []\n              [json| [{}] |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n        it \"makes no updates and returns 204 without return= and with ?select=\" $ do\n          request methodPatch \"/items?select=id\"\n              []\n              [json| {} |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n          request methodPatch \"/items?select=id\"\n              []\n              [json| [] |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n          request methodPatch \"/items?select=id\"\n              []\n              [json| [{}] |]\n            `shouldRespondWith`\n              \"\"\n              { matchStatus  = 204\n              , matchHeaders = [ matchHeaderAbsent hContentType\n                               , matchHeaderAbsent hContentLength\n                               , \"Content-Range\" <:> \"*/*\" ]\n              }\n\n        it \"makes no updates and returns 200 with return=rep and without ?select=\" $\n          request methodPatch \"/items\" [(\"Prefer\", \"return=representation\")] [json| {} |]\n            `shouldRespondWith` \"[]\"\n            {\n              matchStatus  = 200,\n              matchHeaders = [\"Content-Range\" <:> \"*/*\"\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n        it \"makes no updates and returns 200 with return=rep and with ?select=\" $\n          request methodPatch \"/items?select=id\" [(\"Prefer\", \"return=representation\")] [json| {} |]\n            `shouldRespondWith` \"[]\"\n            {\n              matchStatus  = 200,\n              matchHeaders = [\"Content-Range\" <:> \"*/*\"\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n        it \"makes no updates and returns 200 with return=rep and with ?select= for overloaded computed columns\" $\n          request methodPatch \"/items?select=id,computed_overload\" [(\"Prefer\", \"return=representation\")] [json| {} |]\n            `shouldRespondWith` \"[]\"\n            {\n              matchStatus  = 200,\n              matchHeaders = [\"Content-Range\" <:> \"*/*\"\n                             , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n    context \"with unicode values\" $\n      it \"succeeds and returns values intact\" $ do\n        request methodPatch \"/no_pk?a=eq.1\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| { \"a\":\"圍棋\", \"b\":\"￥\" } |]\n          `shouldRespondWith`\n            [json|[ { \"a\":\"圍棋\", \"b\":\"￥\" } ]|]\n\n    context \"PATCH with ?columns parameter\" $ do\n      it \"ignores json keys not included in ?columns\" $ do\n        request methodPatch \"/articles?id=eq.1&columns=body\"\n            [(\"Prefer\", \"return=representation\")]\n            [json| {\"body\": \"Some real content\", \"smth\": \"here\", \"other\": \"stuff\", \"fake_id\": 13} |]\n          `shouldRespondWith`\n            [json|[{\"id\": 1, \"body\": \"Some real content\", \"owner\": \"postgrest_test_anonymous\"}]|]\n\n      it \"ignores json keys and gives 200 if no record updated\" $\n        request methodPatch \"/articles?id=eq.2001&columns=body\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"body\": \"Some real content\", \"smth\": \"here\", \"other\": \"stuff\", \"fake_id\": 13} |] `shouldRespondWith` 200\n\n      it \"disallows ?columns which don't exist\" $ do\n        request methodPatch \"/articles?id=eq.1&columns=helicopter\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|{\"body\": \"yyy\"}|]\n          `shouldRespondWith`\n          [json|{\"code\":\"PGRST204\",\"details\":null,\"hint\":null,\"message\":\"Could not find the 'helicopter' column of 'articles' in the schema cache\"}|]\n          { matchStatus  = 400\n          , matchHeaders = []\n          }\n\n      it \"returns missing table error even if also has invalid ?columns\" $ do\n        request methodPatch \"/garlic?columns=helicopter\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|[\n            {\"id\": 204, \"body\": \"yyy\"},\n            {\"id\": 205, \"body\": \"zzz\"}]|]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST205\",\"details\":null,\"hint\":null,\"message\":\"Could not find the table 'test.garlic' in the schema cache\"} |]\n          { matchStatus  = 404\n          , matchHeaders = [\"Content-Length\" <:> \"117\"]\n          }\n\n      context \"apply defaults on missing values\" $ do\n        it \"updates table using default values(field-with_sep) when json keys are undefined\" $ do\n          request methodPatch \"/complex_items?id=eq.3&columns=name,field-with_sep\"\n            [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n            [json|{\"name\": \"Tres\"}|]\n            `shouldRespondWith`\n            [json|[\n              {\"id\":3,\"name\":\"Tres\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}},\"arr_data\":[1,2,3],\"field-with_sep\":1}\n            ]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n            }\n\n        it \"updates table default values(field-with_sep) when json keys are undefined\" $ do\n          request methodPatch \"/complex_items?id=eq.3&columns=name,field-with_sep\"\n            [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n            [json|{\"name\": \"Tres\"}|]\n            `shouldRespondWith`\n            [json|[\n              {\"id\":3,\"name\":\"Tres\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}},\"arr_data\":[1,2,3],\"field-with_sep\":1}\n            ]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n            }\n\n        it \"updates view default values(field-with_sep) when json keys are undefined\" $\n          request methodPatch \"/complex_items_view?id=eq.3&columns=arr_data,name\"\n            [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"missing=default\")]\n            [json|\n              {\"arr_data\":null}\n            |]\n            `shouldRespondWith`\n            [json|[\n              {\"id\":3,\"name\":\"Default\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}},\"arr_data\":null,\"field-with_sep\":3}\n            ]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"missing=default, return=representation\"]\n            }\n\n    -- https://github.com/PostgREST/postgrest/issues/2861\n    context \"bit and char columns with length\" $ do\n      it \"should update a bit column with length\" $\n        request methodPatch \"/bitchar_with_length?select=bit,char&char=eq.aaaaa\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{\"bit\": \"11100\"}|]\n          `shouldRespondWith` [json|[{ \"bit\": \"11100\", \"char\": \"aaaaa\" }]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      it \"should update a char column with length\" $\n        request methodPatch \"/bitchar_with_length?select=bit,char&bit=eq.00000\"\n            [(\"Prefer\", \"return=representation\")]\n            [json|{\"char\": \"zzzyy\"}|]\n          `shouldRespondWith` [json|[{ \"bit\": \"00000\", \"char\": \"zzzyy\" }]|]\n            { matchStatus  = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n  context \"tables with self reference foreign keys\" $ do\n    context \"embeds children after update\" $ do\n      it \"without filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=id,name,web_content(name)\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched\"}|]\n          `shouldRespondWith`\n          [json|\n            [ { \"id\": 0, \"name\": \"tardis-patched\", \"web_content\": [ { \"name\": \"fezz\" }, { \"name\": \"foo\" }, { \"name\": \"bar\" } ]} ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with ordering on top-level resource\" $\n        request methodPatch \"/no_pk?order=a.desc\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{ \"b\": \"1\" }|]\n          `shouldRespondWith`\n          [json|\n            [ { \"a\": null, \"b\": \"1\" },\n              { \"a\": \"2\", \"b\": \"1\" },\n              { \"a\": \"1\", \"b\": \"1\" } ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=id,name,web_content(name)&web_content.name=like.f*\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched\"}|]\n          `shouldRespondWith`\n          [json|\n            [ { \"id\": 0, \"name\": \"tardis-patched\", \"web_content\": [ { \"name\": \"fezz\" }, { \"name\": \"foo\" } ]} ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n    context \"embeds parent, children and grandchildren after update\" $ do\n      it \"without filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched-2\"}|]\n          `shouldRespondWith`\n          [json| [\n            {\n              \"id\": 0,\n              \"name\": \"tardis-patched-2\",\n              \"parent_content\": { \"name\": \"wat\" },\n              \"web_content\": [\n                  { \"name\": \"fezz\", \"web_content\": [ { \"name\": \"wut\" } ] },\n                  { \"name\": \"foo\",  \"web_content\": [] },\n                  { \"name\": \"bar\",  \"web_content\": [] }\n              ]\n            }\n          ] |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)&web_content.name=like.f*&web_content.web_content.id=eq.4&parent_content.name=neq.wat\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched-2\"}|]\n          `shouldRespondWith`\n          [json| [\n            {\n              \"id\": 0,\n              \"name\": \"tardis-patched-2\",\n              \"parent_content\": null,\n              \"web_content\": [\n                  { \"name\": \"fezz\", \"web_content\": [ { \"name\": \"wut\" } ] },\n                  { \"name\": \"foo\",  \"web_content\": [] }\n              ]\n            }\n          ] |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n    context \"embeds children after update without explicitly including the id in the ?select\" $ do\n      it \"without filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=name,web_content(name)\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched\"}|]\n          `shouldRespondWith`\n          [json|\n            [ { \"name\": \"tardis-patched\", \"web_content\": [ { \"name\": \"fezz\" }, { \"name\": \"foo\" }, { \"name\": \"bar\" } ]} ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with filters\" $\n        request methodPatch \"/web_content?id=eq.0&select=name,web_content(name)&web_content.name=like.b*\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"tardis-patched\"}|]\n          `shouldRespondWith`\n          [json|\n            [ { \"name\": \"tardis-patched\", \"web_content\": [ { \"name\": \"bar\" } ]} ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n  context \"tables with foreign keys referencing other tables\" $ do\n    context \"embeds an M2M relationship plus parent after update\" $ do\n      it \"without filters\" $\n        request methodPatch \"/users?id=eq.1&select=name,tasks(name,project:projects(name))\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"Kevin Malone\"}|]\n          `shouldRespondWith`\n          [json|[\n            {\n              \"name\": \"Kevin Malone\",\n              \"tasks\": [\n                  { \"name\": \"Design w7\", \"project\": { \"name\": \"Windows 7\" } },\n                  { \"name\": \"Code w7\", \"project\": { \"name\": \"Windows 7\" } },\n                  { \"name\": \"Design w10\", \"project\": { \"name\": \"Windows 10\" } },\n                  { \"name\": \"Code w10\", \"project\": { \"name\": \"Windows 10\" } }\n              ]\n            }\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with filters\" $\n        request methodPatch \"/users?id=eq.1&select=name,tasks(name,project:projects(name))&tasks.name=ilike.code*&tasks.project.name=like.*10\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"Kevin Malone\"}|]\n          `shouldRespondWith`\n          [json|[\n            {\n              \"name\": \"Kevin Malone\",\n              \"tasks\": [\n                  { \"name\": \"Code w7\", \"project\": null },\n                  { \"name\": \"Code w10\", \"project\": { \"name\": \"Windows 10\" } }\n              ]\n            }\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n\n    context \"embeds an O2O relationship after update\" $ do\n      it \"without filters\" $ do\n        request methodPatch \"/students?id=eq.1&select=name,students_info(address)\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"Johnny Doe\"}|]\n          `shouldRespondWith`\n          [json|[\n            {\n              \"name\": \"Johnny Doe\",\n              \"students_info\":{\"address\":\"Street 1\"}\n            }\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n        request methodPatch \"/students_info?id=eq.1&select=address,students(name)\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"address\": \"New Street 1\"}|]\n          `shouldRespondWith`\n          [json|[\n            {\n              \"address\": \"New Street 1\",\n              \"students\":{\"name\": \"John Doe\"}\n            }\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"with filters\" $ do\n        request methodPatch \"/students?id=eq.1&select=name,students_info(address)&students_info.code=like.0002\"\n                [(\"Prefer\", \"return=representation\")]\n          [json|{\"name\": \"Johnny Doe\"}|]\n          `shouldRespondWith`\n          [json|[\n            {\n              \"name\": \"Johnny Doe\",\n              \"students_info\": null\n            }\n          ]|]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n  context \"table with limited privileges\" $ do\n    it \"succeeds updating row and gives a 204 when using return=minimal\" $\n      request methodPatch \"/app_users?id=eq.1\"\n          [(\"Prefer\", \"return=minimal\")]\n          [json| { \"password\": \"passxyz\" } |]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [matchHeaderAbsent hContentType\n                           , matchHeaderAbsent hContentLength\n                           , \"Preference-Applied\" <:> \"return=minimal\"]\n          }\n\n    it \"can update without return=minimal and no explicit select\" $\n      request methodPatch \"/app_users?id=eq.1\"\n          []\n          [json| { \"password\": \"passabc\" } |]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , matchHeaderAbsent hContentLength ]\n          }\n\n  -- Data representations for payload parsing requires Postgres 10 or above.\n  describe \"Data representations\" $ do\n    context \"for a single row\" $ do\n      it \"parses values in payload\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2\" [(\"Prefer\", \"return=headers-only\")]\n          [json| {\"label_color\": \"#221100\", \"due_at\": \"2019-01-03T11:00:00Z\"} |]\n          `shouldRespondWith`\n          \"\"\n            { matchStatus  = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"Content-Range\" <:> \"0-0/*\" ]\n            }\n\n      it \"parses values in payload and formats individually selected values in return=representation\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2&select=id,label_color\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"label_color\": \"#221100\", \"due_at\": \"2019-01-03T11:00:00Z\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":2, \"label_color\": \"#221100\"}] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Range\" <:> \"0-0/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      it \"parses values in payload and formats values in return=representation\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"label_color\": \"#221100\", \"due_at\": \"2019-01-03T11:00:20Z\"} |]\n          `shouldRespondWith`\n          [json| [{\"id\":2, \"name\": \"Essay\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\", \"due_at\":\"2019-01-03T11:00:20Z\"}] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Range\" <:> \"0-0/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n    context \"for multiple rows\" $ do\n      it \"parses values in payload and formats individually selected values in return=representation\" $\n        request methodPatch \"/datarep_todos_computed?id=lt.4&select=id,name,label_color,dark_color\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"label_color\": \"#221100\", \"due_at\": \"2019-01-03T11:00:00Z\"} |]\n          `shouldRespondWith`\n          [json| [\n            {\"id\":1, \"name\": \"Report\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\"},\n            {\"id\":2, \"name\": \"Essay\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\"},\n            {\"id\":3, \"name\": \"Algebra\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\"}\n          ] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Range\" <:> \"0-2/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      it \"parses values in payload and formats values in return=representation\" $\n        request methodPatch \"/datarep_todos_computed?id=lt.4\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"label_color\": \"#221100\", \"due_at\": \"2019-01-03T11:00:00Z\"} |]\n          `shouldRespondWith`\n          [json| [\n            {\"id\":1, \"name\": \"Report\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\", \"due_at\":\"2019-01-03T11:00:00Z\"},\n            {\"id\":2, \"name\": \"Essay\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\", \"due_at\":\"2019-01-03T11:00:00Z\"},\n            {\"id\":3, \"name\": \"Algebra\", \"label_color\": \"#221100\", \"dark_color\":\"#110880\", \"due_at\":\"2019-01-03T11:00:00Z\"}\n          ] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Range\" <:> \"0-2/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n    context \"with ?columns parameter\" $ do\n      it \"ignores json keys not included in ?columns; parses only the ones specified\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2&columns=due_at\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"due_at\": \"2019-01-03T11:00:00Z\", \"smth\": \"here\", \"label_color\": \"invalid\", \"fake_id\": 13} |]\n          `shouldRespondWith`\n          [json| [\n            {\"id\":2, \"name\": \"Essay\", \"label_color\": \"#000100\", \"dark_color\": \"#000080\", \"due_at\":\"2019-01-03T11:00:00Z\"}\n          ] |]\n            { matchStatus  = 200\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"\n                           , \"Content-Range\" <:> \"0-0/*\"\n                           , \"Preference-Applied\" <:> \"return=representation\"]\n            }\n\n      it \"fails if at least one specified column doesn't exist\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2&columns=label_color,helicopters\" [(\"Prefer\", \"return=representation\")]\n          [json| {\"due_at\": \"2019-01-03T11:00:00Z\", \"smth\": \"here\", \"label_color\": \"invalid\", \"fake_id\": 13} |]\n          `shouldRespondWith`\n          [json| {\"code\":\"PGRST204\",\"details\":null,\"hint\":null,\"message\":\"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache\"} |]\n            { matchStatus  = 400\n            , matchHeaders = [\"Content-Type\" <:> \"application/json; charset=utf-8\"]\n            }\n\n      it \"ignores json keys and gives 200 if no record updated\" $\n        request methodPatch \"/datarep_todos_computed?id=eq.2001&columns=label_color\" [(\"Prefer\", \"return=representation\")]\n         [json| {\"due_at\": \"2019-01-03T11:00:00Z\", \"smth\": \"here\", \"label_color\": \"invalid\", \"fake_id\": 13} |]\n         `shouldRespondWith` 200\n\n    context \"with ordering\" $\n      it \"works with request method PATCH and embedded resource\" $\n        request methodPatch \"/web_content?id=eq.0&select=id,name,web_content(name)&web_content.order=name.asc\"\n                  [(\"Prefer\", \"return=representation\")]\n            [json|{\"name\": \"tardis-patched\"}|]\n            `shouldRespondWith`\n            [json|\n              [ { \"id\": 0, \"name\": \"tardis-patched\", \"web_content\": [ { \"name\": \"bar\" }, { \"name\": \"fezz\" }, { \"name\": \"foo\" } ]} ]\n            |]\n            { matchStatus  = 200\n            , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n            }\n"
  },
  {
    "path": "test/spec/Feature/Query/UpsertSpec.hs",
    "content": "-- TODO: Separate this module into:\n--    - Upsert/MergeDuplicates.hs and\n--    - Upsert/IgnoreDuplicates.hs\nmodule Feature.Query.UpsertSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get, put)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"UPSERT\" $ do\n    context \"with POST\" $ do\n      context \"when Prefer: resolution=merge-duplicates is specified\" $ do\n        it \"INSERTs and UPDATEs rows on pk conflict\" $\n          request methodPost \"/tiobe_pls\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"name\": \"Javascript\", \"rank\": 6 },\n              { \"name\": \"Java\", \"rank\": 2 },\n              { \"name\": \"C\", \"rank\": 1 }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"Javascript\", \"rank\": 6 },\n              { \"name\": \"Java\", \"rank\": 2 },\n              { \"name\": \"C\", \"rank\": 1 }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"UPDATEs rows on pk conflict\" $\n          request methodPost \"/tiobe_pls\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"name\": \"Python\", \"rank\": 6 },\n              { \"name\": \"Java\", \"rank\": 2 },\n              { \"name\": \"C\", \"rank\": 1 }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"Python\", \"rank\": 6 },\n              { \"name\": \"Java\", \"rank\": 2 },\n              { \"name\": \"C\", \"rank\": 1 }\n            ]|]\n            { matchStatus = 200\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs row on composite pk conflict\" $\n          request methodPost \"/employees\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"salary\": \"30000\" },\n              { \"first_name\": \"Peter S.\", \"last_name\": \"Yang\", \"salary\": 42000 }\n            ]|] `shouldRespondWith` [json| [\n              { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"salary\": \"$30,000.00\", \"company\": \"One-Up Realty\", \"occupation\": \"Author\" },\n              { \"first_name\": \"Peter S.\", \"last_name\": \"Yang\", \"salary\": \"$42,000.00\", \"company\": null, \"occupation\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows on composite pk conflict for partitioned tables\" $\n          request methodPost \"/car_models\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"name\": \"Murcielago\", \"year\": 2001, \"car_brand_name\": null},\n              { \"name\": \"Roma\", \"year\": 2021, \"car_brand_name\": \"Ferrari\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"Murcielago\", \"year\": 2001, \"car_brand_name\": null},\n              { \"name\": \"Roma\", \"year\": 2021, \"car_brand_name\": \"Ferrari\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"succeeds when the payload has no elements\" $\n          request methodPost \"/articles\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json|[]|] `shouldRespondWith`\n            [json|[]|] { matchStatus = 200 -- nothing was inserted, so it should be 200\n                       , matchHeaders = [matchContentTypeJson] }\n\n        it \"INSERTs and UPDATEs rows on single unique key conflict\" $\n          request methodPost \"/single_unique?on_conflict=unique_key\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"unique_key\": 1, \"value\": \"B\" },\n              { \"unique_key\": 2, \"value\": \"C\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"unique_key\": 1, \"value\": \"B\" },\n              { \"unique_key\": 2, \"value\": \"C\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows on compound unique keys conflict\" $\n          request methodPost \"/compound_unique?on_conflict=key1,key2\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json| [\n              { \"key1\": 1, \"key2\": 1, \"value\": \"B\" },\n              { \"key1\": 1, \"key2\": 2, \"value\": \"C\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"key1\": 1, \"key2\": 1, \"value\": \"B\" },\n              { \"key1\": 1, \"key2\": 2, \"value\": \"C\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows with SERIAL surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/surr_serial_upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"updated value\", \"extra\": \"existing value\" },\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows with GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/surr_gen_default_upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"updated value\", \"extra\": \"existing value\" },\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows with case sensitive table name with GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/Surr_Gen_Default_Upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"updated value\", \"extra\": \"existing value cs\" },\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"succeeds if the table has only PK cols and no other cols\" $\n          request methodPost \"/only_pk\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json|[ { \"id\": 1 }, { \"id\": 2 }, { \"id\": 4} ]|]\n            `shouldRespondWith`\n            [json|[ { \"id\": 1 }, { \"id\": 2 }, { \"id\": 4} ]|]\n            { matchStatus = 201 ,\n              matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\",\n              matchContentTypeJson] }\n\n        it \"succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK\" $\n          request methodPost \"/no_pk\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json|[ { \"a\": \"1\", \"b\": \"0\" } ]|]\n            `shouldRespondWith`\n            [json|[ { \"a\": \"1\", \"b\": \"0\" } ]|]\n            { matchStatus = 201\n            , matchHeaders = [matchContentTypeJson\n                             , \"Preference-Applied\" <:> \"return=representation\"] }\n\n      context \"when Prefer: resolution=ignore-duplicates is specified\" $ do\n        it \"INSERTs and ignores rows on pk conflict\" $\n          request methodPost \"/tiobe_pls\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[\n              { \"name\": \"PHP\", \"rank\": 9 },\n              { \"name\": \"Python\", \"rank\": 10 }\n            ]|] `shouldRespondWith` [json|[\n              { \"name\": \"PHP\", \"rank\": 9 }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and ignores rows on composite pk conflict\" $\n          request methodPost \"/employees\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[\n              { \"first_name\": \"Daniel B.\", \"last_name\": \"Lyon\", \"salary\": \"72000\", \"company\": null, \"occupation\": null },\n              { \"first_name\": \"Sara M.\", \"last_name\": \"Torpey\", \"salary\": 60000, \"company\": \"Burstein-Applebee\", \"occupation\": \"Soil scientist\" }\n            ]|] `shouldRespondWith` [json|[\n              { \"first_name\": \"Sara M.\", \"last_name\": \"Torpey\", \"salary\": \"$60,000.00\", \"company\": \"Burstein-Applebee\", \"occupation\": \"Soil scientist\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and ignores rows on composite pk conflict for partitioned tables\" $\n          request methodPost \"/car_models\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json| [\n              { \"name\": \"Murcielago\", \"year\": 2001, \"car_brand_name\": \"Ferrari\" },\n              { \"name\": \"Huracán\", \"year\": 2021, \"car_brand_name\": \"Lamborghini\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"Huracán\", \"year\": 2021, \"car_brand_name\": \"Lamborghini\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and ignores rows on single unique key conflict\" $\n          request methodPost \"/single_unique?on_conflict=unique_key\"\n              [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n              [json| [\n                { \"unique_key\": 1, \"value\": \"B\" },\n                { \"unique_key\": 2, \"value\": \"C\" },\n                { \"unique_key\": 3, \"value\": \"D\" }\n              ]|]\n            `shouldRespondWith`\n              [json| [\n                { \"unique_key\": 2, \"value\": \"C\" },\n                { \"unique_key\": 3, \"value\": \"D\" }\n              ]|]\n              { matchStatus = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\"]\n              }\n\n        it \"INSERTs and UPDATEs rows on compound unique keys conflict\" $\n          request methodPost \"/compound_unique?on_conflict=key1,key2\"\n              [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n              [json| [\n                { \"key1\": 1, \"key2\": 1, \"value\": \"B\" },\n                { \"key1\": 1, \"key2\": 2, \"value\": \"C\" },\n                { \"key1\": 1, \"key2\": 3, \"value\": \"D\" }\n              ]|]\n            `shouldRespondWith`\n              [json| [\n                { \"key1\": 1, \"key2\": 2, \"value\": \"C\" },\n                { \"key1\": 1, \"key2\": 3, \"value\": \"D\" }\n              ]|]\n              { matchStatus = 201\n              , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\"]\n              }\n\n        it \"INSERTs and UPDATEs rows with SERIAL surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/surr_serial_upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows with GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/surr_gen_default_upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"INSERTs and UPDATEs rows with case sensitive table name GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default\" $\n          request methodPost \"/Surr_Gen_Default_Upsert?columns=id,name&select=name,extra\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\"), (\"Prefer\", \"missing=default\")]\n            [json| [\n              { \"id\": 1, \"name\": \"updated value\" },\n              { \"name\": \"new value\" }\n            ]|] `shouldRespondWith` [json| [\n              { \"name\": \"new value\", \"extra\": null }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, missing=default, return=representation\", matchContentTypeJson]\n            }\n\n        it \"succeeds if the table has only PK cols and no other cols\" $\n          request methodPost \"/only_pk\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[ { \"id\": 1 }, { \"id\": 2 }, { \"id\": 3} ]|]\n            `shouldRespondWith`\n            [json|[ { \"id\": 3} ]|]\n            { matchStatus = 201 ,\n              matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\",\n              matchContentTypeJson] }\n\n        it \"succeeds if not a single resource is created\" $ do\n          request methodPost \"/tiobe_pls\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[ { \"name\": \"Java\", \"rank\": 1 } ]|] `shouldRespondWith`\n            [json|[]|]\n            { matchStatus = 201\n            , matchHeaders = [matchContentTypeJson] }\n\n          request methodPost \"/tiobe_pls\" [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[ { \"name\": \"Java\", \"rank\": 1 }, { \"name\": \"C\", \"rank\": 2 } ]|] `shouldRespondWith`\n            [json|[]|]\n            { matchStatus = 201\n            , matchHeaders = [matchContentTypeJson] }\n\n    context \"with PUT\" $ do\n      context \"Restrictions\" $ do\n        it \"fails if limit is specified\" $\n          put \"/tiobe_pls?name=eq.Javascript&limit=1\"\n            [json| [ { \"name\": \"Javascript\", \"rank\": 1 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"limit/offset querystring parameters are not allowed for PUT\",\"code\":\"PGRST114\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }\n\n        it \"fails if offset is specified\" $\n          put \"/tiobe_pls?name=eq.Javascript&offset=1\"\n            [json| [ { \"name\": \"Javascript\", \"rank\": 1 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"limit/offset querystring parameters are not allowed for PUT\",\"code\":\"PGRST114\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }\n\n        it \"rejects every other filter than pk cols eq's\" $ do\n          put \"/tiobe_pls?rank=eq.19\"\n            [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n\n          put \"/tiobe_pls?id=not.eq.Java\"\n            [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n          put \"/tiobe_pls?id=in.(Go)\"\n            [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n          put \"/tiobe_pls?and=(id.eq.Go)\"\n            [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n\n        it \"fails if not all composite key cols are specified as eq filters\" $ do\n          put \"/employees?first_name=eq.Susan\"\n            [json| [ { \"first_name\": \"Susan\", \"last_name\": \"Heidt\", \"salary\": \"48000\", \"company\": \"GEX\", \"occupation\": \"Railroad engineer\" } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n          put \"/employees?last_name=eq.Heidt\"\n            [json| [ { \"first_name\": \"Susan\", \"last_name\": \"Heidt\", \"salary\": \"48000\", \"company\": \"GEX\", \"occupation\": \"Railroad engineer\" } ]|]\n            `shouldRespondWith`\n            [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n            { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n\n      it \"fails if the uri primary key doesn't match the payload primary key\" $ do\n        put \"/tiobe_pls?name=eq.MATLAB\" [json| [ { \"name\": \"Perl\", \"rank\": 17 } ]|]\n          `shouldRespondWith`\n          [json|{\"message\":\"Payload values do not match URL in primary key column(s)\",\"code\":\"PGRST115\",\"details\":null,\"hint\":null}|]\n          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }\n        put \"/employees?first_name=eq.Wendy&last_name=eq.Anderson\"\n          [json| [ { \"first_name\": \"Susan\", \"last_name\": \"Heidt\", \"salary\": \"48000\", \"company\": \"GEX\", \"occupation\": \"Railroad engineer\" } ]|]\n          `shouldRespondWith`\n          [json|{\"message\":\"Payload values do not match URL in primary key column(s)\",\"code\":\"PGRST115\",\"details\":null,\"hint\":null}|]\n          { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }\n\n      it \"fails if the table has no PK\" $\n        put \"/no_pk?a=eq.one&b=eq.two\" [json| [ { \"a\": \"one\", \"b\": \"two\" } ]|]\n          `shouldRespondWith`\n          [json|{\"message\":\"Filters must include all and only primary key columns with 'eq' operators\",\"code\":\"PGRST105\",\"details\":null,\"hint\":null}|]\n          { matchStatus = 405 , matchHeaders = [matchContentTypeJson] }\n\n      context \"Inserting row\" $ do\n        it \"succeeds on table with single pk col\" $ do\n          -- assert that the next request will indeed be an insert\n          get \"/tiobe_pls?name=eq.Go\"\n            `shouldRespondWith`\n              [json|[]|]\n\n          request methodPut \"/tiobe_pls?name=eq.Go\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n            `shouldRespondWith`\n              [json| [ { \"name\": \"Go\", \"rank\": 19 } ]|]\n              { matchStatus = 201 }\n\n        it \"succeeds on table with composite pk\" $ do\n          -- assert that the next request will indeed be an insert\n          get \"/employees?first_name=eq.Susan&last_name=eq.Heidt\"\n            `shouldRespondWith`\n              [json|[]|]\n\n          request methodPut \"/employees?first_name=eq.Susan&last_name=eq.Heidt\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"first_name\": \"Susan\", \"last_name\": \"Heidt\", \"salary\": \"48000\", \"company\": \"GEX\", \"occupation\": \"Railroad engineer\" } ]|]\n            `shouldRespondWith`\n              [json| [ { \"first_name\": \"Susan\", \"last_name\": \"Heidt\", \"salary\": \"$48,000.00\", \"company\": \"GEX\", \"occupation\": \"Railroad engineer\" } ]|]\n              { matchStatus = 201 }\n\n        it \"succeeds on a partitioned table with composite pk\" $ do\n          -- assert that the next request will indeed be an insert\n          get \"/car_models?name=eq.Supra&year=eq.2021\"\n            `shouldRespondWith`\n              [json|[]|]\n\n          request methodPut \"/car_models?name=eq.Supra&year=eq.2021\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"name\": \"Supra\", \"year\": 2021 } ]|]\n            `shouldRespondWith`\n              [json| [ { \"name\": \"Supra\", \"year\": 2021, \"car_brand_name\": null } ]|]\n            { matchStatus = 201 }\n\n        it \"succeeds if the table has only PK cols and no other cols\" $ do\n          -- assert that the next request will indeed be an insert\n          get \"/only_pk?id=eq.10\"\n            `shouldRespondWith`\n              [json|[]|]\n\n          request methodPut \"/only_pk?id=eq.10\"\n              [(\"Prefer\", \"return=representation\")]\n              [json|[ { \"id\": 10 } ]|]\n            `shouldRespondWith`\n              [json|[ { \"id\": 10 } ]|]\n              { matchStatus = 201 }\n\n      context \"Updating row\" $ do\n        it \"succeeds on table with single pk col\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/tiobe_pls?name=eq.Java\"\n            `shouldRespondWith`\n              [json|[ { \"name\": \"Java\", \"rank\": 1 } ]|]\n\n          request methodPut \"/tiobe_pls?name=eq.Java\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"name\": \"Java\", \"rank\": 13 } ]|]\n            `shouldRespondWith`\n              [json| [ { \"name\": \"Java\", \"rank\": 13 } ]|]\n\n        -- TODO: move this to SingularSpec?\n        it \"succeeds if the payload has more than one row, but it only puts the first element\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/tiobe_pls?name=eq.Java\"\n            `shouldRespondWith`\n              [json|[ { \"name\": \"Java\", \"rank\": 1 } ]|]\n\n          request methodPut \"/tiobe_pls?name=eq.Java\"\n              [(\"Prefer\", \"return=representation\"), (\"Accept\", \"application/vnd.pgrst.object+json\")]\n              [json| [ { \"name\": \"Java\", \"rank\": 19 }, { \"name\": \"Swift\", \"rank\": 12 } ] |]\n            `shouldRespondWith`\n              [json|{ \"name\": \"Java\", \"rank\": 19 }|]\n              { matchHeaders = [matchContentTypeSingular] }\n\n        it \"succeeds on table with composite pk\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/employees?first_name=eq.Frances M.&last_name=eq.Roe\"\n            `shouldRespondWith`\n              [json| [ { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"salary\": \"$24,000.00\", \"company\": \"One-Up Realty\", \"occupation\": \"Author\" } ]|]\n\n          request methodPut \"/employees?first_name=eq.Frances M.&last_name=eq.Roe\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"salary\": \"60000\", \"company\": \"Gamma Gas\", \"occupation\": \"Railroad engineer\" } ]|]\n            `shouldRespondWith`\n              [json| [ { \"first_name\": \"Frances M.\", \"last_name\": \"Roe\", \"salary\": \"$60,000.00\", \"company\": \"Gamma Gas\", \"occupation\": \"Railroad engineer\" } ]|]\n\n        it \"succeeds on a partitioned table with composite pk\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/car_models?name=eq.DeLorean&year=eq.1981\"\n            `shouldRespondWith`\n              [json| [ { \"name\": \"DeLorean\", \"year\": 1981, \"car_brand_name\": \"DMC\" } ]|]\n\n          request methodPut \"/car_models?name=eq.DeLorean&year=eq.1981\"\n              [(\"Prefer\", \"return=representation\")]\n              [json| [ { \"name\": \"DeLorean\", \"year\": 1981, \"car_brand_name\": null } ]|]\n            `shouldRespondWith`\n              [json| [ { \"name\": \"DeLorean\", \"year\": 1981, \"car_brand_name\": null } ]|]\n\n        it \"succeeds if the table has only PK cols and no other cols\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/only_pk?id=eq.1\"\n            `shouldRespondWith`\n              [json|[ { \"id\": 1 } ]|]\n\n          request methodPut \"/only_pk?id=eq.1\"\n              [(\"Prefer\", \"return=representation\")]\n              [json|[ { \"id\": 1 } ]|]\n            `shouldRespondWith`\n              [json|[ { \"id\": 1 } ]|]\n\n        it \"ignores the Range header\" $ do\n          -- assert that the next request will indeed be an update\n          get \"/tiobe_pls?name=eq.Java\"\n            `shouldRespondWith`\n              [json|[ { \"name\": \"Java\", \"rank\": 1 } ]|]\n\n          request methodPut \"/tiobe_pls?name=eq.Java\"\n              [(\"Prefer\", \"return=representation\"), (\"Range\", \"1-1\")]\n              [json| [ { \"name\": \"Java\", \"rank\": 5 } ]|]\n            `shouldRespondWith`\n              [json| [ { \"name\": \"Java\", \"rank\": 5 } ]|]\n\n      -- TODO: move this to SingularSpec?\n      it \"works with return=representation and vnd.pgrst.object+json\" $\n        request methodPut \"/tiobe_pls?name=eq.Ruby\"\n          [(\"Prefer\", \"return=representation\"), (\"Accept\", \"application/vnd.pgrst.object+json\")]\n          [json| [ { \"name\": \"Ruby\", \"rank\": 11 } ]|]\n          `shouldRespondWith`\n          [json|{ \"name\": \"Ruby\", \"rank\": 11 }|]\n          { matchStatus  = 201\n          , matchHeaders = [matchContentTypeSingular] }\n\n\n    context \"with a camel case pk column\" $ do\n      it \"works with POST and merge-duplicates\" $ do\n        request methodPost \"/UnitTest\"\n            [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n            [json|[\n              { \"idUnitTest\": 1, \"nameUnitTest\": \"name of unittest 1\" },\n              { \"idUnitTest\": 2, \"nameUnitTest\": \"name of unittest 2\" }\n            ]|]\n          `shouldRespondWith`\n            [json|[\n              { \"idUnitTest\": 1, \"nameUnitTest\": \"name of unittest 1\" },\n              { \"idUnitTest\": 2, \"nameUnitTest\": \"name of unittest 2\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=merge-duplicates, return=representation\"]\n            }\n\n      it \"works with POST and ignore-duplicates headers\" $ do\n        request methodPost \"/UnitTest\"\n            [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n            [json|[\n              { \"idUnitTest\": 1, \"nameUnitTest\": \"name of unittest 1\" },\n              { \"idUnitTest\": 2, \"nameUnitTest\": \"name of unittest 2\" }\n            ]|]\n          `shouldRespondWith`\n            [json|[\n              { \"idUnitTest\": 2, \"nameUnitTest\": \"name of unittest 2\" }\n            ]|]\n            { matchStatus = 201\n            , matchHeaders = [\"Preference-Applied\" <:> \"resolution=ignore-duplicates, return=representation\"]\n            }\n\n      it \"works with PUT\" $ do\n        put \"/UnitTest?idUnitTest=eq.1\"\n            [json| [ { \"idUnitTest\": 1, \"nameUnitTest\": \"unit test 1\" } ]|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [matchHeaderAbsent hContentType]\n            }\n        get \"/UnitTest?idUnitTest=eq.1\" `shouldRespondWith`\n          [json| [ { \"idUnitTest\": 1, \"nameUnitTest\": \"unit test 1\" } ]|]\n\n      it \"works with request method PUT and return=minimal\" $ do\n        request methodPut \"/UnitTest?idUnitTest=eq.1\"\n            [(\"Prefer\", \"return=minimal\")]\n            [json| [ { \"idUnitTest\": 1, \"nameUnitTest\": \"unit test 1\" } ]|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [matchHeaderAbsent hContentType\n                             , \"Preference-Applied\" <:> \"return=minimal\"]\n            }\n        get \"/UnitTest?idUnitTest=eq.1\" `shouldRespondWith`\n          [json| [ { \"idUnitTest\": 1, \"nameUnitTest\": \"unit test 1\" } ]|]\n\n    context \"with ordering\" $ do\n      it \"works with request method PUT and embedded resource\" $\n        request methodPut \"/web_content?id=eq.0&select=id,name,web_content(name)&web_content.order=name.asc\"\n          [(\"Prefer\", \"return=representation\")]\n          [json|{ \"id\": 0, \"name\": \"tardis-upserted\", \"p_web_id\": 5 }|]\n          `shouldRespondWith`\n          [json|\n            [ { \"id\": 0, \"name\": \"tardis-upserted\", \"web_content\": [ { \"name\": \"bar\" }, { \"name\": \"fezz\" }, { \"name\": \"foo\" } ]} ]\n          |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson, \"Preference-Applied\" <:> \"return=representation\"]\n          }\n\n      it \"works with batch upserts and embedded resource\" $\n        request methodPost \"/artists?select=id,name,albums(title)&order=id.desc\"\n          [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"resolution=merge-duplicates\")]\n          [json| [{ \"id\": 1, \"name\": \"duster-updated\" },\n                  { \"id\": 2, \"name\": \"bcnr-updated\" }] |]\n          `shouldRespondWith`\n          [json|  [{\"id\":2,\"name\":\"bcnr-updated\",\"albums\":[{\"title\": \"ants from up above\"}]}, {\"id\":1,\"name\":\"duster-updated\",\"albums\":[{\"title\": \"stratosphere\"}, {\"title\": \"contemporary movement\"}]}] |]\n          { matchStatus  = 200\n          , matchHeaders = [matchContentTypeJson]\n          }\n"
  },
  {
    "path": "test/spec/Feature/RollbackSpec.hs",
    "content": "module Feature.RollbackSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get)\nimport SpecHelper\n\n-- two helpers functions to make sure that each test can setup and cleanup properly\n\n-- creates Item to work with for PATCH and DELETE\npostItem =\n  request methodPost \"/items\"\n      [(\"Prefer\", \"tx=commit\"), (\"Prefer\", \"resolution=ignore-duplicates\")]\n      [json|{\"id\":0}|]\n    `shouldRespondWith`\n      \"\"\n      { matchStatus  = 201\n      , matchHeaders = [ matchHeaderAbsent hContentType\n                       , \"Content-Length\" <:> \"0\" ]\n      }\n\n-- removes Items left over from POST, PUT, and PATCH\ndeleteItems =\n  request methodDelete \"/items?id=lte.0\"\n      [(\"Prefer\", \"tx=commit\")]\n      \"\"\n    `shouldRespondWith`\n      \"\"\n      { matchStatus  = 204\n      , matchHeaders = [ matchHeaderAbsent hContentType\n                       , matchHeaderAbsent hContentLength ]\n      }\n\npreferDefault  = [(\"Prefer\", \"return=representation\")]\npreferCommit   = [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"tx=commit\")]\npreferRollback = [(\"Prefer\", \"return=representation\"), (\"Prefer\", \"tx=rollback\")]\n\nwithoutPreferenceApplied      = []\nwithPreferenceCommitApplied   = [ matchHeaderValuePresent \"Preference-Applied\" \"tx=commit\" ]\nwithPreferenceRollbackApplied = [ matchHeaderValuePresent \"Preference-Applied\" \"tx=rollback\" ]\n\nshouldRespondToReads reqHeaders respHeaders = do\n  it \"responds to GET\" $ do\n    request methodGet \"/items?id=eq.1\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n        { matchHeaders = respHeaders }\n\n  it \"responds to HEAD\" $ do\n    request methodHead \"/items?id=eq.1\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        \"\"\n        { matchHeaders = matchContentTypeJson : respHeaders }\n\n  it \"responds to GET on RPC\" $ do\n    request methodGet \"/rpc/search?id=1\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n        { matchHeaders = respHeaders }\n\n  it \"responds to POST on RPC\" $ do\n    request methodPost \"/rpc/search\"\n        reqHeaders\n        [json|{\"id\":1}|]\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n        { matchHeaders = respHeaders }\n\nshouldRaiseExceptions reqHeaders respHeaders = do\n  it \"raises immediate constraints\" $ do\n    request methodPost \"/rpc/raise_constraint\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":\"Key (col)=(1) already exists.\",\n          \"code\":\"23505\",\n          \"message\":\"duplicate key value violates unique constraint \\\"deferrable_unique_constraint_col_key\\\"\"\n        }|]\n        { matchStatus = 409\n        , matchHeaders = respHeaders }\n\n  it \"raises deferred constraints\" $ do\n    request methodPost \"/rpc/raise_constraint\"\n        reqHeaders\n        [json|{\"deferred\": true}|]\n      `shouldRespondWith`\n        [json|{\n          \"hint\":null,\n          \"details\":\"Key (col)=(1) already exists.\",\n          \"code\":\"23505\",\n          \"message\":\"duplicate key value violates unique constraint \\\"deferrable_unique_constraint_col_key\\\"\"\n        }|]\n        { matchStatus = 409\n        , matchHeaders = respHeaders }\n\nshouldPersistMutations reqHeaders respHeaders = do\n  it \"does persist post\" $ do\n    request methodPost \"/items\"\n        reqHeaders\n        [json|{\"id\":0}|]\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n        { matchStatus  = 201\n        , matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n    deleteItems\n\n  it \"does persist put\" $ do\n    request methodPut \"/items?id=eq.0\"\n      reqHeaders\n      [json|{\"id\":0}|]\n      `shouldRespondWith`\n      [json|[{\"id\":0}]|]\n      { matchStatus  = 201\n      , matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n    deleteItems\n\n  it \"does persist patch\" $ do\n    postItem\n    request methodPatch \"/items?id=eq.0\"\n        reqHeaders\n        [json|{\"id\":-1}|]\n      `shouldRespondWith`\n        [json|[{\"id\":-1}]|]\n        { matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[]|]\n    get \"/items?id=eq.-1\"\n      `shouldRespondWith`\n        [json|[{\"id\":-1}]|]\n    deleteItems\n\n  it \"does persist delete\" $ do\n    postItem\n    request methodDelete \"/items?id=eq.0\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n        { matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[]|]\n\nshouldNotPersistMutations reqHeaders respHeaders = do\n  it \"does not persist post\" $ do\n    request methodPost \"/items\"\n        reqHeaders\n        [json|{\"id\":0}|]\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n        { matchStatus  = 201\n        , matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[]|]\n\n  it \"does not persist put\" $ do\n    request methodPut \"/items?id=eq.0\"\n        reqHeaders\n        [json|{\"id\":0}|]\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n        { matchStatus  = 201\n        , matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[]|]\n\n  it \"does not persist patch\" $ do\n    request methodPatch \"/items?id=eq.1\"\n        reqHeaders\n        [json|{\"id\":0}|]\n      `shouldRespondWith`\n        [json|[{\"id\":0}]|]\n        { matchHeaders = respHeaders }\n    get \"/items?id=eq.0\"\n      `shouldRespondWith`\n        [json|[]|]\n    get \"items?id=eq.1\"\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n\n  it \"does not persist delete\" $ do\n    request methodDelete \"/items?id=eq.1\"\n        reqHeaders\n        \"\"\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n        { matchHeaders = respHeaders }\n    get \"/items?id=eq.1\"\n      `shouldRespondWith`\n        [json|[{\"id\":1}]|]\n\nallowed :: SpecWith ((), Application)\nallowed = describe \"tx-allow-override = true\" $ do\n  describe \"without Prefer tx\" $ do\n    preferDefault `shouldRespondToReads` withoutPreferenceApplied\n    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied\n    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=commit\" $ do\n    preferCommit `shouldRespondToReads` withPreferenceCommitApplied\n    preferCommit `shouldPersistMutations` withPreferenceCommitApplied\n    -- Exceptions are always without preference applied,\n    -- because they return before the end of the transaction.\n    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=rollback\" $ do\n    preferRollback `shouldRespondToReads` withPreferenceRollbackApplied\n    preferRollback `shouldNotPersistMutations` withPreferenceRollbackApplied\n    -- Exceptions are always without preference applied,\n    -- because they return before the end of the transaction.\n    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied\n\ndisallowed :: SpecWith ((), Application)\ndisallowed = describe \"tx-rollback-all = false, tx-allow-override = false\" $ do\n  describe \"without Prefer tx\" $ do\n    preferDefault `shouldRespondToReads` withoutPreferenceApplied\n    preferDefault `shouldPersistMutations` withoutPreferenceApplied\n    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=commit\" $ do\n    preferCommit `shouldRespondToReads` withoutPreferenceApplied\n    preferCommit `shouldPersistMutations` withoutPreferenceApplied\n    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=rollback\" $ do\n    preferRollback `shouldRespondToReads` withoutPreferenceApplied\n    preferRollback `shouldPersistMutations` withoutPreferenceApplied\n    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied\n\n\nforced :: SpecWith ((), Application)\nforced = describe \"tx-rollback-all = true, tx-allow-override = false\" $ do\n  describe \"without Prefer tx\" $ do\n    preferDefault `shouldRespondToReads` withoutPreferenceApplied\n    preferDefault `shouldNotPersistMutations` withoutPreferenceApplied\n    preferDefault `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=commit\" $ do\n    preferCommit `shouldRespondToReads` withoutPreferenceApplied\n    preferCommit `shouldNotPersistMutations` withoutPreferenceApplied\n    preferCommit `shouldRaiseExceptions` withoutPreferenceApplied\n\n  describe \"Prefer tx=rollback\" $ do\n    preferRollback `shouldRespondToReads` withoutPreferenceApplied\n    preferRollback `shouldNotPersistMutations` withoutPreferenceApplied\n    preferRollback `shouldRaiseExceptions` withoutPreferenceApplied\n"
  },
  {
    "path": "test/spec/Feature/RpcPreRequestGucsSpec.hs",
    "content": "module Feature.RpcPreRequestGucsSpec where\n\nimport Network.Wai (Application)\n\nimport Network.HTTP.Types\nimport Test.Hspec          hiding (pendingWith)\nimport Test.Hspec.Wai\nimport Test.Hspec.Wai.JSON\n\nimport Protolude  hiding (get, put)\nimport SpecHelper\n\nspec :: SpecWith ((), Application)\nspec =\n  describe \"GUC headers on all methods via pre-request\" $ do\n    it \"succeeds setting the headers on POST\" $\n      post \"/items\"\n          [json|[{\"id\": 11111}]|]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 201\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , \"X-Custom-Header\" <:> \"mykey=myval\" ]\n          }\n\n    it \"succeeds setting the headers on GET and HEAD\" $ do\n      request methodGet \"/items?id=eq.1\"\n          [(\"User-Agent\", \"MSIE 6.0\")]\n          \"\"\n        `shouldRespondWith`\n          [json|[{\"id\": 1}]|]\n          { matchHeaders = [\"Cache-Control\" <:> \"no-cache, no-store, must-revalidate\"] }\n\n      request methodHead \"/items?id=eq.1\"\n          [(\"User-Agent\", \"MSIE 7.0\")]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ matchContentTypeJson\n                           , \"Cache-Control\" <:> \"no-cache, no-store, must-revalidate\" ]\n          }\n\n      request methodHead \"/projects\"\n          [(\"Accept\", \"text/csv\")]\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchHeaders = [ \"Content-Type\" <:> \"text/csv; charset=utf-8\"\n                           , \"Content-Disposition\" <:> \"attachment; filename=projects.csv\" ]\n          }\n\n    it \"succeeds setting the headers on PATCH\" $\n        patch \"/items?id=eq.1\"\n            [json|[{\"id\": 11111}]|]\n          `shouldRespondWith`\n            \"\"\n            { matchStatus = 204\n            , matchHeaders = [ matchHeaderAbsent hContentType\n                             , matchHeaderAbsent hContentLength\n                             , \"X-Custom-Header\" <:> \"mykey=myval\" ]\n            }\n\n    it \"succeeds setting the headers on PUT\" $\n      put \"/items?id=eq.1\"\n          [json|[{\"id\": 1}]|]\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , matchHeaderAbsent hContentLength\n                           , \"X-Custom-Header\" <:> \"mykey=myval\" ]\n          }\n\n    it \"succeeds setting the headers on DELETE\" $\n      delete \"/items?id=eq.1\"\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 204\n          , matchHeaders = [ matchHeaderAbsent hContentType\n                           , matchHeaderAbsent hContentLength\n                           , \"X-Custom-Header\" <:> \"mykey=myval\" ]\n          }\n    it \"can override the Content-Type header\" $ do\n      request methodHead \"/clients?id=eq.1\"\n          []\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/custom+json\"]\n          }\n      request methodHead \"/rpc/getallprojects\"\n          []\n          \"\"\n        `shouldRespondWith`\n          \"\"\n          { matchStatus = 200\n          , matchHeaders = [\"Content-Type\" <:> \"application/custom+json\"]\n          }\n"
  },
  {
    "path": "test/spec/Main.hs",
    "content": "module Main where\n\nimport qualified Hasql.Pool                 as P\nimport qualified Hasql.Pool.Config          as P\nimport qualified Hasql.Transaction.Sessions as HT\n\nimport Data.Function (id)\n\nimport Test.Hspec\n\nimport PostgREST.App             (postgrest)\nimport PostgREST.Config          (AppConfig (..))\nimport PostgREST.Config.Database (queryPgVersion)\nimport PostgREST.SchemaCache     (querySchemaCache)\nimport Protolude                 hiding (toList, toS)\nimport SpecHelper\n\nimport qualified PostgREST.AppState as AppState\nimport qualified PostgREST.Logger   as Logger\nimport qualified PostgREST.Metrics  as Metrics\n\nimport qualified Feature.Auth.AsymmetricJwtSpec\nimport qualified Feature.Auth.AudienceJwtSecretSpec\nimport qualified Feature.Auth.AuthSpec\nimport qualified Feature.Auth.BinaryJwtSecretSpec\nimport qualified Feature.Auth.NoAnonSpec\nimport qualified Feature.Auth.NoJwtSecretSpec\nimport qualified Feature.ConcurrentSpec\nimport qualified Feature.CorsSpec\nimport qualified Feature.ExtraSearchPathSpec\nimport qualified Feature.NoSuperuserSpec\nimport qualified Feature.ObservabilitySpec\nimport qualified Feature.OpenApi.DisabledOpenApiSpec\nimport qualified Feature.OpenApi.IgnorePrivOpenApiSpec\nimport qualified Feature.OpenApi.OpenApiSpec\nimport qualified Feature.OpenApi.ProxySpec\nimport qualified Feature.OpenApi.RootSpec\nimport qualified Feature.OpenApi.SecurityOpenApiSpec\nimport qualified Feature.OptionsSpec\nimport qualified Feature.Query.AggregateFunctionsSpec\nimport qualified Feature.Query.AndOrParamsSpec\nimport qualified Feature.Query.ComputedRelsSpec\nimport qualified Feature.Query.CustomMediaSpec\nimport qualified Feature.Query.DeleteSpec\nimport qualified Feature.Query.EmbedDisambiguationSpec\nimport qualified Feature.Query.EmbedInnerJoinSpec\nimport qualified Feature.Query.ErrorSpec\nimport qualified Feature.Query.InsertSpec\nimport qualified Feature.Query.JsonOperatorSpec\nimport qualified Feature.Query.MultipleSchemaSpec\nimport qualified Feature.Query.NullsStripSpec\nimport qualified Feature.Query.PgSafeUpdateSpec\nimport qualified Feature.Query.PlanSpec\nimport qualified Feature.Query.PostGISSpec\nimport qualified Feature.Query.PreferencesSpec\nimport qualified Feature.Query.QueryLimitedSpec\nimport qualified Feature.Query.QuerySpec\nimport qualified Feature.Query.RangeSpec\nimport qualified Feature.Query.RawOutputTypesSpec\nimport qualified Feature.Query.RelatedQueriesSpec\nimport qualified Feature.Query.RpcSpec\nimport qualified Feature.Query.ServerTimingSpec\nimport qualified Feature.Query.SingularSpec\nimport qualified Feature.Query.SpreadQueriesSpec\nimport qualified Feature.Query.UnicodeSpec\nimport qualified Feature.Query.UpdateSpec\nimport qualified Feature.Query.UpsertSpec\nimport qualified Feature.RollbackSpec\nimport qualified Feature.RpcPreRequestGucsSpec\n\n\nmain :: IO ()\nmain = do\n  pool <- P.acquire $ P.settings\n    [ P.size 3\n    , P.acquisitionTimeout 10\n    , P.agingTimeout 60\n    , P.idlenessTimeout 60\n    , P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)\n    ]\n\n  actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)\n\n  -- cached schema cache so most tests run fast\n  baseSchemaCache <- loadSCache pool testCfg\n  loggerState <- Logger.init\n  metricsState <- Metrics.init (configDbPoolSize testCfg)\n\n  let\n    initApp sCache st config = do\n      appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState)\n      AppState.putPgVersion appState actualPgVersion\n      AppState.putSchemaCache appState (Just sCache)\n      return (st, postgrest (configLogLevel config) appState (pure ()))\n\n    -- For tests that run with the same schema cache\n    app = initApp baseSchemaCache ()\n\n    -- For tests that run with a different SchemaCache (depends on configSchemas)\n    appDbs config = do\n      customSchemaCache <- loadSCache pool config\n      initApp customSchemaCache () config\n\n  let withApp              = app testCfg\n      maxRowsApp           = app testMaxRowsCfg\n      disabledOpenApi      = app testDisabledOpenApiCfg\n      securityOpenApi      = app testSecurityOpenApiCfg\n      proxyApp             = app testProxyCfg\n      noAnonApp            = app testCfgNoAnon\n      noJwtSecretApp       = app testCfgNoJwtSecret\n      binaryJwtApp         = app testCfgBinaryJWT\n      audJwtApp            = app testCfgAudienceJWT\n      asymJwkApp           = app testCfgAsymJWK\n      asymJwkSetApp        = app testCfgAsymJWKSet\n      rootSpecApp          = app testCfgRootSpec\n      responseHeadersApp   = app testCfgResponseHeaders\n      disallowRollbackApp  = app testCfgDisallowRollback\n      forceRollbackApp     = app testCfgForceRollback\n      planEnabledApp       = app testPlanEnabledCfg\n      pgSafeUpdateApp      = app testPgSafeUpdateEnabledCfg\n      obsApp               = app testObservabilityCfg\n      serverTiming         = app testCfgServerTiming\n      aggregatesEnabled    = app testCfgAggregatesEnabled\n\n      extraSearchPathApp   = appDbs testCfgExtraSearchPath\n      unicodeApp           = appDbs testUnicodeCfg\n      multipleSchemaApp    = appDbs testMultipleSchemaCfg\n      ignorePrivOpenApi    = appDbs testIgnorePrivOpenApiCfg\n\n\n  let analyze :: IO ()\n      analyze = do\n        analyzeTable \"items\"\n        analyzeTable \"child_entities\"\n\n      specs = uncurry describe <$> [\n          (\"Feature.Auth.AudienceJwtSecretSpec\"          , Feature.Auth.AudienceJwtSecretSpec.disabledSpec)\n        , (\"Feature.Auth.AuthSpec\"                       , Feature.Auth.AuthSpec.spec)\n        , (\"Feature.ConcurrentSpec\"                      , Feature.ConcurrentSpec.spec)\n        , (\"Feature.CorsSpec\"                            , Feature.CorsSpec.spec)\n        , (\"Feature.CustomMediaSpec\"                     , Feature.Query.CustomMediaSpec.spec)\n        , (\"Feature.NoSuperuserSpec\"                     , Feature.NoSuperuserSpec.spec)\n        , (\"Feature.OpenApi.OpenApiSpec\"                 , Feature.OpenApi.OpenApiSpec.spec)\n        , (\"Feature.OptionsSpec\"                         , Feature.OptionsSpec.spec)\n        , (\"Feature.Query.AndOrParamsSpec\"               , Feature.Query.AndOrParamsSpec.spec)\n        , (\"Feature.Query.ComputedRelsSpec\"              , Feature.Query.ComputedRelsSpec.spec)\n        , (\"Feature.Query.DeleteSpec\"                    , Feature.Query.DeleteSpec.spec)\n        , (\"Feature.Query.EmbedDisambiguationSpec\"       , Feature.Query.EmbedDisambiguationSpec.spec)\n        , (\"Feature.Query.EmbedInnerJoinSpec\"            , Feature.Query.EmbedInnerJoinSpec.spec)\n        , (\"Feature.Query.InsertSpec\"                    , Feature.Query.InsertSpec.spec actualPgVersion)\n        , (\"Feature.Query.JsonOperatorSpec\"              , Feature.Query.JsonOperatorSpec.spec)\n        , (\"Feature.Query.NullsStripSpec\"                , Feature.Query.NullsStripSpec.spec)\n        , (\"Feature.Query.PgErrorCodeMappingSpec\"        , Feature.Query.ErrorSpec.pgErrorCodeMapping)\n        , (\"Feature.Query.PgSafeUpdateSpec.disabledSpec\" , Feature.Query.PgSafeUpdateSpec.disabledSpec)\n        , (\"Feature.Query.PlanSpec.disabledSpec\"         , Feature.Query.PlanSpec.disabledSpec)\n        , (\"Feature.Query.PreferencesSpec\"               , Feature.Query.PreferencesSpec.spec)\n        , (\"Feature.Query.QuerySpec\"                     , Feature.Query.QuerySpec.spec)\n        , (\"Feature.Query.RawOutputTypesSpec\"            , Feature.Query.RawOutputTypesSpec.spec)\n        , (\"Feature.Query.RelatedQueriesSpec\"            , Feature.Query.RelatedQueriesSpec.spec)\n        , (\"Feature.Query.RpcSpec\"                       , Feature.Query.RpcSpec.spec)\n        , (\"Feature.Query.SingularSpec\"                  , Feature.Query.SingularSpec.spec)\n        , (\"Feature.Query.SpreadQueriesSpec\"             , Feature.Query.SpreadQueriesSpec.spec)\n        , (\"Feature.Query.UpdateSpec\"                    , Feature.Query.UpdateSpec.spec)\n        , (\"Feature.Query.UpsertSpec\"                    , Feature.Query.UpsertSpec.spec)\n        ]\n\n  hspec $ do\n    mapM_ (parallel . before withApp) specs\n\n    -- we analyze to get accurate results from EXPLAIN\n    parallel $ beforeAll_ analyze . before withApp $\n      describe \"Feature.Query.RangeSpec\" Feature.Query.RangeSpec.spec\n\n    -- this test runs with a different server flag\n    parallel $ before maxRowsApp $\n      describe \"Feature.Query.QueryLimitedSpec\" Feature.Query.QueryLimitedSpec.spec\n\n    -- this test runs with a different schema\n    parallel $ before unicodeApp $\n      describe \"Feature.Query.UnicodeSpec\" Feature.Query.UnicodeSpec.spec\n\n    -- this test runs with openapi-mode set to disabled\n    parallel $ before disabledOpenApi $\n      describe \"Feature.DisabledOpenApiSpec\" Feature.OpenApi.DisabledOpenApiSpec.spec\n\n    -- this test runs with openapi-mode set to ignore-acl\n    parallel $ before ignorePrivOpenApi $\n      describe \"Feature.OpenApi.IgnorePrivOpenApiSpec\" Feature.OpenApi.IgnorePrivOpenApiSpec.spec\n\n    -- this test runs with a proxy\n    parallel $ before proxyApp $\n      describe \"Feature.OpenApi.ProxySpec\" Feature.OpenApi.ProxySpec.spec\n\n    -- this test runs with openapi-security-active set to true\n    parallel $ before securityOpenApi $\n      describe \"Feature.OpenApi.SecurityOpenApiSpec\" Feature.OpenApi.SecurityOpenApiSpec.spec\n\n    -- this test runs without an anonymous role\n    parallel $ before noAnonApp $\n      describe \"Feature.Auth.NoAnonSpec\" Feature.Auth.NoAnonSpec.spec\n\n    -- this test runs without a JWT secret\n    parallel $ before noJwtSecretApp $\n      describe \"Feature.Auth.NoJwtSecretSpec\" Feature.Auth.NoJwtSecretSpec.spec\n\n    -- this test runs with a binary JWT secret\n    parallel $ before binaryJwtApp $\n      describe \"Feature.Auth.BinaryJwtSecretSpec\" Feature.Auth.BinaryJwtSecretSpec.spec\n\n    -- this test runs with a binary JWT secret and an audience claim\n    parallel $ before audJwtApp $\n      describe \"Feature.Auth.AudienceJwtSecretSpec\" Feature.Auth.AudienceJwtSecretSpec.spec\n\n    -- this test runs with asymmetric JWK\n    parallel $ before asymJwkApp $\n      describe \"Feature.Auth.AsymmetricJwtSpec\" Feature.Auth.AsymmetricJwtSpec.spec\n\n    -- this test runs with asymmetric JWKSet\n    parallel $ before asymJwkSetApp $\n      describe \"Feature.Auth.AsymmetricJwtSpec\" Feature.Auth.AsymmetricJwtSpec.spec\n\n    -- this test runs with an extra search path\n    parallel $ before extraSearchPathApp $ do\n      describe \"Feature.ExtraSearchPathSpec\" Feature.ExtraSearchPathSpec.spec\n      describe \"Feature.Query.PostGISSpec\" Feature.Query.PostGISSpec.spec\n\n    -- this test runs with a root spec function override\n    parallel $ before rootSpecApp $\n      describe \"Feature.OpenApi.RootSpec\" Feature.OpenApi.RootSpec.spec\n\n    -- this test runs with a pre request function override\n    parallel $ before responseHeadersApp $\n      describe \"Feature.RpcPreRequestGucsSpec\" Feature.RpcPreRequestGucsSpec.spec\n\n    -- this test runs with multiple schemas\n    parallel $ before multipleSchemaApp $\n      describe \"Feature.Query.MultipleSchemaSpec\" Feature.Query.MultipleSchemaSpec.spec\n\n    -- this test runs with db-plan-enabled = true\n    parallel $ before planEnabledApp $\n      describe \"Feature.Query.PlanSpec.spec\" $ Feature.Query.PlanSpec.spec actualPgVersion\n\n    -- this test runs with server-trace-header set\n    parallel $ before obsApp $\n      describe \"Feature.ObservabilitySpec.spec\" Feature.ObservabilitySpec.spec\n\n    parallel $ before serverTiming $\n      describe \"Feature.Query.ServerTimingSpec.spec\" Feature.Query.ServerTimingSpec.spec\n\n    parallel $ before aggregatesEnabled $\n      describe \"Feature.Query.AggregateFunctionsSpec\" Feature.Query.AggregateFunctionsSpec.allowed\n\n    parallel $ before withApp $\n      describe \"Feature.Query.AggregateFunctionsDisallowedSpec.\" Feature.Query.AggregateFunctionsSpec.disallowed\n\n    -- Note: the rollback tests can not run in parallel, because they test persistance and\n    -- this results in race conditions\n\n    -- this test runs with tx-rollback-all = true and tx-allow-override = true\n    before withApp $\n      describe \"Feature.RollbackAllowedSpec\" Feature.RollbackSpec.allowed\n\n    -- this test runs with tx-rollback-all = false and tx-allow-override = false\n    before disallowRollbackApp $\n      describe \"Feature.RollbackDisallowedSpec\" Feature.RollbackSpec.disallowed\n\n    -- this test runs with tx-rollback-all = true and tx-allow-override = false\n    before forceRollbackApp $\n      describe \"Feature.RollbackForcedSpec\" Feature.RollbackSpec.forced\n\n    -- This test runs with a pre request to enable the pg-safeupdate library per-session.\n    -- This needs to run last, because once pg safe update is loaded, it can't be unloaded again.\n    before pgSafeUpdateApp $\n      describe \"Feature.Query.PgSafeUpdateSpec.spec\" Feature.Query.PgSafeUpdateSpec.spec\n\n  where\n    loadSCache pool conf =\n      either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)\n"
  },
  {
    "path": "test/spec/SpecHelper.hs",
    "content": "module SpecHelper where\n\nimport           Control.Lens           ((^?))\nimport qualified Data.Aeson             as JSON\nimport           Data.Aeson.Lens\nimport qualified Data.ByteString.Base64 as B64 (decodeLenient)\nimport qualified Data.ByteString.Char8  as BS\nimport qualified Data.ByteString.Lazy   as BL\nimport qualified Data.Map.Strict        as M\nimport           Data.Scientific        (toRealFloat)\nimport qualified Data.Set               as S\nimport qualified Jose.Jwa               as JWT\nimport qualified Jose.Jws               as JWT\nimport qualified Jose.Jwt               as JWT\n\nimport Data.Aeson           ((.=))\nimport Data.CaseInsensitive (CI (..), mk, original)\nimport Data.List            (lookup)\nimport Data.List.NonEmpty   (fromList)\nimport Network.Wai.Test     (SResponse (simpleBody, simpleHeaders, simpleStatus))\nimport System.IO.Unsafe     (unsafePerformIO)\nimport System.Process       (readProcess)\nimport Text.Regex.TDFA      ((=~))\n\n\nimport Network.HTTP.Types\nimport Test.Hspec\nimport Test.Hspec.Wai\nimport Text.Heredoc\n\nimport Data.String                       (String)\nimport PostgREST.Config                  (AppConfig (..),\n                                          JSPathExp (..),\n                                          LogLevel (..),\n                                          OpenAPIMode (..),\n                                          Verbosity (..), parseSecret)\nimport PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))\nimport Protolude                         hiding (get, toS)\nimport Protolude.Conv                    (toS)\n\nfilterAndMatchCT :: BS.ByteString -> MatchHeader\nfilterAndMatchCT val = MatchHeader $ \\headers _ ->\n        case filter (\\(n,_) -> n == hContentType) headers of\n          [(_,v)] -> if v == val\n                     then Nothing\n                     else Just $ \"missing value:\" <> toS val <> \"\\n\"\n          _   -> Just \"unexpected header: zero or multiple headers present\\n\"\n\nmatchContentTypeJson :: MatchHeader\nmatchContentTypeJson =\n  filterAndMatchCT \"application/json; charset=utf-8\"\n\nmatchContentTypeSingular :: MatchHeader\nmatchContentTypeSingular =\n  filterAndMatchCT \"application/vnd.pgrst.object+json; charset=utf-8\"\n\nmatchCTArrayStrip :: MatchHeader\nmatchCTArrayStrip =\n  filterAndMatchCT \"application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8\"\n\nmatchCTSingularStrip :: MatchHeader\nmatchCTSingularStrip =\n  filterAndMatchCT \"application/vnd.pgrst.object+json;nulls=stripped; charset=utf-8\"\n\nmatchHeaderValuePresent :: HeaderName -> BS.ByteString -> MatchHeader\nmatchHeaderValuePresent name val = MatchHeader $ \\headers _ ->\n  case lookup name headers of\n    Just hdr -> if val `BS.isInfixOf` hdr then Nothing else Just $ \"missing header value: \" <> toS val <> \"\\n\"\n    Nothing  -> Just $ \"missing header: \" <> toS (original name) <> \"\\n\"\n\nmatchHeaderAbsent :: HeaderName -> MatchHeader\nmatchHeaderAbsent name = MatchHeader $ \\headers _body ->\n  case lookup name headers of\n    Just _  -> Just $ \"unexpected header: \" <> toS (original name) <> \"\\n\"\n    Nothing -> Nothing\n\n-- | Matches Server-Timing header has a well-formed metric with the given name\nmatchServerTimingHasTiming :: String -> MatchHeader\nmatchServerTimingHasTiming metric = MatchHeader $ \\headers _body ->\n  case lookup \"Server-Timing\" headers of\n    Just hdr -> if hdr =~ (metric <> \";dur=[[:digit:]]+.[[:digit:]]+\")\n                  then Nothing\n                  else Just $ \"missing metric: \" <> metric <> \"\\n\"\n    Nothing  -> Just \"missing Server-Timing header\\n\"\n\nvalidateOpenApiResponse :: [Header] -> WaiSession () ()\nvalidateOpenApiResponse headers = do\n  r <- request methodGet \"/\" headers \"\"\n  liftIO $\n    let respStatus = simpleStatus r in\n    respStatus `shouldSatisfy`\n      \\s -> s == Status { statusCode = 200, statusMessage=\"OK\" }\n  liftIO $\n    let respHeaders = simpleHeaders r in\n    respHeaders `shouldSatisfy`\n      \\hs -> (\"Content-Type\", \"application/openapi+json; charset=utf-8\") `elem` hs\n  Just body <- pure $ JSON.decode (simpleBody r)\n  Just schema <- liftIO $ JSON.decode <$> BL.readFile \"test/spec/fixtures/openapi.json\"\n  let args :: M.Map Text JSON.Value\n      args = M.fromList\n        [ ( \"schema\", schema )\n        , ( \"data\", body ) ]\n      hdrs = acceptHdrs \"application/json\"\n  request methodPost \"/rpc/validate_json_schema\" hdrs (JSON.encode args)\n      `shouldRespondWith` \"true\"\n      { matchStatus = 200\n      , matchHeaders = []\n      }\n\n\nbaseCfg :: AppConfig\nbaseCfg = let secret = encodeUtf8 \"reallyreallyreallyreallyverysafe\" in\n  AppConfig {\n    configAppSettings               = [ (\"app.settings.app_host\", \"localhost\") , (\"app.settings.external_api_secret\", \"0123456789abcdef\") ]\n  , configClientErrorVerbosity      = Verbose\n  , configDbAggregates              = False\n  , configDbAnonRole                = Just \"postgrest_test_anonymous\"\n  , configDbChannel                 = mempty\n  , configDbChannelEnabled          = True\n  , configDbExtraSearchPath         = []\n  , configDbHoistedTxSettings       = [\"default_transaction_isolation\",\"plan_filter.statement_cost_limit\",\"statement_timeout\"]\n  , configDbMaxRows                 = Nothing\n  , configDbPlanEnabled             = False\n  , configDbPoolSize                = 10\n  , configDbPoolAcquisitionTimeout  = 10\n  , configDbPoolMaxLifetime         = 1800\n  , configDbPoolMaxIdletime         = 600\n  , configDbPoolAutomaticRecovery   = True\n  , configDbPreRequest              = Just $ QualifiedIdentifier \"test\" \"switch_role\"\n  , configDbPreparedStatements      = True\n  , configDbRootSpec                = Nothing\n  , configDbSchemas                 = fromList [\"test\"]\n  , configDbConfig                  = False\n  , configDbPreConfig               = Nothing\n  , configDbUri                     = \"postgresql://\"\n  , configFilePath                  = Nothing\n  , configJWKS                      = rightToMaybe $ parseSecret secret\n  , configJwtAudience               = Nothing\n  , configJwtRoleClaimKey           = [JSPKey \"role\"]\n  , configJwtSecret                 = Just secret\n  , configJwtSecretIsBase64         = False\n  , configJwtCacheMaxEntries        = 10\n  , configLogLevel                  = LogCrit\n  , configLogQuery                  = False\n  , configOpenApiMode               = OAFollowPriv\n  , configOpenApiSecurityActive     = False\n  , configOpenApiServerProxyUri     = Nothing\n  , configServerCorsAllowedOrigins  = Nothing\n  , configServerHost                = \"localhost\"\n  , configServerPort                = 3000\n  , configServerTraceHeader         = Nothing\n  , configServerUnixSocket          = Nothing\n  , configServerUnixSocketMode      = 432\n  , configDbTxAllowOverride         = True\n  , configDbTxRollbackAll           = True\n  , configAdminServerHost           = \"localhost\"\n  , configAdminServerPort           = Nothing\n  , configRoleSettings              = mempty\n  , configRoleIsoLvl                = mempty\n  , configInternalSCQuerySleep      = Nothing\n  , configInternalSCLoadSleep       = Nothing\n  , configInternalSCRelLoadSleep    = Nothing\n  , configServerTimingEnabled       = True\n  }\n\ntestCfg :: AppConfig\ntestCfg = baseCfg\n\ntestCfgDisallowRollback :: AppConfig\ntestCfgDisallowRollback = baseCfg { configDbTxAllowOverride = False, configDbTxRollbackAll = False }\n\ntestCfgForceRollback :: AppConfig\ntestCfgForceRollback = baseCfg { configDbTxAllowOverride = False, configDbTxRollbackAll = True }\n\ntestCfgNoAnon :: AppConfig\ntestCfgNoAnon = baseCfg { configDbAnonRole = Nothing }\n\ntestCfgNoJwtSecret :: AppConfig\ntestCfgNoJwtSecret = baseCfg { configJwtSecret = Nothing, configJWKS = Nothing }\n\ntestUnicodeCfg :: AppConfig\ntestUnicodeCfg = baseCfg { configDbSchemas = fromList [\"تست\"] }\n\ntestMaxRowsCfg :: AppConfig\ntestMaxRowsCfg = baseCfg { configDbMaxRows = Just 2 }\n\ntestDisabledOpenApiCfg :: AppConfig\ntestDisabledOpenApiCfg = baseCfg { configOpenApiMode = OADisabled }\n\ntestIgnorePrivOpenApiCfg :: AppConfig\ntestIgnorePrivOpenApiCfg = baseCfg { configOpenApiMode = OAIgnorePriv, configDbSchemas = fromList [\"test\", \"v1\"] }\n\ntestProxyCfg :: AppConfig\ntestProxyCfg = baseCfg { configOpenApiServerProxyUri = Just \"https://postgrest.com/openapi.json\" }\n\ntestSecurityOpenApiCfg :: AppConfig\ntestSecurityOpenApiCfg = baseCfg { configOpenApiSecurityActive = True }\n\ntestPlanEnabledCfg :: AppConfig\ntestPlanEnabledCfg = baseCfg { configDbPlanEnabled = True }\n\ntestCfgBinaryJWT :: AppConfig\ntestCfgBinaryJWT =\n  baseCfg {\n    configJwtSecret = Just generateSecret\n  , configJWKS = rightToMaybe $ parseSecret generateSecret\n  }\n\ntestCfgAudienceJWT :: AppConfig\ntestCfgAudienceJWT =\n  baseCfg {\n    configJwtSecret = Just generateSecret\n  , configJwtAudience = Just \"youraudience\"\n  , configJWKS = rightToMaybe $ parseSecret generateSecret\n  }\n\ntestCfgAsymJWK :: AppConfig\ntestCfgAsymJWK =\n  let secret = encodeUtf8 [str|{\"alg\":\"RS256\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"kty\":\"RSA\",\"n\":\"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ\",\"use\":\"sig\"}|]\n  in baseCfg {\n    configJwtSecret = Just secret\n  , configJWKS = rightToMaybe $ parseSecret secret\n  }\n\ntestCfgAsymJWKSet :: AppConfig\ntestCfgAsymJWKSet =\n  let secret = encodeUtf8 [str|{\"keys\": [{\"alg\":\"RS256\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"kty\":\"RSA\",\"n\":\"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ\",\"use\":\"sig\"}]}|]\n  in baseCfg {\n    configJwtSecret = Just secret\n  , configJWKS = rightToMaybe $ parseSecret secret\n  }\n\ntestCfgExtraSearchPath :: AppConfig\ntestCfgExtraSearchPath = baseCfg { configDbExtraSearchPath = [\"public\", \"extensions\", \"EXTRA \\\"@/\\\\#~_-\"] }\n\ntestCfgRootSpec :: AppConfig\ntestCfgRootSpec = baseCfg { configDbRootSpec = Just $ QualifiedIdentifier mempty \"root\"}\n\ntestCfgResponseHeaders :: AppConfig\ntestCfgResponseHeaders = baseCfg { configDbPreRequest = Just $ QualifiedIdentifier mempty \"custom_headers\" }\n\ntestMultipleSchemaCfg :: AppConfig\ntestMultipleSchemaCfg = baseCfg { configDbSchemas = fromList [\"v1\", \"v2\", \"SPECIAL \\\"@/\\\\#~_-\"] }\n\ntestPgSafeUpdateEnabledCfg :: AppConfig\ntestPgSafeUpdateEnabledCfg = baseCfg { configDbPreRequest = Just $ QualifiedIdentifier \"test\" \"load_safeupdate\" }\n\ntestObservabilityCfg :: AppConfig\ntestObservabilityCfg = baseCfg { configServerTraceHeader = Just $ mk \"X-Request-Id\" }\n\ntestCfgServerTiming :: AppConfig\ntestCfgServerTiming = baseCfg { configDbPlanEnabled = True }\n\ntestCfgAggregatesEnabled :: AppConfig\ntestCfgAggregatesEnabled = baseCfg { configDbAggregates = True }\n\nanalyzeTable :: Text -> IO ()\nanalyzeTable tableName =\n  void $ readProcess \"psql\" [\"-U\", \"postgres\", \"--set\", \"ON_ERROR_STOP=1\", \"-a\", \"-c\", toS $ \"ANALYZE test.\\\"\" <> tableName <> \"\\\"\"] []\n\nrangeHdrs :: ByteRange -> [Header]\nrangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]\n\nrangeHdrsWithCount :: ByteRange -> [Header]\nrangeHdrsWithCount r = (\"Prefer\", \"count=exact\") : rangeHdrs r\n\nacceptHdrs :: BS.ByteString -> [Header]\nacceptHdrs mime = [(hAccept, mime)]\n\nplanHdr :: Header\nplanHdr = (hAccept, \"application/vnd.pgrst.plan+json\")\n\nrangeUnit :: Header\nrangeUnit = (\"Range-Unit\" :: CI BS.ByteString, \"items\")\n\nmatchHeader :: CI BS.ByteString -> BS.ByteString -> [Header] -> Bool\nmatchHeader name valRegex headers =\n  maybe False (=~ valRegex) $ lookup name headers\n\nnoBlankHeader :: [Header] -> Bool\nnoBlankHeader = notElem mempty\n\nnoProfileHeader :: [Header] -> Bool\nnoProfileHeader headers = isNothing $ find ((== \"Content-Profile\") . fst) headers\n\nnotZeroContentLength :: [Header] -> Bool\nnotZeroContentLength headers = maybe False (/= \"0\") $ lookup hContentLength headers\n\nauthHeader :: BS.ByteString -> BS.ByteString -> Header\nauthHeader typ creds =\n  (hAuthorization, typ <> \" \" <> creds)\n\nauthHeaderJWT :: BS.ByteString -> Header\nauthHeaderJWT = authHeader \"Bearer\"\n\ngenerateSecret :: ByteString\ngenerateSecret = B64.decodeLenient \"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=\"\n\ngenerateJWT :: BL.ByteString -> ByteString\ngenerateJWT claims =\n  either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)\n\n-- | Tests whether the text can be parsed as a json object containing\n-- the key \"message\", and optional keys \"details\", \"hint\", \"code\",\n-- and no extraneous keys\nisErrorFormat :: BL.ByteString -> Bool\nisErrorFormat s =\n  \"message\" `S.member` keys &&\n    S.null (S.difference keys validKeys)\n where\n  obj = JSON.decode s :: Maybe (M.Map Text JSON.Value)\n  keys = maybe S.empty M.keysSet obj\n  validKeys = S.fromList [\"message\", \"details\", \"hint\", \"code\"]\n\nplanCost :: SResponse -> Float\nplanCost resp =\n  let res = simpleBody resp ^? nth 0 . key \"Plan\" . key \"Total Cost\" in\n  -- big value in case parsing fails\n  fromMaybe 1000000000.0 $ unbox =<< res\n  where\n    unbox :: JSON.Value -> Maybe Float\n    unbox (JSON.Number n) = Just $ toRealFloat n\n    unbox _               = Nothing\n\ndata TiobePlsRow = TiobePlsRow {\n  name' :: Text,\n  rank  :: Int\n} deriving (Show)\n\ninstance JSON.ToJSON TiobePlsRow where\n  toJSON (TiobePlsRow name'' rank') = JSON.object [\"name\" .= name'', \"rank\" .= rank']\n\ngetInsertDataForTiobePlsTable :: Int -> BL.ByteString\ngetInsertDataForTiobePlsTable rows =\n  JSON.encode $ fromList $ [TiobePlsRow {name' = nm, rank = rk} | (nm,rk) <- nameRankList]\n   where\n     nameRankList = [(\"Lang \" <> show i, i) | i <- [20..(rows+20)] ] :: [(Text, Int)]\n\nreadFixtureFile :: FilePath -> BL.ByteString\nreadFixtureFile file = unsafePerformIO $ BL.readFile $ \"test/spec/fixtures/\" <> file\n"
  },
  {
    "path": "test/spec/fixtures/data.sql",
    "content": "--\n-- PostgreSQL database dump\n--\n\n-- Dumped from database version 9.5beta1\n-- Dumped by pg_dump version 9.5beta1\n\nSET statement_timeout = 0;\nSET lock_timeout = 0;\nSET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSET check_function_bodies = false;\nSET client_min_messages = warning;\n\nSET search_path = postgrest, pg_catalog;\n\n--\n-- Data for Name: auth; Type: TABLE DATA; Schema: postgrest; Owner: -\n--\n\nTRUNCATE TABLE auth CASCADE;\nINSERT INTO auth VALUES ('jdoe', 'postgrest_test_author', '1234                                                        ');\n\n\nSET search_path = private, pg_catalog;\n\n--\n-- Data for Name: articles; Type: TABLE DATA; Schema: private; Owner: -\n--\n\nTRUNCATE TABLE articles CASCADE;\nINSERT INTO articles VALUES (1, 'No… It''s a thing; it''s like a plan, but with more greatness.', 'diogo');\nINSERT INTO articles VALUES (2, 'Stop talking, brain thinking. Hush.', 'diogo');\nINSERT INTO articles VALUES (3, 'It''s a fez. I wear a fez now. Fezes are cool.', 'diogo');\n\n\nSET search_path = test, pg_catalog;\n\n--\n-- Data for Name: users; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE users CASCADE;\nINSERT INTO users VALUES (1, 'Angela Martin');\nINSERT INTO users VALUES (2, 'Michael Scott');\nINSERT INTO users VALUES (3, 'Dwight Schrute');\n\n\nSET search_path = private, pg_catalog;\n\n--\n-- Data for Name: article_stars; Type: TABLE DATA; Schema: private; Owner: -\n--\n\nTRUNCATE TABLE article_stars CASCADE;\nINSERT INTO article_stars VALUES (1, 1, '2015-12-08 04:22:57.472738');\nINSERT INTO article_stars VALUES (1, 2, '2015-12-08 04:22:57.472738');\nINSERT INTO article_stars VALUES (2, 3, '2015-12-08 04:22:57.472738');\nINSERT INTO article_stars VALUES (3, 2, '2015-12-08 04:22:57.472738');\nINSERT INTO article_stars VALUES (1, 3, '2015-12-08 04:22:57.472738');\n\n\nSET search_path = test, pg_catalog;\n\n--\n-- Data for Name: authors_only; Type: TABLE DATA; Schema: test; Owner: -\n--\nTRUNCATE TABLE authors_only CASCADE;\n\n\n--\n-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: test; Owner: -\n--\nTRUNCATE TABLE auto_incrementing_pk CASCADE;\n\n\n--\n-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -\n--\n\nSELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 1, true);\n\n\n--\n-- Data for Name: clients; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE clients CASCADE;\nINSERT INTO clients VALUES (1, 'Microsoft');\nINSERT INTO clients VALUES (2, 'Apple');\n\n\n--\n-- Data for Name: projects; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE projects CASCADE;\nINSERT INTO projects VALUES (1, 'Windows 7', 1);\nINSERT INTO projects VALUES (2, 'Windows 10', 1);\nINSERT INTO projects VALUES (3, 'IOS', 2);\nINSERT INTO projects VALUES (4, 'OSX', 2);\nINSERT INTO projects VALUES (5, 'Orphan', NULL);\n\n\n--\n-- Data for Name: tasks; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE tasks CASCADE;\nINSERT INTO tasks VALUES (1, 'Design w7', 1);\nINSERT INTO tasks VALUES (2, 'Code w7', 1);\nINSERT INTO tasks VALUES (3, 'Design w10', 2);\nINSERT INTO tasks VALUES (4, 'Code w10', 2);\nINSERT INTO tasks VALUES (5, 'Design IOS', 3);\nINSERT INTO tasks VALUES (6, 'Code IOS', 3);\nINSERT INTO tasks VALUES (7, 'Design OSX', 4);\nINSERT INTO tasks VALUES (8, 'Code OSX', 4);\n\n\n--\n-- Data for Name: users_tasks; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE users_tasks CASCADE;\nINSERT INTO users_tasks VALUES (1, 1);\nINSERT INTO users_tasks VALUES (1, 2);\nINSERT INTO users_tasks VALUES (1, 3);\nINSERT INTO users_tasks VALUES (1, 4);\nINSERT INTO users_tasks VALUES (2, 5);\nINSERT INTO users_tasks VALUES (2, 6);\nINSERT INTO users_tasks VALUES (2, 7);\nINSERT INTO users_tasks VALUES (3, 1);\nINSERT INTO users_tasks VALUES (3, 5);\n\n\n--\n-- Data for Name: comments; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE comments CASCADE;\nINSERT INTO comments VALUES (1, 1, 2, 6, 'Needs to be delivered ASAP');\n\n--\n-- Data for Name: files; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE files CASCADE;\nINSERT INTO files VALUES\n\t (1, 'command.com', '#include <unix.h>')\n\t,(1, 'autoexec.bat', '@ECHO OFF')\n\t,(1, 'io.sys', 'TODO')\n\t,(2, 'README.md', '# make $$$!')\n\t,(2, 'marketing.key', '$-$')\n\t;\n\nTRUNCATE TABLE touched_files CASCADE;\nINSERT INTO touched_files VALUES\n\t (1, 1, 1, 'command.com')\n\t,(1, 1, 1, 'autoexec.bat')\n\t,(1, 1, 2, 'README.md')\n\t,(3, 1, 1, 'autoexec.bat')\n\t;\n\n--\n-- Data for Name: complex_items; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE complex_items CASCADE;\nINSERT INTO complex_items VALUES (1, 'One', '{\"foo\":{\"int\":1,\"bar\":\"baz\"}}', '{1}');\nINSERT INTO complex_items VALUES (2, 'Two', '{\"foo\":{\"int\":1,\"bar\":\"baz\"}}', '{1,2}');\nINSERT INTO complex_items VALUES (3, 'Three', '{\"foo\":{\"int\":1,\"bar\":\"baz\"}}', '{1,2,3}', 3);\n\n\n--\n-- Data for Name: compound_pk; Type: TABLE DATA; Schema: test; Owner: -\n--\nTRUNCATE TABLE compound_pk CASCADE;\n\n\n--\n-- Data for Name: simple_pk; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE simple_pk CASCADE;\nINSERT INTO simple_pk VALUES ('xyyx', 'u');\nINSERT INTO simple_pk VALUES ('xYYx', 'v');\n\n--\n-- Data for Name: has_fk; Type: TABLE DATA; Schema: test; Owner: -\n--\nTRUNCATE TABLE has_fk CASCADE;\n\n\n--\n-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -\n--\n\nSELECT pg_catalog.setval('has_fk_id_seq', 1, false);\n\n\n--\n-- Data for Name: items; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE items CASCADE;\nINSERT INTO items VALUES (1);\nINSERT INTO items VALUES (2);\nINSERT INTO items VALUES (3);\nINSERT INTO items VALUES (4);\nINSERT INTO items VALUES (5);\nINSERT INTO items VALUES (6);\nINSERT INTO items VALUES (7);\nINSERT INTO items VALUES (8);\nINSERT INTO items VALUES (9);\nINSERT INTO items VALUES (10);\nINSERT INTO items VALUES (11);\nINSERT INTO items VALUES (12);\nINSERT INTO items VALUES (13);\nINSERT INTO items VALUES (14);\nINSERT INTO items VALUES (15);\n\n\n--\n-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -\n--\n\nSELECT pg_catalog.setval('items_id_seq', 15, true);\n\n\n--\n-- Data for Name: items2; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE items2 CASCADE;\nINSERT INTO items2 VALUES (1);\nINSERT INTO items2 VALUES (2);\nINSERT INTO items2 VALUES (3);\nINSERT INTO items2 VALUES (4);\nINSERT INTO items2 VALUES (5);\nINSERT INTO items2 VALUES (6);\nINSERT INTO items2 VALUES (7);\nINSERT INTO items2 VALUES (8);\nINSERT INTO items2 VALUES (9);\nINSERT INTO items2 VALUES (10);\nINSERT INTO items2 VALUES (11);\nINSERT INTO items2 VALUES (12);\nINSERT INTO items2 VALUES (13);\nINSERT INTO items2 VALUES (14);\nINSERT INTO items2 VALUES (15);\n\n\n--\n-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -\n--\n\nSELECT pg_catalog.setval('items2_id_seq', 15, true);\n\n\n--\n-- Data for Name: json_table; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE json_table CASCADE;\nINSERT INTO json_table VALUES ('{\"foo\":{\"bar\":\"baz\"},\"id\":1}');\nINSERT INTO json_table VALUES ('{\"id\":3}');\nINSERT INTO json_table VALUES ('{\"id\":0}');\n\n\n--\n-- Data for Name: menagerie; Type: TABLE DATA; Schema: test; Owner: -\n--\nTRUNCATE TABLE menagerie CASCADE;\n\n\n--\n-- Data for Name: no_pk; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE no_pk CASCADE;\nINSERT INTO no_pk VALUES (NULL, NULL);\nINSERT INTO no_pk VALUES ('1', '0');\nINSERT INTO no_pk VALUES ('2', '0');\n\n\n--\n-- Data for Name: nullable_integer; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE nullable_integer CASCADE;\nINSERT INTO nullable_integer VALUES (NULL);\n\n\n--\n-- Data for Name: tsearch; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE tsearch CASCADE;\nINSERT INTO tsearch VALUES (to_tsvector('It''s kind of fun to do the impossible'));\nINSERT INTO tsearch VALUES (to_tsvector('But also fun to do what is possible'));\nINSERT INTO tsearch VALUES (to_tsvector('Fat cats ate rats'));\nINSERT INTO tsearch VALUES (to_tsvector('french', 'C''est un peu amusant de faire l''impossible'));\nINSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));\n\n--\n-- Data for Name: users_projects; Type: TABLE DATA; Schema: test; Owner: -\n--\n\nTRUNCATE TABLE users_projects CASCADE;\nINSERT INTO users_projects VALUES (1, 1);\nINSERT INTO users_projects VALUES (1, 2);\nINSERT INTO users_projects VALUES (2, 3);\nINSERT INTO users_projects VALUES (2, 4);\nINSERT INTO users_projects VALUES (3, 1);\nINSERT INTO users_projects VALUES (3, 3);\n\nTRUNCATE TABLE \"Escap3e;\" CASCADE;\nINSERT INTO \"Escap3e;\" VALUES (1), (2), (3), (4), (5);\n\nTRUNCATE TABLE \"ghostBusters\" CASCADE;\nINSERT INTO \"ghostBusters\" VALUES (1), (3), (5);\n\nTRUNCATE TABLE \"withUnique\" CASCADE;\nINSERT INTO \"withUnique\" VALUES ('nodup', 'blah');\n\n\n\nTRUNCATE TABLE addresses CASCADE;\nINSERT INTO addresses VALUES (1, 'address 1');\nINSERT INTO addresses VALUES (2, 'address 2');\nINSERT INTO addresses VALUES (3, 'address 3');\nINSERT INTO addresses VALUES (4, 'address 4');\n\nTRUNCATE TABLE orders CASCADE;\nINSERT INTO orders VALUES (1, 'order 1', 1, 2);\nINSERT INTO orders VALUES (2, 'order 2', 3, 4);\n\nTRUNCATE TABLE images CASCADE;\nINSERT INTO images(name, img) VALUES ('A.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC', 'base64'));\nINSERT INTO images(name, img) VALUES ('B.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=', 'base64'));\n\nTRUNCATE TABLE w_or_wo_comma_names CASCADE;\nINSERT INTO w_or_wo_comma_names VALUES ('Hebdon, John');\nINSERT INTO w_or_wo_comma_names VALUES ('Williams, Mary');\nINSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph');\nINSERT INTO w_or_wo_comma_names VALUES ('David White');\nINSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');\nINSERT INTO w_or_wo_comma_names VALUES ('Double O Seven(007)');\nINSERT INTO w_or_wo_comma_names VALUES ('\"');\nINSERT INTO w_or_wo_comma_names VALUES ('Double\"Quote\"McGraw\"');\nINSERT INTO w_or_wo_comma_names VALUES ('\\');\nINSERT INTO w_or_wo_comma_names VALUES ('/\\Slash/\\Beast/\\');\n\nTRUNCATE TABLE items_with_different_col_types CASCADE;\nINSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);\n\nTRUNCATE TABLE entities CASCADE;\nINSERT INTO entities VALUES (1, 'entity 1', '{1}', '''bar'':2 ''foo'':1');\nINSERT INTO entities VALUES (2, 'entity 2', '{1,2}', '''baz'':1 ''qux'':2');\nINSERT INTO entities VALUES (3, 'entity 3', '{1,2,3}', null);\nINSERT INTO entities VALUES (4, null, null, null);\n\nTRUNCATE TABLE child_entities CASCADE;\nINSERT INTO child_entities VALUES (1, 'child entity 1', 1);\nINSERT INTO child_entities VALUES (2, 'child entity 2', 1);\nINSERT INTO child_entities VALUES (3, 'child entity 3', 2);\nINSERT INTO child_entities VALUES (4, 'child entity 4', 1);\nINSERT INTO child_entities VALUES (5, 'child entity 5', 1);\nINSERT INTO child_entities VALUES (6, 'child entity 6', 2);\n\nTRUNCATE TABLE grandchild_entities CASCADE;\nINSERT INTO grandchild_entities VALUES (1, 'grandchild entity 1', 1, null, null, null);\nINSERT INTO grandchild_entities VALUES (2, 'grandchild entity 2', 1, null, null, null);\nINSERT INTO grandchild_entities VALUES (3, 'grandchild entity 3', 2, null, null, null);\nINSERT INTO grandchild_entities VALUES (4, '(grandchild,entity,4)', 2, null, null, '{\"a\": {\"b\":\"foo\"}}');\nINSERT INTO grandchild_entities VALUES (5, '(grandchild,entity,5)', 2, null, null, '{\"b\":\"bar\"}');\n\nTRUNCATE TABLE ranges CASCADE;\nINSERT INTO ranges VALUES (1, '[1,3]');\nINSERT INTO ranges VALUES (2, '[3,6]');\nINSERT INTO ranges VALUES (3, '[6,9]');\nINSERT INTO ranges VALUES (4, '[9,12]');\nINSERT INTO ranges VALUES (5, null);\n\nTRUNCATE TABLE being CASCADE;\nINSERT INTO being VALUES (1), (2), (3), (4);\n\nTRUNCATE TABLE descendant CASCADE;\nINSERT INTO descendant VALUES (1,1), (2,1), (3,1), (4,2);\n\nTRUNCATE TABLE part CASCADE;\nINSERT INTO part VALUES (1), (2), (3), (4);\n\nTRUNCATE TABLE being_part CASCADE;\nINSERT INTO being_part VALUES (1,1), (2,1), (3,2), (4,3);\n\nTRUNCATE TABLE employees CASCADE;\nINSERT INTO employees VALUES\n  ('Frances M.', 'Roe', '24000', 'One-Up Realty', 'Author'),\n  ('Daniel B.', 'Lyon', '36000', 'Dubrow''s Cafeteria', 'Packer'),\n  ('Edwin S.', 'Smith', '48000', 'Pro Garden Management', 'Marine biologist');\n\nTRUNCATE TABLE tiobe_pls CASCADE;\nINSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4);\n\nTRUNCATE TABLE single_unique CASCADE;\nINSERT INTO single_unique (unique_key, value) VALUES (1, 'A');\n\nTRUNCATE TABLE compound_unique CASCADE;\nINSERT INTO compound_unique (key1, key2, value) VALUES (1, 1, 'A');\n\nTRUNCATE TABLE only_pk CASCADE;\nINSERT INTO only_pk VALUES (1), (2);\n\nTRUNCATE TABLE family_tree CASCADE;\nINSERT INTO family_tree VALUES ('1', 'Parental Unit', NULL);\nINSERT INTO family_tree VALUES ('2', 'Kid One', '1');\nINSERT INTO family_tree VALUES ('3', 'Kid Two', '1');\nINSERT INTO family_tree VALUES ('4', 'Grandkid One', '2');\nINSERT INTO family_tree VALUES ('5', 'Grandkid Two', '3');\n\nTRUNCATE TABLE managers CASCADE;\nINSERT INTO managers VALUES (1, 'Referee Manager');\nINSERT INTO managers VALUES (2, 'Auditor Manager');\nINSERT INTO managers VALUES (3, 'Acme Manager');\nINSERT INTO managers VALUES (4, 'Umbrella Manager');\nINSERT INTO managers VALUES (5, 'Cyberdyne Manager');\nINSERT INTO managers VALUES (6, 'Oscorp Manager');\n\nTRUNCATE TABLE organizations CASCADE;\nINSERT INTO organizations VALUES (1, 'Referee Org', null, null, 1);\nINSERT INTO organizations VALUES (2, 'Auditor Org', null, null, 2);\nINSERT INTO organizations VALUES (3, 'Acme', 1, 2, 3);\nINSERT INTO organizations VALUES (4, 'Umbrella', 1, 2, 4);\nINSERT INTO organizations VALUES (5, 'Cyberdyne', 3, 4, 5);\nINSERT INTO organizations VALUES (6, 'Oscorp', 3, 4, 6);\n\nSET search_path = private, pg_catalog;\n\nTRUNCATE TABLE authors CASCADE;\nINSERT INTO authors VALUES (1, 'George Orwell');\nINSERT INTO authors VALUES (2, 'Anne Frank');\nINSERT INTO authors VALUES (3, 'Antoine de Saint-Exupéry');\nINSERT INTO authors VALUES (4, 'J.D. Salinger');\nINSERT INTO authors VALUES (5, 'Ray Bradbury');\nINSERT INTO authors VALUES (6, 'William Golding');\nINSERT INTO authors VALUES (7, 'Harper Lee');\nINSERT INTO authors VALUES (8, 'Kurt Vonnegut');\nINSERT INTO authors VALUES (9, 'Ken Kesey');\nINSERT INTO authors VALUES (10, 'Fyodor Dostoevsky');\n\nTRUNCATE TABLE publishers CASCADE;\nINSERT INTO publishers VALUES (1, 'Secker & Warburg');\nINSERT INTO publishers VALUES (2, 'Contact Publishing');\nINSERT INTO publishers VALUES (3, 'Reynal & Hitchcock');\nINSERT INTO publishers VALUES (4, 'Little, Brown and Company');\nINSERT INTO publishers VALUES (5, 'Ballantine Books');\nINSERT INTO publishers VALUES (6, 'Faber and Faber');\nINSERT INTO publishers VALUES (7, 'J. B. Lippincott & Co.');\nINSERT INTO publishers VALUES (8, 'Delacorte');\nINSERT INTO publishers VALUES (9, 'Viking Press & Signet Books');\n\nTRUNCATE TABLE books CASCADE;\nINSERT INTO books VALUES (1, '1984', 1949, 1, 1);\nINSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2, 2);\nINSERT INTO books VALUES (3, 'The Little Prince', 1947, 3, 3);\nINSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4, 4);\nINSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5, 5);\nINSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6, 6);\nINSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7, 7);\nINSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8, 8);\nINSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9, 9);\nINSERT INTO books VALUES (10, 'Crime and Punishment', 1866, 10, null);\n\nSET search_path = test, pg_catalog;\n\nTRUNCATE TABLE person CASCADE;\n\nINSERT INTO person VALUES (1, 'John');\nINSERT INTO person VALUES (2, 'Jane');\nINSERT INTO person VALUES (3, 'Jake');\nINSERT INTO person VALUES (4, 'Julie');\n\nTRUNCATE TABLE message CASCADE;\nINSERT INTO message VALUES (1, 'Hello Jane', 1, 2);\nINSERT INTO message VALUES (2, 'Hi John', 2, 1);\nINSERT INTO message VALUES (3, 'How are you doing?', 1, 2);\nINSERT INTO message VALUES (4, 'Hey Julie', 3, 4);\nINSERT INTO message VALUES (5, 'What''s up Jake', 4, 3);\n\nTRUNCATE TABLE space CASCADE;\nINSERT INTO space VALUES (1, 'space 1');\n\nTRUNCATE TABLE zone CASCADE;\nINSERT INTO zone VALUES (1, 'zone 1', 2, 1);\nINSERT INTO zone VALUES (2, 'zone 2', 2, 1);\nINSERT INTO zone VALUES (3, 'store 3', 3, 1);\nINSERT INTO zone VALUES (4, 'store 4', 3, 1);\n\n-- for foreign table projects_dump\ncopy (select id, name, client_id from projects) to '/tmp/projects_dump.csv' with csv;\n\nTRUNCATE TABLE \"UnitTest\" CASCADE;\nINSERT INTO \"UnitTest\" VALUES (1, 'unit test 1');\n\nTRUNCATE TABLE json_arr CASCADE;\nINSERT INTO json_arr VALUES (1, '[1, 2, 3]');\nINSERT INTO json_arr VALUES (2, '[4, 5, 6]');\nINSERT INTO json_arr VALUES (3, '[[9, 8, 7], [11, 12, 13]]');\nINSERT INTO json_arr VALUES (4, '[[[5, 6], 7, 8]]');\nINSERT INTO json_arr VALUES (5, '[{\"a\": \"A\"}, {\"b\": \"B\"}]');\nINSERT INTO json_arr VALUES (6, '[{\"a\": [1,2,3]}, {\"b\": [4,5]}]');\nINSERT INTO json_arr VALUES (7, '{\"c\": [1,2,3], \"d\": [4,5]}');\nINSERT INTO json_arr VALUES (8, '{\"c\": [{\"d\": [4,5,6,7,8]}]}');\nINSERT INTO json_arr VALUES (9, '[{\"0xy1\": [1,{\"23-xy-45\": [2, {\"xy-6\": [3]}]}]}]');\nINSERT INTO json_arr VALUES (10, '{\"!@#$%^&*_a\": [{\"!@#$%^&*_b\": 1}, {\"!@#$%^&*_c\": [2]}], \"!@#$%^&*_d\": {\"!@#$%^&*_e\": 3}}');\n\nTRUNCATE TABLE jsonb_test CASCADE;\nINSERT INTO jsonb_test VALUES (1, '{ \"a\": {\"b\": 2} }');\nINSERT INTO jsonb_test VALUES (2, '{ \"c\": [1,2,3] }');\nINSERT INTO jsonb_test VALUES (3, '[{ \"d\": \"test\" }]');\nINSERT INTO jsonb_test VALUES (4, '{ \"e\": 1 }');\n\nTRUNCATE TABLE private.player CASCADE;\nINSERT into private.player\nSELECT\n  generate_series,\n  'first_name_' || generate_series,\n  'last_name_' || generate_series,\n  '2018-10-11'\nFROM generate_series(1, 12);\n\nTRUNCATE TABLE contract CASCADE;\ninsert into contract\nselect\n  'tournament_' || generate_series,\n  tsrange(now()::timestamp, null),\n  10*generate_series,\n  generate_series,\n  'first_name_' || generate_series,\n  'last_name_' || generate_series,\n  '2018-10-11'\nfrom generate_series(1, 6);\n\nTRUNCATE TABLE ltree_sample CASCADE;\nINSERT INTO ltree_sample VALUES ('Top');\nINSERT INTO ltree_sample VALUES ('Top.Science');\nINSERT INTO ltree_sample VALUES ('Top.Science.Astronomy');\nINSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Astrophysics');\nINSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Cosmology');\nINSERT INTO ltree_sample VALUES ('Top.Hobbies');\nINSERT INTO ltree_sample VALUES ('Top.Hobbies.Amateurs_Astronomy');\nINSERT INTO ltree_sample VALUES ('Top.Collections');\nINSERT INTO ltree_sample VALUES ('Top.Collections.Pictures');\nINSERT INTO ltree_sample VALUES ('Top.Collections.Pictures.Astronomy');\nINSERT INTO ltree_sample VALUES ('Top.Collections.Pictures.Astronomy.Stars');\nINSERT INTO ltree_sample VALUES ('Top.Collections.Pictures.Astronomy.Galaxies');\nINSERT INTO ltree_sample VALUES ('Top.Collections.Pictures.Astronomy.Astronauts');\n\nTRUNCATE TABLE isn_sample CASCADE;\nINSERT INTO isn_sample VALUES ('978-0-393-04002-9', 'Mathematics: From the Birth of Numbers');\n\nTRUNCATE TABLE \"Server Today\" CASCADE;\nCOPY \"Server Today\" (\"cHostname\", \"Just A Server Model\") FROM STDIN CSV DELIMITER '|';\nargnim1    | IBM,9113-550 (P5-550)\nargnim2    | IBM,9113-550 (P5-550)\ndaaa2nim71 | IBM,9131-52A (P5-52A)\ndaah3nim71 | IBM,8406-71Y (P7-PS701)\nhbnim1     | IBM,9133-55A (P5-55A)\n\\.\n\nTRUNCATE TABLE pgrst_reserved_chars CASCADE;\nCOPY pgrst_reserved_chars (\"*id*\", \":arr->ow::cast\", \"(inside,parens)\", \"a.dotted.column\", \"  col  w  space  \") FROM STDIN CSV DELIMITER '|';\n1 | arrow-1 | parens-1 | dotted-1 | space-1\n2 | arrow-2 | parens-2 | dotted-2 | space-2\n3 | arrow-3 | parens-3 | dotted-3 | space-3\n\\.\n\nTRUNCATE TABLE web_content CASCADE;\nINSERT INTO web_content VALUES (5, 'wat', null);\nINSERT INTO web_content VALUES (0, 'tardis', 5);\nINSERT INTO web_content VALUES (1, 'fezz', 0);\nINSERT INTO web_content VALUES (2, 'foo', 0);\nINSERT INTO web_content VALUES (3, 'bar', 0);\nINSERT INTO web_content VALUES (4, 'wut', 1);\n\nTRUNCATE TABLE app_users CASCADE;\nINSERT INTO app_users (id, email, \"password\") VALUES (1, 'test@123.com','pass');\nINSERT INTO app_users (id, email, \"password\") VALUES (2, 'abc@123.com','pass');\nINSERT INTO app_users (id, email, \"password\") VALUES (3, 'def@123.com','pass');\n\nTRUNCATE TABLE private.pages CASCADE;\nINSERT INTO private.pages VALUES (1, 'http://postgrest.org/en/v6.0/api.html');\nINSERT INTO private.pages VALUES (2, 'http://postgrest.org/en/v6.0/admin.html');\n\nTRUNCATE TABLE private.referrals CASCADE;\nINSERT INTO private.referrals VALUES ('github.com', 1);\nINSERT INTO private.referrals VALUES ('hub.docker.com', 2);\n\nTRUNCATE TABLE big_projects CASCADE;\nINSERT INTO big_projects (big_project_id, name)\nVALUES (1, 'big project 1'),\n       (2, 'big project 2');\n\nTRUNCATE TABLE sites CASCADE;\nINSERT INTO sites (site_id, name, main_project_id)\nVALUES (1, 'site 1', 1),\n       (2, 'site 2', null),\n       (3, 'site 3', 2),\n       (4, 'site 4', null);\n\nTRUNCATE TABLE jobs CASCADE;\nINSERT INTO jobs (job_id, name, site_id, big_project_id)\nVALUES ('bc5d5362-b881-438f-b9f5-7417e08704ed', 'job 1-1', 1, 1),\n       ('3bd52697-033b-4edd-8a28-46a9c04b7c1e', 'job 2-1', 2, 1),\n       ('e6e67e4e-19b1-11e9-ab14-d663bd873d93', 'job 2-2', 2, 2);\n\nTRUNCATE TABLE departments CASCADE;\nTRUNCATE TABLE agents CASCADE;\nINSERT INTO agents (id, name)\nVALUES (1, 'agent 1'),\n       (2, 'agent 2'),\n       (3, 'agent 3'),\n       (4, 'agent 4');\n\nINSERT INTO departments (id, name, head_id)\nVALUES (1, 'dep 1', 1),\n       (2, 'dep 3', 3);\n\nUPDATE agents SET department_id = 1 WHERE id in (1, 2);\nUPDATE agents SET department_id = 2 WHERE id in (3, 4);\n\nTRUNCATE TABLE schedules CASCADE;\nINSERT INTO schedules VALUES(1, 'morning', '06:00:00', '11:59:00');\nINSERT INTO schedules VALUES(2, 'afternoon', '12:00:00', '17:59:00');\nINSERT INTO schedules VALUES(3, 'night', '18:00:00', '23:59:00');\nINSERT INTO schedules VALUES(4, 'early morning', '00:00:00', '05:59:00');\n\nTRUNCATE TABLE activities CASCADE;\nINSERT INTO activities(id, schedule_id, car_id)    VALUES(1, 1, 'CAR-349');\nINSERT INTO activities(id, schedule_id, camera_id) VALUES(2, 3, 'CAM-123');\n\nTRUNCATE TABLE unit_workdays CASCADE;\nINSERT INTO unit_workdays VALUES(1, '2019-12-02', 1, 1, 2, 3);\n\nTRUNCATE TABLE v1.parents CASCADE;\nINSERT INTO v1.parents VALUES(1, 'parent v1-1'), (2, 'parent v1-2');\n\nTRUNCATE TABLE v1.children CASCADE;\nINSERT INTO v1.children VALUES(1, 'child v1-1', 1), (2, 'child v1-2', 2);\n\nTRUNCATE TABLE v2.parents CASCADE;\nINSERT INTO v2.parents VALUES(3, 'parent v2-3'), (4, 'parent v2-4');\n\nTRUNCATE TABLE v2.children CASCADE;\nINSERT INTO v2.children VALUES(1, 'child v2-3', 3);\n\nTRUNCATE TABLE v2.another_table CASCADE;\nINSERT INTO v2.another_table VALUES(5, 'value 5'), (6, 'value 6');\n\nTRUNCATE TABLE private.stuff CASCADE;\nINSERT INTO private.stuff (id, name) VALUES (1, 'stuff 1');\n\nTRUNCATE TABLE private.screens CASCADE;\nINSERT INTO private.screens(name) VALUES ('banana'), ('helicopter'), ('formula 1 banana');\n\nINSERT INTO private.labels(name) VALUES ('vehicles'), ('fruit');\n\nINSERT INTO private.label_screen(label_id, screen_id) VALUES\n    ((SELECT id FROM labels WHERE name='vehicles'), (SELECT id FROM screens WHERE name='helicopter')),\n    ((SELECT id FROM labels WHERE name='vehicles'), (SELECT id FROM screens WHERE name='formula 1 banana')),\n    ((SELECT id FROM labels WHERE name='fruit'), (SELECT id FROM screens WHERE name='banana')),\n    ((SELECT id FROM labels WHERE name='fruit'), (SELECT id FROM screens WHERE name='formula 1 banana'));\n\nTRUNCATE TABLE private.actors CASCADE;\nINSERT INTO private.actors (id, name) VALUES (1,'john'), (2,'mary');\n\nTRUNCATE TABLE private.films CASCADE;\nINSERT INTO private.films (id, title) VALUES (12,'douze commandements'), (2001,'odyssée de l''espace');\n\nTRUNCATE TABLE private.personnages CASCADE;\nINSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute');\n\nINSERT INTO test.car_models(name, year) VALUES ('DeLorean',1981);\nINSERT INTO test.car_models(name, year) VALUES ('F310-B',1997);\nINSERT INTO test.car_models(name, year) VALUES ('Veneno',2013);\nINSERT INTO test.car_models(name, year) VALUES ('Murcielago',2001);\n\nINSERT INTO test.car_brands(name) VALUES ('DMC');\nINSERT INTO test.car_brands(name) VALUES ('Ferrari');\nINSERT INTO test.car_brands(name) VALUES ('Lamborghini');\n\nUPDATE test.car_models SET car_brand_name = 'DMC' WHERE name = 'DeLorean';\nUPDATE test.car_models SET car_brand_name = 'Ferrari' WHERE name = 'F310-B';\nUPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Veneno';\nUPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Murcielago';\n\nINSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-14',7,'DeLorean',1981);\nINSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-15',9,'DeLorean',1981);\nINSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-11',1,'Murcielago',2001);\nINSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-12',3,'Murcielago',2001);\n\nINSERT INTO test.car_racers(name) VALUES ('Alain Prost');\nINSERT INTO test.car_racers(name, car_model_name, car_model_year) VALUES ('Michael Schumacher', 'F310-B', 1997);\n\nINSERT INTO test.car_dealers(name,city) VALUES ('Springfield Cars S.A.','Springfield');\nINSERT INTO test.car_dealers(name,city) VALUES ('The Best Deals S.A.','Franklin');\n\nINSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('DeLorean',1981,'Springfield Cars S.A.','Springfield',15);\nINSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('Murcielago',2001,'The Best Deals S.A.','Franklin',2);\n\nTRUNCATE TABLE test.products CASCADE;\nINSERT INTO test.products (id, name) VALUES (1,'product-1'), (2,'product-2'), (3,'product-3');\n\nTRUNCATE TABLE test.suppliers CASCADE;\nINSERT INTO test.suppliers (id, name) VALUES (1,'supplier-1'), (2,'supplier-2'), (3, 'supplier-3');\n\nTRUNCATE TABLE test.products_suppliers CASCADE;\nINSERT INTO test.products_suppliers (product_id, supplier_id) VALUES (1,1), (1,2), (2,1), (2,3);\n\nTRUNCATE TABLE test.trade_unions CASCADE;\nINSERT INTO test.trade_unions (id, name) VALUES (1,'union-1'), (2,'union-2'), (3, 'union-3'), (4, 'union-4');\n\nTRUNCATE TABLE test.suppliers_trade_unions CASCADE;\nINSERT INTO test.suppliers_trade_unions (supplier_id, trade_union_id) VALUES (1,1), (1,2), (2,3), (2,4);\n\nTRUNCATE TABLE test.client CASCADE;\nINSERT INTO test.client (id,name) values (1,'Walmart'),(2,'Target'),(3,'Big Lots');\n\nTRUNCATE TABLE test.contact CASCADE;\nINSERT INTO test.contact (id,name, clientid) values (1,'Wally Walton',1),(2,'Wilma Wellers',1),(3,'Tabby Targo',2),(4,'Bobby Bots',3),(5,'Bonnie Bits',3),(6,'Billy Boats',3) returning *;\n\nTRUNCATE TABLE test.clientinfo CASCADE;\nINSERT INTO test.clientinfo (id,clientid, other) values (1,1,'123 Main St'),(2,2,'456 South 3rd St'),(3,3,'789 Palm Tree Ln');\n\nTRUNCATE TABLE test.chores CASCADE;\nINSERT INTO test.chores (id, name, done) values (1, 'take out the garbage', true), (2, 'do the laundry', false), (3, 'wash the dishes', null);\n\nTRUNCATE TABLE test.fav_numbers CASCADE;\nINSERT INTO test.fav_numbers VALUES (ROW(0.5, 0.5), 'A'),  (ROW(0.6, 0.6), 'B');\n\nTRUNCATE TABLE test.arrays CASCADE;\nINSERT INTO test.arrays VALUES (0, '{1,2,3}', '{{1,2,3},{4,5,6},{7,8,9}}'), (1, '{11,12,13}', '{{11,12,13},{14,15,16},{17,18,19}}');\n\nTRUNCATE TABLE test.xmltest CASCADE;\nINSERT INTO test.xmltest VALUES\n(1, '<myxml>foo</myxml>'),\n(2, 'bar'),\n(3, '<foobar><baz/></foobar>');\n\nTRUNCATE TABLE test.oid_test CASCADE;\nINSERT INTO oid_test(id, oid_col, oid_array_col) VALUES (1, '12345', '{1,2,3,4,5}'::oid[]);\n\nTRUNCATE TABLE private.internal_job CASCADE;\nINSERT INTO private.internal_job (id, parent_id) VALUES (1, null);\nINSERT INTO private.internal_job (id, parent_id) VALUES (2, 1);\n\nTRUNCATE TABLE test.test CASCADE;\nINSERT INTO test.test (id, parent_id) VALUES (1, null), (2, 1);\n\nTRUNCATE TABLE shops CASCADE;\nINSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)');\nINSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)');\nINSERT INTO shops(id, address, shop_geom) VALUES(3, '605 W Kendall St', 'SRID=4326;POINT(-71.081924 42.36437)');\n\nTRUNCATE TABLE shop_bles CASCADE;\nINSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(1, 'Beacon-1', 'SRID=4326;POINT(-71.10044 42.373695)', 1,\n  extensions.ST_GeomFromGeoJSON('{\"type\": \"Polygon\", \"coordinates\": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ]}'));\nINSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(2, 'Beacon-2', 'SRID=4326;POINT(-71.10044 42.373695)', 1,\n  extensions.ST_GeomFromGeoJSON('{\"type\": \"Polygon\", \"coordinates\": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ]}'));\n\nTRUNCATE TABLE \"SPECIAL \"\"@/\\#~_-\".languages CASCADE;\nINSERT INTO \"SPECIAL \"\"@/\\#~_-\".languages (id, name) VALUES (1, 'English'), (2, 'Spanish');\nTRUNCATE TABLE \"SPECIAL \"\"@/\\#~_-\".names CASCADE;\nINSERT INTO \"SPECIAL \"\"@/\\#~_-\".names (id, name, language_id) VALUES (1, 'John', 1), (2, 'Mary', 1), (3, 'José', 2);\n\nTRUNCATE TABLE do$llar$s CASCADE;\nINSERT INTO do$llar$s (a$num$) VALUES (100), (200), (300);\n\nTRUNCATE TABLE safe_update_items CASCADE;\nINSERT INTO safe_update_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);\nTRUNCATE TABLE safe_delete_items CASCADE;\nINSERT INTO safe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);\nTRUNCATE TABLE unsafe_update_items CASCADE;\nINSERT INTO unsafe_update_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);\nTRUNCATE TABLE unsafe_delete_items CASCADE;\nINSERT INTO unsafe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);\n\nTRUNCATE TABLE designers CASCADE;\nINSERT INTO designers(id, name) VALUES (1, 'Sid Meier'), (2, 'Hironobu Sakaguchi');\n\nTRUNCATE TABLE videogames CASCADE;\nINSERT INTO videogames(id, name, designer_id) VALUES (1, 'Civilization I', 1), (2, 'Civilization II', 1), (3, 'Final Fantasy I', 2), (4, 'Final Fantasy II', 2);\n\nTRUNCATE TABLE students CASCADE;\nINSERT INTO students(id, code, name) VALUES (1, '0001', 'John Doe'), (2, '0002', 'Jane Doe');\n\nTRUNCATE TABLE students_info CASCADE;\nINSERT INTO students_info(id, code, address) VALUES (1, '0001', 'Street 1'), (2, '0002', 'Street 2');\n\nTRUNCATE TABLE country CASCADE;\nINSERT INTO country(id, name) VALUES (1, 'Afghanistan'), (2, 'Algeria');\n\nTRUNCATE TABLE capital CASCADE;\nINSERT INTO capital(id, name, country_id) VALUES (1, 'Kabul', 1), (2, 'Algiers', 2);\n\nTRUNCATE TABLE trash CASCADE;\nINSERT INTO trash(id) VALUES (1), (2), (3);\n\nTRUNCATE TABLE trash_details CASCADE;\nINSERT INTO trash_details(id,jsonb_col) VALUES (1,'{\"key\": 10}'), (2,'{\"key\": 6}'), (3,'{\"key\": 8}');\n\nTRUNCATE TABLE posters CASCADE;\nINSERT INTO posters(id,name) VALUES (1,'Mark'), (2,'Elon'), (3,'Bill'), (4,'Jeff');\n\nTRUNCATE TABLE subscriptions CASCADE;\nINSERT INTO subscriptions(subscriber,subscribed) VALUES (3,1), (4,1), (1,2);\n\nTRUNCATE TABLE datarep_todos CASCADE;\nINSERT INTO datarep_todos VALUES (1, 'Report', 0, '2018-01-02', '\\x89504e470d0a1a0a0000000d4948445200000001000000010100000000376ef924000000001049444154789c62600100000000ffff03000000060005057bfabd400000000049454e44ae426082', '2017-12-14 01:02:30', 12.50); -- smallest possible PNG\nINSERT INTO datarep_todos VALUES (2, 'Essay', 256, '2018-01-03', NULL, '2017-12-14 01:02:30', 100000000000000.13); -- a number which can't be represented by a 64-bit float\nINSERT INTO datarep_todos VALUES (3, 'Algebra', 123456, '2018-01-01 14:12:34.123456');\nINSERT INTO datarep_todos VALUES (4, 'Opus Magnum', NULL, NULL);\n\nTRUNCATE TABLE datarep_next_two_todos CASCADE;\nINSERT INTO datarep_next_two_todos VALUES (1, 2, 3, 'school related');\nINSERT INTO datarep_next_two_todos VALUES (2, 1, 3, 'do these first');\n\nTRUNCATE TABLE bitchar_with_length CASCADE;\nINSERT INTO bitchar_with_length(bit, char) VALUES ('00000', 'aaaaa');\nINSERT INTO bitchar_with_length(bit, char) VALUES ('11111', 'bbbbb');\n\nTRUNCATE TABLE table_a CASCADE;\nINSERT INTO table_a(id, name) VALUES (1, 'Not null 1'), (2, null), (3, 'Not null 2');\nTRUNCATE TABLE table_b CASCADE;\nINSERT INTO table_b(table_a_id, name) VALUES (1, 'Test 1'), (2, 'Test 2'), (null, 'Test 3');\n\nTRUNCATE TABLE lines CASCADE;\ninsert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry);\n\nTRUNCATE TABLE timestamps CASCADE;\nINSERT INTO timestamps VALUES ('2023-10-18 12:37:59.611000+0000');\nINSERT INTO timestamps VALUES ('2023-10-18 14:37:59.611000+0000');\nINSERT INTO timestamps VALUES ('2023-10-18 16:37:59.611000+0000');\n\nTRUNCATE TABLE project_invoices CASCADE;\nINSERT INTO project_invoices VALUES (1, 100, 1);\nINSERT INTO project_invoices VALUES (2, 200, 1);\nINSERT INTO project_invoices VALUES (3, 500, 2);\nINSERT INTO project_invoices VALUES (4, 700, 2);\nINSERT INTO project_invoices VALUES (5, 1200, 3);\nINSERT INTO project_invoices VALUES (6, 2000, 3);\nINSERT INTO project_invoices VALUES (7, 100, 4);\nINSERT INTO project_invoices VALUES (8, 4000, 4);\n\nTRUNCATE TABLE budget_categories CASCADE;\nINSERT INTO budget_categories VALUES (1, 'Beanie Babies', 'Brian Smith', 1000.31);\nINSERT INTO budget_categories VALUES (2, 'DVDs', 'Jane Clarkson', 2000.12);\nINSERT INTO budget_categories VALUES (3, 'Pizza', 'Brian Smith', 1000.11);\nINSERT INTO budget_categories VALUES (4, 'Opera Tickets', 'Jane Clarkson', 7000.41);\nINSERT INTO budget_categories VALUES (5, 'Nuclear Fusion Research', 'Sally Hughes', 500.23);\nINSERT INTO budget_categories VALUES (6, 'T-5hirts', 'Dana de Groot', 500.33);\n\nTRUNCATE TABLE budget_expenses CASCADE;\nINSERT INTO budget_expenses VALUES (1, 200.26, 1);\nINSERT INTO budget_expenses VALUES (2, 400.26, 3);\nINSERT INTO budget_expenses VALUES (3, 100.22, 4);\nINSERT INTO budget_expenses VALUES (5, 900.27, 5);\n\nTRUNCATE TABLE factories CASCADE;\nINSERT INTO factories VALUES (1, 'Factory A');\nINSERT INTO factories VALUES (2, 'Factory B');\nINSERT INTO factories VALUES (3, 'Factory C');\nINSERT INTO factories VALUES (4, 'Factory D');\n\nTRUNCATE TABLE process_categories CASCADE;\nINSERT INTO process_categories VALUES (1, 'Batch');\nINSERT INTO process_categories VALUES (2, 'Mass');\n\nTRUNCATE TABLE processes CASCADE;\nINSERT INTO processes VALUES (1, 'Process A1', 1, 1);\nINSERT INTO processes VALUES (2, 'Process A2', 1, 2);\nINSERT INTO processes VALUES (3, 'Process B1', 2, 1);\nINSERT INTO processes VALUES (4, 'Process B2', 2, 1);\nINSERT INTO processes VALUES (5, 'Process C1', 3, 2);\nINSERT INTO processes VALUES (6, 'Process C2', 3, 2);\nINSERT INTO processes VALUES (7, 'Process XX', 3, 2);\nINSERT INTO processes VALUES (8, 'Process YY', 3, 2);\n\nTRUNCATE TABLE process_costs CASCADE;\nINSERT INTO process_costs VALUES (1, 150.00);\nINSERT INTO process_costs VALUES (2, 200.00);\nINSERT INTO process_costs VALUES (3, 180.00);\nINSERT INTO process_costs VALUES (4, 70.00);\nINSERT INTO process_costs VALUES (5, 40.00);\nINSERT INTO process_costs VALUES (6, 70.00);\nINSERT INTO process_costs VALUES (8, 40.00);\n\nTRUNCATE TABLE supervisors CASCADE;\nINSERT INTO supervisors VALUES (1, 'Mary');\nINSERT INTO supervisors VALUES (2, 'John');\nINSERT INTO supervisors VALUES (3, 'Peter');\nINSERT INTO supervisors VALUES (4, 'Sarah');\nINSERT INTO supervisors VALUES (5, 'Jane');\n\nTRUNCATE TABLE process_supervisor CASCADE;\nINSERT INTO process_supervisor VALUES (1, 1);\nINSERT INTO process_supervisor VALUES (2, 2);\nINSERT INTO process_supervisor VALUES (3, 3);\nINSERT INTO process_supervisor VALUES (3, 4);\nINSERT INTO process_supervisor VALUES (4, 1);\nINSERT INTO process_supervisor VALUES (4, 2);\nINSERT INTO process_supervisor VALUES (5, 3);\nINSERT INTO process_supervisor VALUES (6, 3);\n\nTRUNCATE TABLE operators CASCADE;\nINSERT INTO operators VALUES (1, 'Anne', '{\"id\": \"543210\", \"afk\": true}');\nINSERT INTO operators VALUES (2, 'Louis', '{\"id\": \"012345\"}');\nINSERT INTO operators VALUES (3, 'Jeff', '{\"id\": \"666666\", \"afk\": true}');\nINSERT INTO operators VALUES (4, 'Liz', '{\"id\": \"999999\"}');\nINSERT INTO operators VALUES (5, 'Alfred', '{\"id\": \"000000\"}');\n\nTRUNCATE TABLE process_operator CASCADE;\nINSERT INTO process_operator VALUES (1,1);\nINSERT INTO process_operator VALUES (1,2);\nINSERT INTO process_operator VALUES (2,1);\nINSERT INTO process_operator VALUES (2,2);\nINSERT INTO process_operator VALUES (2,3);\nINSERT INTO process_operator VALUES (3,3);\nINSERT INTO process_operator VALUES (4,1);\nINSERT INTO process_operator VALUES (4,3);\nINSERT INTO process_operator VALUES (6,3);\nINSERT INTO process_operator VALUES (6,5);\nINSERT INTO process_operator VALUES (7,5);\n\nTRUNCATE TABLE factory_buildings CASCADE;\nINSERT INTO factory_buildings VALUES (1, 'A001', 150, 'A', 1, '{\"ins\": \"2024C\", \"pending\": true}');\nINSERT INTO factory_buildings VALUES (2, 'A002', 200, 'A', 1, '{\"ins\": \"2025A\", \"pending\": true}');\nINSERT INTO factory_buildings VALUES (3, 'B001', 50, 'B', 2, '{\"ins\": \"2025A\", \"pending\": true}');\nINSERT INTO factory_buildings VALUES (4, 'B002', 120, 'C', 2, '{\"ins\": \"2023A\"}');\nINSERT INTO factory_buildings VALUES (5, 'C001', 240, 'B', 3, '{\"ins\": \"2022B\"}' );\nINSERT INTO factory_buildings VALUES (6, 'D001', 310, 'A', 4, '{\"ins\": \"2024C\", \"pending\": true}');\n\nTRUNCATE TABLE surr_serial_upsert CASCADE;\nINSERT INTO surr_serial_upsert(name, extra) VALUES ('value', 'existing value');\n\nTRUNCATE TABLE surr_gen_default_upsert CASCADE;\nINSERT INTO surr_gen_default_upsert(name, extra) VALUES ('value', 'existing value');\n\nTRUNCATE TABLE \"Surr_Gen_Default_Upsert\" CASCADE;\nINSERT INTO \"Surr_Gen_Default_Upsert\"(name, extra) VALUES ('value cs', 'existing value cs');\n\nTRUNCATE TABLE tsearch_to_tsvector CASCADE;\nINSERT INTO tsearch_to_tsvector(text_search) VALUES ('It''s kind of fun to do the impossible');\nINSERT INTO tsearch_to_tsvector(text_search) VALUES ('But also fun to do what is possible');\nINSERT INTO tsearch_to_tsvector(text_search) VALUES ('Fat cats ate rats');\nINSERT INTO tsearch_to_tsvector(text_search) VALUES ('C''est un peu amusant de faire l''impossible');\nINSERT INTO tsearch_to_tsvector(text_search) VALUES ('Es ist eine Art Spaß, das Unmögliche zu machen');\n\nUPDATE tsearch_to_tsvector SET jsonb_search = jsonb_build_object('text_search', text_search);\nUPDATE tsearch_to_tsvector SET text_search_domain = to_tsvector('simple', text_search);\nUPDATE tsearch_to_tsvector SET text_search_rec_domain = to_tsvector('simple', text_search);\n\nTRUNCATE TABLE artists CASCADE;\nINSERT INTO artists\nVALUES (1, 'duster'), (2, 'black country, new road'), (3, 'bjork');\n\nTRUNCATE TABLE albums CASCADE;\nINSERT INTO albums\nVALUES (1, 'stratosphere', 1),\n       (2, 'ants from up above',2),\n       (3, 'vespertine',3),\n       (4, 'contemporary movement', 1);\n"
  },
  {
    "path": "test/spec/fixtures/database.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS pgcrypto;\nALTER DATABASE :DBNAME SET request.jwt.claim.id = '-1';\n\nset client_min_messages to warning;\nDROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions, v1, v2 CASCADE;\nDROP TYPE IF EXISTS jwt_token CASCADE;\n"
  },
  {
    "path": "test/spec/fixtures/draft04.json",
    "content": "{\n    \"id\": \"draft04.json\",\n    \"$schema\": \"draft04.json\",\n    \"description\": \"Core schema meta-schema\",\n    \"definitions\": {\n        \"schemaArray\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": { \"$ref\": \"#\" }\n        },\n        \"positiveInteger\": {\n            \"type\": \"integer\",\n            \"minimum\": 0\n        },\n        \"positiveIntegerDefault0\": {\n            \"allOf\": [ { \"$ref\": \"#/definitions/positiveInteger\" }, { \"default\": 0 } ]\n        },\n        \"simpleTypes\": {\n            \"enum\": [ \"array\", \"boolean\", \"integer\", \"null\", \"number\", \"object\", \"string\" ]\n        },\n        \"stringArray\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" },\n            \"minItems\": 1,\n            \"uniqueItems\": true\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"id\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n        },\n        \"$schema\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n        },\n        \"title\": {\n            \"type\": \"string\"\n        },\n        \"description\": {\n            \"type\": \"string\"\n        },\n        \"default\": {},\n        \"multipleOf\": {\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"exclusiveMinimum\": true\n        },\n        \"maximum\": {\n            \"type\": \"number\"\n        },\n        \"exclusiveMaximum\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"minimum\": {\n            \"type\": \"number\"\n        },\n        \"exclusiveMinimum\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"maxLength\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minLength\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"pattern\": {\n            \"type\": \"string\",\n            \"format\": \"regex\"\n        },\n        \"additionalItems\": {\n            \"anyOf\": [\n                { \"type\": \"boolean\" },\n                { \"$ref\": \"#\" }\n            ],\n            \"default\": {}\n        },\n        \"items\": {\n            \"anyOf\": [\n                { \"$ref\": \"#\" },\n                { \"$ref\": \"#/definitions/schemaArray\" }\n            ],\n            \"default\": {}\n        },\n        \"maxItems\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minItems\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"uniqueItems\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"maxProperties\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minProperties\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"required\": { \"$ref\": \"#/definitions/stringArray\" },\n        \"additionalProperties\": {\n            \"anyOf\": [\n                { \"type\": \"boolean\" },\n                { \"$ref\": \"#\" }\n            ],\n            \"default\": {}\n        },\n        \"definitions\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"properties\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"patternProperties\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"dependencies\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n                \"anyOf\": [\n                    { \"$ref\": \"#\" },\n                    { \"$ref\": \"#/definitions/stringArray\" }\n                ]\n            }\n        },\n        \"enum\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"uniqueItems\": true\n        },\n        \"type\": {\n            \"anyOf\": [\n                { \"$ref\": \"#/definitions/simpleTypes\" },\n                {\n                    \"type\": \"array\",\n                    \"items\": { \"$ref\": \"#/definitions/simpleTypes\" },\n                    \"minItems\": 1,\n                    \"uniqueItems\": true\n                }\n            ]\n        },\n        \"format\": { \"type\": \"string\" },\n        \"allOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"anyOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"oneOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"not\": { \"$ref\": \"#\" }\n    },\n    \"dependencies\": {\n        \"exclusiveMaximum\": [ \"maximum\" ],\n        \"exclusiveMinimum\": [ \"minimum\" ]\n    },\n    \"default\": {}\n}\n"
  },
  {
    "path": "test/spec/fixtures/jsonschema.sql",
    "content": "-- from gavinwahl/postgres-json-schema commit 5a257e19a1569a77b82e9182b0b7d9fc8b6f6382\n\n/*\nCopyright (c) 2016, Gavin Wahl\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written agreement is\nhereby granted, provided that the above copyright notice and this paragraph and\nthe following two paragraphs appear in all copies.\n\nIN NO EVENT SHALL GAVIN WAHL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,\nSPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING\nOUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF GAVIN WAHL HAS\nBEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nGAVIN WAHL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN \"AS IS\" BASIS, AND GAVIN WAHL\nHAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR\nMODIFICATIONS.\n*/\n\nCREATE OR REPLACE FUNCTION public._validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$\nBEGIN\n  IF type = 'integer' THEN\n    IF jsonb_typeof(data) != 'number' THEN\n      RETURN false;\n    END IF;\n    IF trunc(data::text::numeric) != data::text::numeric THEN\n      RETURN false;\n    END IF;\n  ELSE\n    IF type != jsonb_typeof(data) THEN\n      RETURN false;\n    END IF;\n  END IF;\n  RETURN true;\nEND;\n$f$ LANGUAGE 'plpgsql' IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION test.validate_json_schema(schema jsonb, data jsonb, root_schema jsonb DEFAULT NULL) RETURNS boolean AS $f$\nDECLARE\n  prop text;\n  item jsonb;\n  path text[];\n  types text[];\n  pattern text;\n  props text[];\nBEGIN\n  IF root_schema IS NULL THEN\n    root_schema = schema;\n  END IF;\n\n  IF schema ? 'type' THEN\n    IF jsonb_typeof(schema->'type') = 'array' THEN\n      types = ARRAY(SELECT jsonb_array_elements_text(schema->'type'));\n    ELSE\n      types = ARRAY[schema->>'type'];\n    END IF;\n    IF (SELECT NOT bool_or(public._validate_json_schema_type(type, data)) FROM unnest(types) type) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'properties' THEN\n    FOR prop IN SELECT jsonb_object_keys(schema->'properties') LOOP\n      IF data ? prop AND NOT validate_json_schema(schema->'properties'->prop, data->prop, root_schema) THEN\n        RETURN false;\n      END IF;\n    END LOOP;\n  END IF;\n\n  IF schema ? 'required' AND jsonb_typeof(data) = 'object' THEN\n    IF NOT ARRAY(SELECT jsonb_object_keys(data)) @>\n           ARRAY(SELECT jsonb_array_elements_text(schema->'required')) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'items' AND jsonb_typeof(data) = 'array' THEN\n    IF jsonb_typeof(schema->'items') = 'object' THEN\n      FOR item IN SELECT jsonb_array_elements(data) LOOP\n        IF NOT validate_json_schema(schema->'items', item, root_schema) THEN\n          RETURN false;\n        END IF;\n      END LOOP;\n    ELSE\n      IF NOT (\n        SELECT bool_and(i > jsonb_array_length(schema->'items') OR validate_json_schema(schema->'items'->(i::int - 1), elem, root_schema))\n        FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)\n      ) THEN\n        RETURN false;\n      END IF;\n    END IF;\n  END IF;\n\n  IF jsonb_typeof(schema->'additionalItems') = 'boolean' and NOT (schema->'additionalItems')::text::boolean AND jsonb_typeof(schema->'items') = 'array' THEN\n    IF jsonb_array_length(data) > jsonb_array_length(schema->'items') THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF jsonb_typeof(schema->'additionalItems') = 'object' THEN\n    IF NOT (\n        SELECT bool_and(validate_json_schema(schema->'additionalItems', elem, root_schema))\n        FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)\n        WHERE i > jsonb_array_length(schema->'items')\n      ) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'minimum' AND jsonb_typeof(data) = 'number' THEN\n    IF data::text::numeric < (schema->>'minimum')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'maximum' AND jsonb_typeof(data) = 'number' THEN\n    IF data::text::numeric > (schema->>'maximum')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF COALESCE((schema->'exclusiveMinimum')::text::bool, FALSE) THEN\n    IF data::text::numeric = (schema->>'minimum')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF COALESCE((schema->'exclusiveMaximum')::text::bool, FALSE) THEN\n    IF data::text::numeric = (schema->>'maximum')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'anyOf' THEN\n    IF NOT (SELECT bool_or(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'anyOf') sub_schema) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'allOf' THEN\n    IF NOT (SELECT bool_and(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'allOf') sub_schema) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'oneOf' THEN\n    IF 1 != (SELECT COUNT(*) FROM jsonb_array_elements(schema->'oneOf') sub_schema WHERE validate_json_schema(sub_schema, data, root_schema)) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF COALESCE((schema->'uniqueItems')::text::boolean, false) THEN\n    IF (SELECT COUNT(*) FROM jsonb_array_elements(data)) != (SELECT count(DISTINCT val) FROM jsonb_array_elements(data) val) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'additionalProperties' AND jsonb_typeof(data) = 'object' THEN\n    props := ARRAY(\n      SELECT key\n      FROM jsonb_object_keys(data) key\n      WHERE key NOT IN (SELECT jsonb_object_keys(schema->'properties'))\n        AND NOT EXISTS (SELECT * FROM jsonb_object_keys(schema->'patternProperties') pat WHERE key ~ pat)\n    );\n    IF jsonb_typeof(schema->'additionalProperties') = 'boolean' THEN\n      IF NOT (schema->'additionalProperties')::text::boolean AND jsonb_typeof(data) = 'object' AND NOT props <@ ARRAY(SELECT jsonb_object_keys(schema->'properties')) THEN\n        RETURN false;\n      END IF;\n    ELSEIF NOT (\n      SELECT bool_and(validate_json_schema(schema->'additionalProperties', data->key, root_schema))\n      FROM unnest(props) key\n    ) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? '$ref' THEN\n    path := ARRAY(\n      SELECT regexp_replace(regexp_replace(path_part, '~1', '/'), '~0', '~')\n      FROM UNNEST(regexp_split_to_array(schema->>'$ref', '/')) path_part\n    );\n    -- ASSERT path[1] = '#', 'only refs anchored at the root are supported';\n    IF NOT validate_json_schema(root_schema #> path[2:array_length(path, 1)], data, root_schema) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'enum' THEN\n    IF NOT EXISTS (SELECT * FROM jsonb_array_elements(schema->'enum') val WHERE val = data) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'minLength' AND jsonb_typeof(data) = 'string' THEN\n    IF char_length(data #>> '{}') < (schema->>'minLength')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'maxLength' AND jsonb_typeof(data) = 'string' THEN\n    IF char_length(data #>> '{}') > (schema->>'maxLength')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'not' THEN\n    IF validate_json_schema(schema->'not', data, root_schema) THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'maxProperties' AND jsonb_typeof(data) = 'object' THEN\n    IF (SELECT count(*) FROM jsonb_object_keys(data)) > (schema->>'maxProperties')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'minProperties' AND jsonb_typeof(data) = 'object' THEN\n    IF (SELECT count(*) FROM jsonb_object_keys(data)) < (schema->>'minProperties')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'maxItems' AND jsonb_typeof(data) = 'array' THEN\n    IF (SELECT count(*) FROM jsonb_array_elements(data)) > (schema->>'maxItems')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'minItems' AND jsonb_typeof(data) = 'array' THEN\n    IF (SELECT count(*) FROM jsonb_array_elements(data)) < (schema->>'minItems')::numeric THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'dependencies' THEN\n    FOR prop IN SELECT jsonb_object_keys(schema->'dependencies') LOOP\n      IF data ? prop THEN\n        IF jsonb_typeof(schema->'dependencies'->prop) = 'array' THEN\n          IF NOT (SELECT bool_and(data ? dep) FROM jsonb_array_elements_text(schema->'dependencies'->prop) dep) THEN\n            RETURN false;\n          END IF;\n        ELSE\n          IF NOT validate_json_schema(schema->'dependencies'->prop, data, root_schema) THEN\n            RETURN false;\n          END IF;\n        END IF;\n      END IF;\n    END LOOP;\n  END IF;\n\n  IF schema ? 'pattern' AND jsonb_typeof(data) = 'string' THEN\n    IF (data #>> '{}') !~ (schema->>'pattern') THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  IF schema ? 'patternProperties' AND jsonb_typeof(data) = 'object' THEN\n    FOR prop IN SELECT jsonb_object_keys(data) LOOP\n      FOR pattern IN SELECT jsonb_object_keys(schema->'patternProperties') LOOP\n        RAISE NOTICE 'prop %s, pattern %, schema %', prop, pattern, schema->'patternProperties'->pattern;\n        IF prop ~ pattern AND NOT validate_json_schema(schema->'patternProperties'->pattern, data->prop, root_schema) THEN\n          RETURN false;\n        END IF;\n      END LOOP;\n    END LOOP;\n  END IF;\n\n  IF schema ? 'multipleOf' AND jsonb_typeof(data) = 'number' THEN\n    IF data::text::numeric % (schema->>'multipleOf')::numeric != 0 THEN\n      RETURN false;\n    END IF;\n  END IF;\n\n  RETURN true;\nEND;\n$f$ LANGUAGE 'plpgsql' IMMUTABLE;\n"
  },
  {
    "path": "test/spec/fixtures/jwt.sql",
    "content": "-- From michelp/pgjwt commit c02bbd3\nBEGIN;\n\nset search_path to public;\n\nset client_min_messages to warning;\nDROP SCHEMA IF EXISTS jwt CASCADE;\nCREATE SCHEMA jwt;\n\nCREATE OR REPLACE FUNCTION jwt.url_encode(data bytea) RETURNS text LANGUAGE sql AS $$\n    SELECT translate(encode(data, 'base64'), E'+/=\\n', '-_');\n$$;\n\n\nCREATE OR REPLACE FUNCTION jwt.url_decode(data text) RETURNS bytea LANGUAGE sql AS $$\nWITH t AS (SELECT translate(data, '-_', '+/')),\n     rem AS (SELECT length((SELECT * FROM t)) % 4) -- compute padding size\n    SELECT decode(\n        (SELECT * FROM t) ||\n        CASE WHEN (SELECT * FROM rem) > 0\n           THEN repeat('=', (4 - (SELECT * FROM rem)))\n           ELSE '' END,\n    'base64');\n$$;\n\n\nCREATE OR REPLACE FUNCTION jwt.algorithm_sign(signables text, secret text, algorithm text)\nRETURNS text LANGUAGE sql AS $$\nWITH\n  alg AS (\n    SELECT CASE\n      WHEN algorithm = 'HS256' THEN 'sha256'\n      WHEN algorithm = 'HS384' THEN 'sha384'\n      WHEN algorithm = 'HS512' THEN 'sha512'\n      ELSE '' END)  -- hmac throws error\nSELECT jwt.url_encode(public.hmac(signables, secret, (select * FROM alg)));\n$$;\n\n\nCREATE OR REPLACE FUNCTION jwt.sign(payload json, secret text, algorithm text DEFAULT 'HS256')\nRETURNS text LANGUAGE sql AS $$\nWITH\n  header AS (\n    SELECT jwt.url_encode(convert_to('{\"alg\":\"' || algorithm || '\",\"typ\":\"JWT\"}', 'utf8'))\n    ),\n  payload AS (\n    SELECT jwt.url_encode(convert_to(payload::text, 'utf8'))\n    ),\n  signables AS (\n    SELECT (SELECT * FROM header) || '.' || (SELECT * FROM payload)\n    )\nSELECT\n    (SELECT * FROM signables)\n    || '.' ||\n    jwt.algorithm_sign((SELECT * FROM signables), secret, algorithm);\n$$;\n\n\nCREATE OR REPLACE FUNCTION jwt.verify(token text, secret text, algorithm text DEFAULT 'HS256')\nRETURNS table(header json, payload json, valid boolean) LANGUAGE sql AS $$\n  SELECT\n    convert_from(jwt.url_decode(r[1]), 'utf8')::json AS header,\n    convert_from(jwt.url_decode(r[2]), 'utf8')::json AS payload,\n    r[3] = jwt.algorithm_sign(r[1] || '.' || r[2], secret, algorithm) AS valid\n  FROM regexp_split_to_array(token, '\\.') r;\n$$;\nCOMMIT;\n"
  },
  {
    "path": "test/spec/fixtures/lines.csv",
    "content": "﻿id,name,geom\n1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440\n2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840\n"
  },
  {
    "path": "test/spec/fixtures/load.sql",
    "content": "-- Loads all fixtures for the PostgREST tests\n\n\\set ON_ERROR_STOP on\n\n\\ir database.sql\n\\ir roles.sql\n\\ir schema.sql\n\\ir jwt.sql\n\\ir jsonschema.sql\n\\ir privileges.sql\n\\ir data.sql\n"
  },
  {
    "path": "test/spec/fixtures/openapi.json",
    "content": "{\n  \"title\": \"A JSON Schema for Swagger 2.0 API.\",\n  \"id\": \"openapi.json\",\n  \"$schema\": \"draft04.json\",\n  \"type\": \"object\",\n  \"required\": [\n    \"swagger\",\n    \"info\",\n    \"paths\"\n  ],\n  \"additionalProperties\": false,\n  \"patternProperties\": {\n    \"^x-\": {\n      \"$ref\": \"#/definitions/vendorExtension\"\n    }\n  },\n  \"properties\": {\n    \"swagger\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"2.0\"\n      ],\n      \"description\": \"The Swagger version of this document.\"\n    },\n    \"info\": {\n      \"$ref\": \"#/definitions/info\"\n    },\n    \"host\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[^{}/ :\\\\\\\\]+(?::\\\\d+)?$\",\n      \"description\": \"The host (name or ip) of the API. Example: 'swagger.io'\"\n    },\n    \"basePath\": {\n      \"type\": \"string\",\n      \"pattern\": \"^/\",\n      \"description\": \"The base path to the API. Example: '/api'.\"\n    },\n    \"schemes\": {\n      \"$ref\": \"#/definitions/schemesList\"\n    },\n    \"consumes\": {\n      \"description\": \"A list of MIME types accepted by the API.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/mediaTypeList\"\n        }\n      ]\n    },\n    \"produces\": {\n      \"description\": \"A list of MIME types the API can produce.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/mediaTypeList\"\n        }\n      ]\n    },\n    \"paths\": {\n      \"$ref\": \"#/definitions/paths\"\n    },\n    \"definitions\": {\n      \"$ref\": \"#/definitions/definitions\"\n    },\n    \"parameters\": {\n      \"$ref\": \"#/definitions/parameterDefinitions\"\n    },\n    \"responses\": {\n      \"$ref\": \"#/definitions/responseDefinitions\"\n    },\n    \"security\": {\n      \"$ref\": \"#/definitions/security\"\n    },\n    \"securityDefinitions\": {\n      \"$ref\": \"#/definitions/securityDefinitions\"\n    },\n    \"tags\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/tag\"\n      },\n      \"uniqueItems\": true\n    },\n    \"externalDocs\": {\n      \"$ref\": \"#/definitions/externalDocs\"\n    }\n  },\n  \"definitions\": {\n    \"info\": {\n      \"type\": \"object\",\n      \"description\": \"General information about the API.\",\n      \"required\": [\n        \"version\",\n        \"title\"\n      ],\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"A unique and precise title of the API.\"\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"A semantic version number of the API.\"\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A longer description of the API. Should be different from the title.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"termsOfService\": {\n          \"type\": \"string\",\n          \"description\": \"The terms of service for the API.\"\n        },\n        \"contact\": {\n          \"$ref\": \"#/definitions/contact\"\n        },\n        \"license\": {\n          \"$ref\": \"#/definitions/license\"\n        }\n      }\n    },\n    \"contact\": {\n      \"type\": \"object\",\n      \"description\": \"Contact information for the owners of the API.\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The identifying name of the contact person/organization.\"\n        },\n        \"url\": {\n          \"type\": \"string\",\n          \"description\": \"The URL pointing to the contact information.\",\n          \"format\": \"uri\"\n        },\n        \"email\": {\n          \"type\": \"string\",\n          \"description\": \"The email address of the contact person/organization.\",\n          \"format\": \"email\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"license\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\"\n      ],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the license type. It's encouraged to use an OSI compatible license.\"\n        },\n        \"url\": {\n          \"type\": \"string\",\n          \"description\": \"The URL pointing to the license.\",\n          \"format\": \"uri\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"paths\": {\n      \"type\": \"object\",\n      \"description\": \"Relative paths to the individual endpoints. They must be relative to the 'basePath'.\",\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        },\n        \"^/\": {\n          \"$ref\": \"#/definitions/pathItem\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"definitions\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"$ref\": \"#/definitions/schema\"\n      },\n      \"description\": \"One or more JSON objects describing the schemas being consumed and produced by the API.\"\n    },\n    \"parameterDefinitions\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"$ref\": \"#/definitions/parameter\"\n      },\n      \"description\": \"One or more JSON representations for parameters\"\n    },\n    \"responseDefinitions\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"$ref\": \"#/definitions/response\"\n      },\n      \"description\": \"One or more JSON representations for parameters\"\n    },\n    \"externalDocs\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"description\": \"information about external documentation\",\n      \"required\": [\n        \"url\"\n      ],\n      \"properties\": {\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"url\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"examples\": {\n      \"type\": \"object\",\n      \"additionalProperties\": true\n    },\n    \"mimeType\": {\n      \"type\": \"string\",\n      \"description\": \"The MIME type of the HTTP message.\"\n    },\n    \"operation\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"responses\"\n      ],\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"tags\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"uniqueItems\": true\n        },\n        \"summary\": {\n          \"type\": \"string\",\n          \"description\": \"A brief summary of the operation.\"\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A longer description of the operation, GitHub Flavored Markdown is allowed.\"\n        },\n        \"externalDocs\": {\n          \"$ref\": \"#/definitions/externalDocs\"\n        },\n        \"operationId\": {\n          \"type\": \"string\",\n          \"description\": \"A unique identifier of the operation.\"\n        },\n        \"produces\": {\n          \"description\": \"A list of MIME types the API can produce.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/mediaTypeList\"\n            }\n          ]\n        },\n        \"consumes\": {\n          \"description\": \"A list of MIME types the API can consume.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/mediaTypeList\"\n            }\n          ]\n        },\n        \"parameters\": {\n          \"$ref\": \"#/definitions/parametersList\"\n        },\n        \"responses\": {\n          \"$ref\": \"#/definitions/responses\"\n        },\n        \"schemes\": {\n          \"$ref\": \"#/definitions/schemesList\"\n        },\n        \"deprecated\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"security\": {\n          \"$ref\": \"#/definitions/security\"\n        }\n      }\n    },\n    \"pathItem\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"$ref\": {\n          \"type\": \"string\"\n        },\n        \"get\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"put\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"post\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"delete\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"options\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"head\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"patch\": {\n          \"$ref\": \"#/definitions/operation\"\n        },\n        \"parameters\": {\n          \"$ref\": \"#/definitions/parametersList\"\n        }\n      }\n    },\n    \"responses\": {\n      \"type\": \"object\",\n      \"description\": \"Response objects names can either be any valid HTTP status code or 'default'.\",\n      \"minProperties\": 1,\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^([0-9]{3})$|^(default)$\": {\n          \"$ref\": \"#/definitions/responseValue\"\n        },\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"not\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"patternProperties\": {\n          \"^x-\": {\n            \"$ref\": \"#/definitions/vendorExtension\"\n          }\n        }\n      }\n    },\n    \"responseValue\": {\n      \"oneOf\": [\n        {\n          \"$ref\": \"#/definitions/response\"\n        },\n        {\n          \"$ref\": \"#/definitions/jsonReference\"\n        }\n      ]\n    },\n    \"response\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"description\"\n      ],\n      \"properties\": {\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"schema\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/definitions/schema\"\n            },\n            {\n              \"$ref\": \"#/definitions/fileSchema\"\n            }\n          ]\n        },\n        \"headers\": {\n          \"$ref\": \"#/definitions/headers\"\n        },\n        \"examples\": {\n          \"$ref\": \"#/definitions/examples\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"headers\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"$ref\": \"#/definitions/header\"\n      }\n    },\n    \"header\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"integer\",\n            \"boolean\",\n            \"array\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormat\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"vendorExtension\": {\n      \"description\": \"Any property starting with x- is valid.\",\n      \"additionalProperties\": true,\n      \"additionalItems\": true\n    },\n    \"bodyParameter\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"in\",\n        \"schema\"\n      ],\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A brief description of the parameter. This could contain examples of use.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the parameter.\"\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"description\": \"Determines the location of the parameter.\",\n          \"enum\": [\n            \"body\"\n          ]\n        },\n        \"required\": {\n          \"type\": \"boolean\",\n          \"description\": \"Determines whether or not this parameter is required or optional.\",\n          \"default\": false\n        },\n        \"schema\": {\n          \"$ref\": \"#/definitions/schema\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"headerParameterSubSchema\": {\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"required\": {\n          \"type\": \"boolean\",\n          \"description\": \"Determines whether or not this parameter is required or optional.\",\n          \"default\": false\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"description\": \"Determines the location of the parameter.\",\n          \"enum\": [\n            \"header\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A brief description of the parameter. This could contain examples of use.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the parameter.\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"boolean\",\n            \"integer\",\n            \"array\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormat\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        }\n      }\n    },\n    \"queryParameterSubSchema\": {\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"required\": {\n          \"type\": \"boolean\",\n          \"description\": \"Determines whether or not this parameter is required or optional.\",\n          \"default\": false\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"description\": \"Determines the location of the parameter.\",\n          \"enum\": [\n            \"query\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A brief description of the parameter. This could contain examples of use.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the parameter.\"\n        },\n        \"allowEmptyValue\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"allows sending a parameter by name only or with an empty value.\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"boolean\",\n            \"integer\",\n            \"array\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormatWithMulti\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        }\n      }\n    },\n    \"formDataParameterSubSchema\": {\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"required\": {\n          \"type\": \"boolean\",\n          \"description\": \"Determines whether or not this parameter is required or optional.\",\n          \"default\": false\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"description\": \"Determines the location of the parameter.\",\n          \"enum\": [\n            \"formData\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A brief description of the parameter. This could contain examples of use.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the parameter.\"\n        },\n        \"allowEmptyValue\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"allows sending a parameter by name only or with an empty value.\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"boolean\",\n            \"integer\",\n            \"array\",\n            \"file\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormatWithMulti\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        }\n      }\n    },\n    \"pathParameterSubSchema\": {\n      \"additionalProperties\": false,\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"required\": [\n        \"required\"\n      ],\n      \"properties\": {\n        \"required\": {\n          \"type\": \"boolean\",\n          \"enum\": [\n            true\n          ],\n          \"description\": \"Determines whether or not this parameter is required or optional.\"\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"description\": \"Determines the location of the parameter.\",\n          \"enum\": [\n            \"path\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A brief description of the parameter. This could contain examples of use.  GitHub Flavored Markdown is allowed.\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"The name of the parameter.\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"boolean\",\n            \"integer\",\n            \"array\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormat\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        }\n      }\n    },\n    \"nonBodyParameter\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"in\",\n        \"type\"\n      ],\n      \"oneOf\": [\n        {\n          \"$ref\": \"#/definitions/headerParameterSubSchema\"\n        },\n        {\n          \"$ref\": \"#/definitions/formDataParameterSubSchema\"\n        },\n        {\n          \"$ref\": \"#/definitions/queryParameterSubSchema\"\n        },\n        {\n          \"$ref\": \"#/definitions/pathParameterSubSchema\"\n        }\n      ]\n    },\n    \"parameter\": {\n      \"oneOf\": [\n        {\n          \"$ref\": \"#/definitions/bodyParameter\"\n        },\n        {\n          \"$ref\": \"#/definitions/nonBodyParameter\"\n        }\n      ]\n    },\n    \"schema\": {\n      \"type\": \"object\",\n      \"description\": \"A deterministic version of a JSON Schema object.\",\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"properties\": {\n        \"$ref\": {\n          \"type\": \"string\"\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"title\": {\n          \"$ref\": \"draft04.json#/properties/title\"\n        },\n        \"description\": {\n          \"$ref\": \"draft04.json#/properties/description\"\n        },\n        \"default\": {\n          \"$ref\": \"draft04.json#/properties/default\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"draft04.json#/properties/multipleOf\"\n        },\n        \"maximum\": {\n          \"$ref\": \"draft04.json#/properties/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"draft04.json#/properties/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"draft04.json#/properties/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"draft04.json#/properties/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"draft04.json#/definitions/positiveInteger\"\n        },\n        \"minLength\": {\n          \"$ref\": \"draft04.json#/definitions/positiveIntegerDefault0\"\n        },\n        \"pattern\": {\n          \"$ref\": \"draft04.json#/properties/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"draft04.json#/definitions/positiveInteger\"\n        },\n        \"minItems\": {\n          \"$ref\": \"draft04.json#/definitions/positiveIntegerDefault0\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"draft04.json#/properties/uniqueItems\"\n        },\n        \"maxProperties\": {\n          \"$ref\": \"draft04.json#/definitions/positiveInteger\"\n        },\n        \"minProperties\": {\n          \"$ref\": \"draft04.json#/definitions/positiveIntegerDefault0\"\n        },\n        \"required\": {\n          \"$ref\": \"draft04.json#/definitions/stringArray\"\n        },\n        \"enum\": {\n          \"$ref\": \"draft04.json#/properties/enum\"\n        },\n        \"additionalProperties\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/schema\"\n            },\n            {\n              \"type\": \"boolean\"\n            }\n          ],\n          \"default\": {}\n        },\n        \"type\": {\n          \"$ref\": \"draft04.json#/properties/type\"\n        },\n        \"items\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/schema\"\n            },\n            {\n              \"type\": \"array\",\n              \"minItems\": 1,\n              \"items\": {\n                \"$ref\": \"#/definitions/schema\"\n              }\n            }\n          ],\n          \"default\": {}\n        },\n        \"allOf\": {\n          \"type\": \"array\",\n          \"minItems\": 1,\n          \"items\": {\n            \"$ref\": \"#/definitions/schema\"\n          }\n        },\n        \"properties\": {\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/schema\"\n          },\n          \"default\": {}\n        },\n        \"discriminator\": {\n          \"type\": \"string\"\n        },\n        \"readOnly\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"xml\": {\n          \"$ref\": \"#/definitions/xml\"\n        },\n        \"externalDocs\": {\n          \"$ref\": \"#/definitions/externalDocs\"\n        },\n        \"example\": {}\n      },\n      \"additionalProperties\": false\n    },\n    \"fileSchema\": {\n      \"type\": \"object\",\n      \"description\": \"A deterministic version of a JSON Schema object.\",\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"properties\": {\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"title\": {\n          \"$ref\": \"draft04.json#/properties/title\"\n        },\n        \"description\": {\n          \"$ref\": \"draft04.json#/properties/description\"\n        },\n        \"default\": {\n          \"$ref\": \"draft04.json#/properties/default\"\n        },\n        \"required\": {\n          \"$ref\": \"draft04.json#/definitions/stringArray\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"file\"\n          ]\n        },\n        \"readOnly\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"externalDocs\": {\n          \"$ref\": \"#/definitions/externalDocs\"\n        },\n        \"example\": {}\n      },\n      \"additionalProperties\": false\n    },\n    \"primitivesItems\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"string\",\n            \"number\",\n            \"integer\",\n            \"boolean\",\n            \"array\"\n          ]\n        },\n        \"format\": {\n          \"type\": \"string\"\n        },\n        \"items\": {\n          \"$ref\": \"#/definitions/primitivesItems\"\n        },\n        \"collectionFormat\": {\n          \"$ref\": \"#/definitions/collectionFormat\"\n        },\n        \"default\": {\n          \"$ref\": \"#/definitions/default\"\n        },\n        \"maximum\": {\n          \"$ref\": \"#/definitions/maximum\"\n        },\n        \"exclusiveMaximum\": {\n          \"$ref\": \"#/definitions/exclusiveMaximum\"\n        },\n        \"minimum\": {\n          \"$ref\": \"#/definitions/minimum\"\n        },\n        \"exclusiveMinimum\": {\n          \"$ref\": \"#/definitions/exclusiveMinimum\"\n        },\n        \"maxLength\": {\n          \"$ref\": \"#/definitions/maxLength\"\n        },\n        \"minLength\": {\n          \"$ref\": \"#/definitions/minLength\"\n        },\n        \"pattern\": {\n          \"$ref\": \"#/definitions/pattern\"\n        },\n        \"maxItems\": {\n          \"$ref\": \"#/definitions/maxItems\"\n        },\n        \"minItems\": {\n          \"$ref\": \"#/definitions/minItems\"\n        },\n        \"uniqueItems\": {\n          \"$ref\": \"#/definitions/uniqueItems\"\n        },\n        \"enum\": {\n          \"$ref\": \"#/definitions/enum\"\n        },\n        \"multipleOf\": {\n          \"$ref\": \"#/definitions/multipleOf\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"security\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/securityRequirement\"\n      },\n      \"uniqueItems\": true\n    },\n    \"securityRequirement\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"string\"\n        },\n        \"uniqueItems\": true\n      }\n    },\n    \"xml\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"prefix\": {\n          \"type\": \"string\"\n        },\n        \"attribute\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"wrapped\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"tag\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"externalDocs\": {\n          \"$ref\": \"#/definitions/externalDocs\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"securityDefinitions\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"oneOf\": [\n          {\n            \"$ref\": \"#/definitions/basicAuthenticationSecurity\"\n          },\n          {\n            \"$ref\": \"#/definitions/apiKeySecurity\"\n          },\n          {\n            \"$ref\": \"#/definitions/oauth2ImplicitSecurity\"\n          },\n          {\n            \"$ref\": \"#/definitions/oauth2PasswordSecurity\"\n          },\n          {\n            \"$ref\": \"#/definitions/oauth2ApplicationSecurity\"\n          },\n          {\n            \"$ref\": \"#/definitions/oauth2AccessCodeSecurity\"\n          }\n        ]\n      }\n    },\n    \"basicAuthenticationSecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"basic\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"apiKeySecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\",\n        \"name\",\n        \"in\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"apiKey\"\n          ]\n        },\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"in\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"header\",\n            \"query\"\n          ]\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"oauth2ImplicitSecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\",\n        \"flow\",\n        \"authorizationUrl\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"oauth2\"\n          ]\n        },\n        \"flow\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"implicit\"\n          ]\n        },\n        \"scopes\": {\n          \"$ref\": \"#/definitions/oauth2Scopes\"\n        },\n        \"authorizationUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"oauth2PasswordSecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\",\n        \"flow\",\n        \"tokenUrl\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"oauth2\"\n          ]\n        },\n        \"flow\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"password\"\n          ]\n        },\n        \"scopes\": {\n          \"$ref\": \"#/definitions/oauth2Scopes\"\n        },\n        \"tokenUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"oauth2ApplicationSecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\",\n        \"flow\",\n        \"tokenUrl\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"oauth2\"\n          ]\n        },\n        \"flow\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"application\"\n          ]\n        },\n        \"scopes\": {\n          \"$ref\": \"#/definitions/oauth2Scopes\"\n        },\n        \"tokenUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"oauth2AccessCodeSecurity\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"required\": [\n        \"type\",\n        \"flow\",\n        \"authorizationUrl\",\n        \"tokenUrl\"\n      ],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"oauth2\"\n          ]\n        },\n        \"flow\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"accessCode\"\n          ]\n        },\n        \"scopes\": {\n          \"$ref\": \"#/definitions/oauth2Scopes\"\n        },\n        \"authorizationUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"tokenUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        }\n      },\n      \"patternProperties\": {\n        \"^x-\": {\n          \"$ref\": \"#/definitions/vendorExtension\"\n        }\n      }\n    },\n    \"oauth2Scopes\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    },\n    \"mediaTypeList\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/mimeType\"\n      },\n      \"uniqueItems\": true\n    },\n    \"parametersList\": {\n      \"type\": \"array\",\n      \"description\": \"The parameters needed to send a valid API call.\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"oneOf\": [\n          {\n            \"$ref\": \"#/definitions/parameter\"\n          },\n          {\n            \"$ref\": \"#/definitions/jsonReference\"\n          }\n        ]\n      },\n      \"uniqueItems\": true\n    },\n    \"schemesList\": {\n      \"type\": \"array\",\n      \"description\": \"The transfer protocol of the API.\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"http\",\n          \"https\",\n          \"ws\",\n          \"wss\"\n        ]\n      },\n      \"uniqueItems\": true\n    },\n    \"collectionFormat\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"csv\",\n        \"ssv\",\n        \"tsv\",\n        \"pipes\"\n      ],\n      \"default\": \"csv\"\n    },\n    \"collectionFormatWithMulti\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"csv\",\n        \"ssv\",\n        \"tsv\",\n        \"pipes\",\n        \"multi\"\n      ],\n      \"default\": \"csv\"\n    },\n    \"title\": {\n      \"$ref\": \"draft04.json#/properties/title\"\n    },\n    \"description\": {\n      \"$ref\": \"draft04.json#/properties/description\"\n    },\n    \"default\": {\n      \"$ref\": \"draft04.json#/properties/default\"\n    },\n    \"multipleOf\": {\n      \"$ref\": \"draft04.json#/properties/multipleOf\"\n    },\n    \"maximum\": {\n      \"$ref\": \"draft04.json#/properties/maximum\"\n    },\n    \"exclusiveMaximum\": {\n      \"$ref\": \"draft04.json#/properties/exclusiveMaximum\"\n    },\n    \"minimum\": {\n      \"$ref\": \"draft04.json#/properties/minimum\"\n    },\n    \"exclusiveMinimum\": {\n      \"$ref\": \"draft04.json#/properties/exclusiveMinimum\"\n    },\n    \"maxLength\": {\n      \"$ref\": \"draft04.json#/definitions/positiveInteger\"\n    },\n    \"minLength\": {\n      \"$ref\": \"draft04.json#/definitions/positiveIntegerDefault0\"\n    },\n    \"pattern\": {\n      \"$ref\": \"draft04.json#/properties/pattern\"\n    },\n    \"maxItems\": {\n      \"$ref\": \"draft04.json#/definitions/positiveInteger\"\n    },\n    \"minItems\": {\n      \"$ref\": \"draft04.json#/definitions/positiveIntegerDefault0\"\n    },\n    \"uniqueItems\": {\n      \"$ref\": \"draft04.json#/properties/uniqueItems\"\n    },\n    \"enum\": {\n      \"$ref\": \"draft04.json#/properties/enum\"\n    },\n    \"jsonReference\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"$ref\"\n      ],\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"$ref\": {\n          \"type\": \"string\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "test/spec/fixtures/privileges.sql",
    "content": "-- Privileges for anonymous\nGRANT USAGE ON SCHEMA\n      \"EXTRA \"\"@/\\#~_-\"\n    , \"SPECIAL \"\"@/\\#~_-\"\n    , \"تست\"\n    , extensions\n    , jwt\n    , postgrest\n    , public\n    , test\n    , v1\n    , v2\nTO postgrest_test_anonymous;\n\n-- Schema test objects\nSET search_path = test, \"تست\", pg_catalog;\n\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA\n      \"SPECIAL \"\"@/\\#~_-\"\n    , \"تست\"\n    , test\n    , v1\n    , v2\nTO postgrest_test_anonymous;\n\nREVOKE ALL PRIVILEGES ON TABLE\n      app_users\n    , authors_only\n    , insertonly\n    , limited_article_stars\nFROM postgrest_test_anonymous;\n\nGRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;\n\nGRANT USAGE ON SEQUENCE\n      auto_incrementing_pk_id_seq\n    , items_id_seq\n    , items2_id_seq\n    , items3_id_seq\n    , callcounter_count\n    , leak_id_seq\n    , surr_serial_upsert_id_seq\n    , surr_gen_default_upsert_id_seq\n    , \"Surr_Gen_Default_Upsert_id_seq\"\nTO postgrest_test_anonymous;\n\nGRANT USAGE ON SEQUENCE channels_id_seq TO postgrest_test_anonymous;\n\n-- Privileges for non anonymous users\nGRANT USAGE ON SCHEMA test TO postgrest_test_author;\nGRANT ALL ON TABLE authors_only TO postgrest_test_author;\n\nGRANT SELECT (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;\nGRANT INSERT (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;\nGRANT UPDATE (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;\n\nGRANT SELECT(id, email) ON TABLE app_users TO postgrest_test_anonymous;\nGRANT INSERT, UPDATE    ON TABLE app_users TO postgrest_test_anonymous;\nGRANT DELETE            ON TABLE app_users TO postgrest_test_anonymous;\n\nREVOKE EXECUTE ON FUNCTION privileged_hello(text) FROM PUBLIC; -- All functions are available to every role(PUBLIC) by default\nGRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;\n\nGRANT USAGE ON SCHEMA test TO postgrest_test_default_role;\n\nGRANT ALL ON TABLE artists TO postgrest_test_anonymous;\nGRANT ALL ON TABLE albums TO postgrest_test_anonymous;\n"
  },
  {
    "path": "test/spec/fixtures/roles.sql",
    "content": "DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser;\nCREATE ROLE postgrest_test_anonymous;\nCREATE ROLE postgrest_test_default_role;\nCREATE ROLE postgrest_test_author;\nCREATE ROLE postgrest_test_superuser WITH SUPERUSER;\n\nGRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser TO :PGUSER;\n"
  },
  {
    "path": "test/spec/fixtures/schema.sql",
    "content": "SET check_function_bodies = false;\n-- Hide warnings because casts on domains would show a lot of:\n--  WARNING:  cast will be ignored because the source data type is a domain\nSET client_min_messages = error;\n\nCREATE SCHEMA public;\nCREATE SCHEMA postgrest;\nCREATE SCHEMA private;\nCREATE SCHEMA test;\nCREATE SCHEMA تست;\nCREATE SCHEMA extensions;\nCREATE SCHEMA v1;\nCREATE SCHEMA v2;\nCREATE SCHEMA \"SPECIAL \"\"@/\\#~_-\";\nCREATE SCHEMA \"EXTRA \"\"@/\\#~_-\";\n\nCOMMENT ON SCHEMA v1 IS 'v1 schema';\nCOMMENT ON SCHEMA v2 IS 'v2 schema';\n\nCOMMENT ON SCHEMA test IS\n$$My API title\n\nMy API description\nthat spans\nmultiple lines$$;\n--\n-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -\n--\n\nCREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;\n\nSET search_path = public, pg_catalog;\n\nCREATE EXTENSION IF NOT EXISTS pgcrypto;\n\n--\n-- Name: jwt_token; Type: TYPE; Schema: public; Owner: -\n--\n\nCREATE TYPE public.jwt_token AS (\n\ttoken text\n);\n\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: enum_menagerie_type; Type: TYPE; Schema: test; Owner: -\n--\n\nCREATE TYPE enum_menagerie_type AS ENUM (\n    'foo',\n    'bar'\n);\n\ncreate type  bit as enum ('one', 'two');\n\nSET search_path = postgrest, pg_catalog;\n\n--\n-- Name: check_role_exists(); Type: FUNCTION; Schema: postgrest; Owner: -\n--\n\nCREATE FUNCTION check_role_exists() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nbegin\nif not exists (select 1 from pg_roles as r where r.rolname = new.rolname) then\n   raise foreign_key_violation using message = 'Cannot create user with unknown role: ' || new.rolname;\n   return null;\n end if;\n return new;\nend\n$$;\n\n\n--\n-- Name: set_authors_only_owner(); Type: FUNCTION; Schema: postgrest; Owner: -\n--\n\nCREATE FUNCTION set_authors_only_owner() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nbegin\n  NEW.owner = current_setting('request.jwt.claims')::json->>'id';\n  RETURN NEW;\nend\n$$;\n\n\n--\n-- Name: update_owner(); Type: FUNCTION; Schema: postgrest; Owner: -\n--\n\nCREATE FUNCTION update_owner() RETURNS trigger\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n   NEW.owner = current_user;\n   RETURN NEW;\nEND;\n$$;\n\n\nSET search_path = test, pg_catalog;\n\nSET default_tablespace = '';\n\nSET default_with_oids = false;\n\ncreate domain \"text/plain\" as text;\ncreate domain \"text/html\" as text;\ncreate domain \"text/xml\" as pg_catalog.xml;\ncreate domain \"application/octet-stream\" as bytea;\ncreate domain \"image/png\" as bytea;\ncreate domain \"application/vnd.twkb\" as bytea;\ncreate domain \"application/openapi+json\" as json;\ncreate domain \"application/geo+json\" as jsonb;\ncreate domain \"application/vnd.geo2+json\" as jsonb;\ncreate domain \"application/json\" as json;\ncreate domain \"application/vnd.pgrst.object\" as json;\ncreate domain \"text/tab-separated-values\" as text;\ncreate domain \"text/csv\" as text;\ncreate domain \"*/*\" as bytea;\n\nCREATE TABLE items (\n    id bigserial primary key\n);\n\nCREATE TABLE items2 (\n    id bigserial primary key\n);\n\nCREATE TABLE items3 (\n    id bigserial primary key\n);\n\nCREATE FUNCTION search(id BIGINT) RETURNS SETOF items\n    LANGUAGE plpgsql\n    STABLE\n    AS $$BEGIN\n        RETURN QUERY SELECT items.id FROM items WHERE items.id=search.id;\n    END$$;\n\nCREATE FUNCTION always_true(test.items) RETURNS boolean\n    LANGUAGE sql IMMUTABLE\n    AS $$ SELECT true $$;\n\nCREATE FUNCTION computed_overload(test.items) RETURNS boolean\n    LANGUAGE sql IMMUTABLE\n    AS $$ SELECT true $$;\n\nCREATE FUNCTION computed_overload(test.items2) RETURNS boolean\n    LANGUAGE sql IMMUTABLE\n    AS $$ SELECT true $$;\n\nCREATE FUNCTION is_first(test.items) RETURNS boolean\n    LANGUAGE sql IMMUTABLE\n    AS $$ SELECT $1.id = 1 $$;\n\n\nCREATE FUNCTION anti_id(test.items) RETURNS bigint\n    LANGUAGE sql IMMUTABLE\n    AS $_$ SELECT $1.id * -1 $_$;\n\nSET search_path = public, pg_catalog;\n\nCREATE FUNCTION always_false(test.items) RETURNS boolean\n    LANGUAGE sql IMMUTABLE\n    AS $$ SELECT false $$;\n\ncreate table public_consumers (\n    id                  serial             not null unique,\n    name                text               not null check (name <> ''),\n    primary key (id)\n);\n\ncreate table public_orders (\n    id                  serial             not null unique,\n    consumer            integer            not null references public_consumers(id),\n    number              integer            not null,\n    primary key (id)\n);\n\nSET search_path = تست, pg_catalog;\n\nCREATE TABLE موارد (\n    هویت bigint NOT NULL\n);\n\n\nSET search_path = test, pg_catalog;\n\n\ncreate view orders_view as\n  select * from public.public_orders;\n\ncreate view consumers_view as\n  select * from public.public_consumers;\n\ncreate view consumers_view_view as\n  select * from consumers_view;\n\ncreate view public.consumers_extra as\n  select * from consumers_view;\n\ncreate view consumers_extra_view as\n  select * from public.consumers_extra;\n\n--\n-- Name: getitemrange(bigint, bigint); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION getitemrange(min bigint, max bigint) RETURNS SETOF items\n    LANGUAGE sql\n    STABLE\n    AS $_$\n    SELECT * FROM test.items WHERE id > $1 AND id <= $2;\n$_$;\n\n--\n-- Name: version(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION noparamsproc() RETURNS text\n\tLANGUAGE sql\n        IMMUTABLE\n\tAS $$\n\t\tSELECT a FROM (VALUES ('Return value of no parameters procedure.')) s(a);\n\t$$;\n\n--\n-- Name: login(text, text); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token\n    LANGUAGE sql SECURITY DEFINER\n    STABLE\n    AS $$\nSELECT jwt.sign(\n    row_to_json(r), 'reallyreallyreallyreallyverysafe'\n  ) as token\n  FROM (\n    SELECT rolname::text, id::text\n      FROM postgrest.auth\n     WHERE id = id AND pass = pass\n  ) r;\n$$;\n\n\nCREATE FUNCTION varied_arguments(\n  double double precision,\n  \"varchar\" character varying,\n  \"boolean\" boolean,\n  date date,\n  money money,\n  enum enum_menagerie_type,\n  arr text[],\n  \"integer\" integer default 42,\n  \"json\" json default '{}',\n  jsonb jsonb default '{}'\n) RETURNS json\nLANGUAGE sql\nIMMUTABLE\nAS $_$\n  SELECT json_build_object(\n    'double', double,\n    'varchar', \"varchar\",\n    'boolean', \"boolean\",\n    'date', date,\n    'money', money,\n    'enum', enum,\n    'arr', arr,\n    'integer', \"integer\",\n    'json', json,\n    'jsonb', jsonb\n  );\n$_$;\n\nCOMMENT ON FUNCTION varied_arguments(double precision, character varying, boolean, date, money, enum_menagerie_type, text[], integer, json, jsonb) IS\n$_$An RPC function\n\nJust a test for RPC function arguments$_$;\n\nCREATE FUNCTION varied_arguments_openapi(\n  double double precision,\n  \"varchar\" character varying,\n  \"boolean\" boolean,\n  date date,\n  money money,\n  enum enum_menagerie_type,\n  text_arr text[],\n  int_arr int[],\n  bool_arr boolean[],\n  char_arr char[],\n  varchar_arr varchar[],\n  bigint_arr bigint[],\n  numeric_arr numeric[],\n  json_arr json[],\n  jsonb_arr jsonb[],\n  \"integer\" integer default 42,\n  \"json\" json default '{}',\n  jsonb jsonb default '{}'\n) RETURNS json\n  LANGUAGE sql\n  IMMUTABLE\nAS $_$\nSELECT json_build_object(\n           'double', double,\n           'varchar', \"varchar\",\n           'boolean', \"boolean\",\n           'date', date,\n           'money', money,\n           'enum', enum,\n           'text_arr', text_arr,\n           'int_arr', int_arr,\n           'bool_arr', bool_arr,\n           'char_arr', char_arr,\n           'varchar_arr', varchar_arr,\n           'bigint_arr', bigint_arr,\n           'numeric_arr', numeric_arr,\n           'json_arr', json_arr,\n           'jsonb_arr', jsonb_arr,\n           'integer', \"integer\",\n           'json', json,\n           'jsonb', jsonb\n         );\n$_$;\n\nCOMMENT 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\n  $_$An RPC function\n\nJust a test for RPC function arguments$_$;\n\n\nCREATE FUNCTION json_argument(arg json) RETURNS text\nLANGUAGE sql\nIMMUTABLE\nAS $_$\n  SELECT json_typeof(arg);\n$_$;\n\n--\n-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION jwt_test() RETURNS public.jwt_token\n    LANGUAGE sql SECURITY DEFINER\n    IMMUTABLE\n    AS $$\nSELECT jwt.sign(\n    row_to_json(r), 'reallyreallyreallyreallyverysafe'\n  ) as token\n  FROM (\n    SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,\n       1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,\n       'foo'::text as jti, 'postgrest_test'::text as role,\n       true as \"http://postgrest.com/foo\"\n  ) r;\n$$;\n\n\nCREATE OR REPLACE FUNCTION switch_role() RETURNS void\n  LANGUAGE plpgsql\n  AS $$\ndeclare\n  user_id text;\nBegin\n  user_id = (current_setting('request.jwt.claims')::json->>'id')::text;\n  if user_id = '1'::text then\n    execute 'set local role postgrest_test_author';\n  elseif user_id = '2'::text then\n    execute 'set local role postgrest_test_default_role';\n  elseif user_id = '3'::text then\n    RAISE EXCEPTION 'Disabled ID --> %', user_id USING HINT = 'Please contact administrator';\n  /* else */\n  /*   execute 'set local role postgrest_test_anonymous'; */\n  end if;\nend\n$$;\n\nCREATE FUNCTION get_current_user() RETURNS text\n  LANGUAGE sql\n  STABLE\n  AS $$\nSELECT current_user::text;\n$$;\n\n--\n-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION reveal_big_jwt() RETURNS TABLE (\n      iss text, sub text, exp bigint,\n      nbf bigint, iat bigint, jti text, \"http://postgrest.com/foo\" boolean\n    )\nAS $$\n  SELECT current_setting('request.jwt.claims')::json->>'iss' as iss,\n         current_setting('request.jwt.claims')::json->>'sub' as sub,\n         (current_setting('request.jwt.claims')::json->>'exp')::bigint as exp,\n         (current_setting('request.jwt.claims')::json->>'nbf')::bigint as nbf,\n         (current_setting('request.jwt.claims')::json->>'iat')::bigint as iat,\n         current_setting('request.jwt.claims')::json->>'jti' as jti,\n         (current_setting('request.jwt.claims')::json->>'http://postgrest.com/foo')::boolean as \"http://postgrest.com/foo\";\n$$ LANGUAGE sql SECURITY DEFINER STABLE;\n\n\nCREATE FUNCTION assert() RETURNS void\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n      ASSERT false, 'bad thing';\nEND;\n$$;\n\n\n--\n-- Name: problem(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION problem() RETURNS void\n    LANGUAGE plpgsql\n    AS $$\nBEGIN\n      RAISE 'bad thing';\nEND;\n$$;\n\n\n--\n-- Name: sayhello(text); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION sayhello(name text) RETURNS text\n    LANGUAGE sql\n    IMMUTABLE\n    AS $_$\n    SELECT 'Hello, ' || $1;\n$_$;\n\n\n--\n-- Name: callcounter(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE SEQUENCE callcounter_count START 1;\n\nCREATE FUNCTION callcounter() RETURNS bigint\n    LANGUAGE sql\n    AS $_$\n    SELECT nextval('test.callcounter_count');\n$_$;\n\nCREATE FUNCTION reset_sequence(name TEXT, value INTEGER) RETURNS void\nSECURITY DEFINER\nLANGUAGE plpgsql AS $_$\nBEGIN\n  EXECUTE FORMAT($exec$\n    ALTER SEQUENCE %s RESTART WITH %s\n  $exec$, name, value);\nEND\n$_$;\n\n--\n-- Name: singlejsonparam(json); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION singlejsonparam(single_param json) RETURNS json\n    LANGUAGE sql\n    IMMUTABLE\n    AS $_$\n    SELECT single_param;\n$_$;\n\n--\n-- Name: test_empty_rowset(); Type: FUNCTION; Schema: test; Owner: -\n--\n\nCREATE FUNCTION test_empty_rowset() RETURNS SETOF integer\n    LANGUAGE sql\n    IMMUTABLE\n    AS $$\n    SELECT null::int FROM (SELECT 1) a WHERE false;\n$$;\n\n\nSET search_path = postgrest, pg_catalog;\n\n--\n-- Name: auth; Type: TABLE; Schema: postgrest; Owner: -\n--\n\nCREATE TABLE auth (\n    id character varying NOT NULL,\n    rolname name DEFAULT 'postgrest_test_author'::name NOT NULL,\n    pass character(60) NOT NULL\n);\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: authors_only; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE authors_only (\n    owner character varying NOT NULL,\n    secret character varying NOT NULL\n);\n\n\n--\n-- Name: auto_incrementing_pk; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE auto_incrementing_pk (\n    id integer NOT NULL,\n    nullable_string character varying,\n    non_nullable_string character varying NOT NULL,\n    inserted_at timestamp with time zone DEFAULT now()\n);\n\n\n--\n-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE; Schema: test; Owner: -\n--\n\nCREATE SEQUENCE auto_incrementing_pk_id_seq\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n--\n-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE OWNED BY; Schema: test; Owner: -\n--\n\nALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;\n\n\n--\n-- Name: clients; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE clients (\n    id integer primary key,\n    name text NOT NULL\n);\n\n--\n-- Name: complex_items; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE complex_items (\n    id bigint NOT NULL,\n    name text,\n    settings json,\n    arr_data integer[],\n\t\t\"field-with_sep\" integer default 1 not null\n);\n\n\n--\n-- Name: compound_pk; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE compound_pk (\n    -- those columns should not be referenced to in a foreign key anywhere\n    -- to allow the InsertSpec.Inserting into VIEWs.returns a location header\n    -- to test properly\n    PRIMARY KEY (k1, k2),\n    k1 integer NOT NULL,\n    k2 text NOT NULL,\n    extra integer\n);\n\n\nCREATE VIEW compound_pk_view AS\nSELECT * FROM compound_pk;\n\n--\n-- Name: empty_table; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE empty_table (\n    k character varying NOT NULL,\n    extra character varying NOT NULL\n);\n\n\n--\n-- Name: private_table; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE private_table ();\n\n\n--\n-- Name: has_count_column; Type: VIEW; Schema: test; Owner: -\n--\n\nCREATE VIEW has_count_column AS\n SELECT 1 AS count;\n\n\n--\n-- Name: has_fk; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE has_fk (\n    id bigint NOT NULL,\n    auto_inc_fk integer,\n    simple_fk character varying(255)\n);\n\n\n--\n-- Name: has_fk_id_seq; Type: SEQUENCE; Schema: test; Owner: -\n--\n\nCREATE SEQUENCE has_fk_id_seq\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\n--\n-- Name: has_fk_id_seq; Type: SEQUENCE OWNED BY; Schema: test; Owner: -\n--\n\nALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;\n\n\n--\n-- Name: insertable_view_with_join; Type: VIEW; Schema: test; Owner: -\n--\n\nCREATE VIEW insertable_view_with_join AS\n SELECT has_fk.id,\n    has_fk.auto_inc_fk,\n    has_fk.simple_fk,\n    auto_incrementing_pk.nullable_string,\n    auto_incrementing_pk.non_nullable_string,\n    auto_incrementing_pk.inserted_at\n   FROM (has_fk\n     JOIN auto_incrementing_pk USING (id));\n\n--\n-- Name: json_table; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE json_table (\n    data json\n);\n\n\n--\n-- Name: materialized_view; Type: MATERIALIZED VIEW; Schema: test; Owner: -\n--\n\nCREATE MATERIALIZED VIEW materialized_view AS\n SELECT version() AS version\n  WITH NO DATA;\n\n\n--\n-- Name: menagerie; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE menagerie (\n    \"integer\" integer NOT NULL,\n    double double precision NOT NULL,\n    \"varchar\" character varying NOT NULL,\n    \"boolean\" boolean NOT NULL,\n    date date NOT NULL,\n    money money NOT NULL,\n    enum enum_menagerie_type NOT NULL\n);\n\n\n--\n-- Name: no_pk; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE no_pk (\n    a character varying,\n    b character varying\n);\n\nCREATE TABLE only_pk (\n    id integer primary key\n);\n\n--\n-- Name: nullable_integer; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE nullable_integer (\n    a integer\n);\n\n\n--\n-- Name: insertonly; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE insertonly (\n    v text NOT NULL\n);\n\n\n--\n-- Name: projects; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE projects (\n    id integer primary key,\n    name text NOT NULL,\n    client_id integer REFERENCES clients(id)\n);\nalter table projects rename constraint projects_client_id_fkey to client;\n\n--\n-- Name: projects_view; Type: VIEW; Schema: test; Owner: -\n--\n\nCREATE VIEW projects_view AS\n SELECT projects.id,\n    projects.name,\n    projects.client_id\n   FROM projects;\n\n\nCREATE VIEW projects_view_alt AS\n SELECT projects.id as t_id,\n    projects.name,\n    projects.client_id as t_client_id\n   FROM projects;\n\nCREATE TABLE sponsors (\n    id integer primary key,\n    name varchar(20) not null\n);\n\nCREATE TABLE competitors (\n    id integer primary key,\n    sponsor_id integer references sponsors(id),\n    full_name varchar(40) not null\n);\n\nCREATE VIEW test_null_pk_competitors_sponsors  AS\nSELECT c.id, s.id as sponsor_id\nFROM competitors c\n    LEFT JOIN sponsors s on c.sponsor_id = s.id;\n\nCREATE RULE test_null_pk_competitors_sponsors  AS\n    ON INSERT TO test_null_pk_competitors_sponsors DO INSTEAD\n    INSERT INTO competitors(id, full_name, sponsor_id)\n    VALUES (new.id, 'Competitor without sponsor', new.sponsor_id)\n    RETURNING id, sponsor_id;\n\n--\n-- Name: simple_pk; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE simple_pk (\n    PRIMARY KEY (k),\n    k character varying NOT NULL,\n    extra character varying NOT NULL\n);\n\nCREATE TABLE simple_pk2 (\n    PRIMARY KEY (k),\n    k character varying NOT NULL,\n    extra character varying NOT NULL\n);\n\nCREATE TABLE users (\n    id integer primary key,\n    name text NOT NULL\n);\n\nCREATE TABLE users_projects (\n    user_id integer NOT NULL REFERENCES users(id),\n    project_id integer NOT NULL REFERENCES projects(id),\n    PRIMARY KEY (project_id, user_id)\n);\n\nCREATE TABLE tasks (\n    id integer primary key,\n    name text NOT NULL,\n    project_id integer REFERENCES projects(id)\n);\nalter table tasks rename constraint tasks_project_id_fkey to project;\n\nCREATE OR REPLACE VIEW filtered_tasks AS\nSELECT id AS \"myId\", name, project_id AS \"projectID\"\nFROM tasks\nWHERE project_id IN (\n\tSELECT id FROM projects WHERE id = 1\n) AND\nproject_id IN (\n\tSELECT project_id FROM users_projects WHERE user_id = 1\n);\n\nCREATE TABLE users_tasks (\n  user_id integer NOT NULL REFERENCES users(id),\n  task_id integer NOT NULL REFERENCES tasks(id),\n  primary key (task_id, user_id)\n);\n\nCREATE TABLE comments (\n    id integer primary key,\n    commenter_id integer NOT NULL,\n    user_id integer NOT NULL,\n    task_id integer NOT NULL,\n    content text NOT NULL\n);\nalter table only comments\n    add constraint \"user\" foreign key (commenter_id) references users(id),\n    add constraint comments_task_id_fkey foreign key (task_id, user_id) references users_tasks(task_id, user_id);\n\nCREATE TABLE files (\n    project_id integer NOT NULL,\n    filename text NOT NULL,\n    content text NOT NULL,\n    PRIMARY KEY (project_id, filename)\n);\n\nCREATE TABLE touched_files (\n  user_id integer NOT NULL,\n  task_id integer NOT NULL,\n  project_id integer NOT NULL,\n  filename text NOT NULL,\n  constraint fk_users_tasks foreign key (user_id, task_id) references users_tasks (user_id, task_id) on delete cascade on update cascade,\n  constraint fk_upload foreign key (project_id, filename) references files (project_id,filename) on delete cascade on update cascade,\n  primary key(user_id, task_id, project_id, filename)\n);\n\ncreate table private.articles (\n    id integer primary key,\n    body text,\n    owner name not null\n);\n\ncreate table private.article_stars (\n    article_id integer not null,\n    user_id integer not null,\n    created_at timestamp without time zone default now() not null,\n    primary key (article_id, user_id)\n);\nalter table only private.article_stars\n  add constraint article foreign key (article_id) references private.articles(id),\n  add constraint \"user\" foreign key (user_id) references test.users(id);\n\nCREATE VIEW limited_article_stars AS\n  SELECT article_id, user_id, created_at FROM private.article_stars;\n\nCREATE VIEW \"articleStars\" AS\n SELECT article_stars.article_id AS \"articleId\",\n    article_stars.user_id AS \"userId\",\n    article_stars.created_at AS \"createdAt\"\n   FROM private.article_stars;\n\nCREATE VIEW articles AS\n SELECT articles.id,\n    articles.body,\n    articles.owner\n   FROM private.articles;\n\n--\n-- Name: tsearch; Type: TABLE; Schema: test; Owner: -\n--\n\nCREATE TABLE tsearch (\n    text_search_vector tsvector\n);\n\nCREATE TABLE \"Escap3e;\" (\n\t\t\"so6meIdColumn\" integer primary key\n);\n\nCREATE TABLE \"ghostBusters\" (\n\t\t\"escapeId\" integer not null references \"Escap3e;\"(\"so6meIdColumn\")\n);\n\nCREATE TABLE \"withUnique\" (\n    uni text UNIQUE,\n    extra text\n);\n\nCREATE TABLE clashing_column (\n    t text\n);\n\n--\n-- Name: id; Type: DEFAULT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_incrementing_pk_id_seq'::regclass);\n\n\n--\n-- Name: id; Type: DEFAULT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);\n\n\nSET search_path = postgrest, pg_catalog;\n\n--\n-- Name: auth_pkey; Type: CONSTRAINT; Schema: postgrest; Owner: -\n--\n\nALTER TABLE ONLY auth\n    ADD CONSTRAINT auth_pkey PRIMARY KEY (id);\n\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY authors_only\n    ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);\n\n\n--\n-- Name: auto_incrementing_pk_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY auto_incrementing_pk\n    ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id);\n\n--\n-- Name: complex_items_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY complex_items\n    ADD CONSTRAINT complex_items_pkey PRIMARY KEY (id);\n\n--\n-- Name: has_fk_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY has_fk\n    ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id);\n\n--\n-- Name: menagerie_pkey; Type: CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY menagerie\n    ADD CONSTRAINT menagerie_pkey PRIMARY KEY (\"integer\");\n\n\nSET search_path = postgrest, pg_catalog;\n\n--\n-- Name: ensure_auth_role_exists; Type: TRIGGER; Schema: postgrest; Owner: -\n--\n\nCREATE CONSTRAINT TRIGGER ensure_auth_role_exists AFTER INSERT OR UPDATE ON auth NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE check_role_exists();\n\n\nSET search_path = private, pg_catalog;\n\n--\n-- Name: articles_owner_track; Type: TRIGGER; Schema: private; Owner: -\n--\n\nCREATE TRIGGER articles_owner_track BEFORE INSERT OR UPDATE ON articles FOR EACH ROW EXECUTE PROCEDURE postgrest.update_owner();\n\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: secrets_owner_track; Type: TRIGGER; Schema: test; Owner: -\n--\n\nCREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();\n\nSET search_path = test, pg_catalog;\n\n--\n-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY has_fk\n    ADD CONSTRAINT has_fk_fk_fkey FOREIGN KEY (auto_inc_fk) REFERENCES auto_incrementing_pk(id);\n\n\n--\n-- Name: has_fk_simple_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -\n--\n\nALTER TABLE ONLY has_fk\n    ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);\n\ncreate table addresses (\n\tid                   int not null unique,\n\taddress              text not null\n);\n\ncreate table orders (\n\tid                   int not null unique,\n\tname                 text not null,\n\tbilling_address_id   int references addresses(id),\n\tshipping_address_id  int references addresses(id)\n);\nalter table orders rename constraint orders_billing_address_id_fkey to billing;\nalter table orders rename constraint orders_shipping_address_id_fkey to shipping;\n\nCREATE FUNCTION getproject(id int) RETURNS SETOF projects\n    LANGUAGE sql\n    AS $_$\n    SELECT * FROM test.projects WHERE id = $1;\n$_$;\n\nCREATE FUNCTION get_projects_below(id int) RETURNS SETOF projects\n    LANGUAGE sql\n    AS $_$\n    SELECT * FROM test.projects WHERE id < $1;\n$_$;\n\nCREATE FUNCTION get_projects_above(id int) RETURNS SETOF projects\n    LANGUAGE sql\n    AS $_$\n    SELECT * FROM test.projects WHERE id > $1;\n$_$ ROWS 1;\n\nCREATE FUNCTION getallprojects() RETURNS SETOF projects\n    LANGUAGE sql\n    AS $_$\n    SELECT * FROM test.projects;\n$_$ ROWS 2019;\n\nCREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF projects\n    LANGUAGE sql\n    AS $_$\n    update test.projects set name = $3 WHERE id >= $1 AND id <= $2 returning *;\n$_$;\n\nCREATE DOMAIN projects_domain AS projects;\n\nCREATE FUNCTION getproject_domain(id int) RETURNS SETOF projects_domain\n    LANGUAGE sql\n    STABLE\n    AS $_$\n    SELECT projects::projects_domain FROM test.projects WHERE id = $1;\n$_$;\n\ncreate table images (\n\tname text  not null,\n\timg  bytea not null\n);\n\ncreate view images_base64 as (\n  -- encoding in base64 puts a '\\n' after every 76 character due to legacy reasons, this is isn't necessary here so it's removed\n  select name, replace(encode(img, 'base64'), E'\\n', '') as img from images\n);\n\ncreate function test.ret_enum(val text) returns test.enum_menagerie_type as $$\n  select val::test.enum_menagerie_type;\n$$ language sql;\n\ncreate domain one_nine as integer check (value >= 1 and value <= 9);\n\ncreate function test.ret_array() returns integer[] as $$\n  select '{1,2,3}'::integer[];\n$$ language sql;\n\ncreate function test.ret_domain(val integer) returns test.one_nine as $$\n  select val::test.one_nine;\n$$ language sql;\n\ncreate function test.ret_range(low integer, up integer) returns int4range as $$\n  select int4range(low, up);\n$$ language sql;\n\ncreate function test.ret_setof_integers() returns setof integer as $$\n  values (1), (2), (3);\n$$ language sql;\n\n-- this function does not have named arguments and should be ignored\n-- if it's not ignored, it will break the test for the function before\ncreate function test.ret_setof_integers(int, int) returns integer AS $$\n  values (1);\n$$ language sql;\n\ncreate function test.ret_scalars() returns table(\n  a text, b test.enum_menagerie_type, c test.one_nine, d int4range\n) as $$\n  select row('scalars'::text, enum_first(null::test.enum_menagerie_type),\n              1::test.one_nine, int4range(10, 20));\n$$ language sql;\n\ncreate type test.point_2d as (x integer, y integer);\n\ncreate function test.ret_point_2d() returns test.point_2d as $$\n  select row(10, 5)::test.point_2d;\n$$ language sql;\n\ncreate function test.ret_point_overloaded(x int, y int) returns test.point_2d as $$\n  select row(x, y)::test.point_2d;\n$$ language sql;\n\ncreate function test.ret_point_overloaded(x json) returns json as $$\n  select $1;\n$$ language sql;\n\ncreate domain test.composite_domain as test.point_2d;\n\ncreate function test.ret_composite_domain() returns test.composite_domain as $$\n  select row(10, 5)::test.composite_domain;\n$$ language sql;\n\ncreate type private.point_3d as (x integer, y integer, z integer);\n\ncreate function test.ret_point_3d() returns private.point_3d as $$\n  select row(7, -3, 4)::private.point_3d;\n$$ language sql;\n\ncreate function test.ret_void() returns void as '' language sql;\n\ncreate or replace function test.ret_null() returns int as $$\n  select null::int;\n$$ language sql;\n\ncreate function test.ret_image() returns \"image/png\" as $$\n  select i.img::\"image/png\" from test.images i where i.name = 'A.png';\n$$ language sql;\n\ncreate function test.single_article(id integer) returns test.articles as $$\n  select a.* from test.articles a where a.id = $1;\n$$ language sql;\n\ncreate function test.get_guc_value(name text) returns text as $$\n  select nullif(current_setting(name), '')::text;\n$$ language sql;\n\n-- Get the JSON type GUC values\ncreate function test.get_guc_value(prefix text, name text) returns text as $$\nselect nullif(current_setting(prefix)::json->>name, '')::text;\n$$ language sql;\n\ncreate table w_or_wo_comma_names ( name text );\n\ncreate table items_with_different_col_types (\n  int_data integer,\n  text_data text,\n  bool_data bool,\n  bin_data bytea,\n  char_data character varying,\n  date_data date,\n  real_data real,\n  time_data time\n);\n\n-- Tables used for testing complex boolean logic with and/or query params\n\ncreate table entities (\n  id integer primary key,\n  name text,\n  arr integer[],\n  text_search_vector tsvector\n);\n\ncreate table child_entities (\n  id integer primary key,\n  name text,\n  parent_id integer references entities(id)\n);\n\ncreate view child_entities_view as table child_entities;\n\ncreate table grandchild_entities (\n  id integer primary key,\n  name text,\n  parent_id integer references child_entities(id),\n  or_starting_col text,\n  and_starting_col text,\n  jsonb_col jsonb\n);\n\n-- Table used for testing range operators\n\ncreate table ranges (\n    id integer primary key,\n    range numrange\n);\n\n\n-- OpenAPI description tests\n\ncomment on table child_entities is 'child_entities comment';\ncomment on column child_entities.id is 'child_entities id comment';\ncomment on column child_entities.name is 'child_entities name comment. Can be longer than sixty-three characters long';\n\ncomment on view child_entities_view is 'child_entities_view comment';\ncomment on column child_entities_view.id is 'child_entities_view id comment';\ncomment on column child_entities_view.name is 'child_entities_view name comment. Can be longer than sixty-three characters long';\n\ncomment on table grandchild_entities is\n$$grandchild_entities summary\n\ngrandchild_entities description\nthat spans\nmultiple lines$$;\n\n-- Used for testing that having the same return column name as the proc name\n-- doesn't conflict with the required output, details in #901\ncreate function test.test() returns table(test text, value int) as $$\n  values ('hello', 1);\n$$ language sql;\n\ncreate function test.privileged_hello(name text) returns text as $$\n  select 'Privileged hello to ' || $1;\n$$ language sql;\n\ncreate function test.get_tsearch() returns setof test.tsearch AS $$\n  SELECT * FROM test.tsearch;\n$$ language sql;\n\ncreate table test.being (\n  being int primary key not null\n);\n\ncreate table test.descendant (\n  descendant int primary key not null,\n  being int references test.being(being)\n);\n\ncreate table test.part (\n  part int primary key not null\n);\n\ncreate table test.being_part (\n  being int not null references test.being(being),\n  part int not null references test.part(part),\n  primary key(being, part)\n);\n\ncreate function test.single_out_param(num int, OUT num_plus_one int) AS $$\n  select num + 1;\n$$ language sql;\n\ncreate function test.single_json_out_param(a int, b text, OUT my_json json) AS $$\n  select json_build_object('a', a, 'b', b);\n$$ language sql;\n\ncreate function test.many_out_params(OUT my_json json, OUT num int, OUT str text) AS $$\n  select '{\"a\": 1, \"b\": \"two\"}'::json, 3, 'four'::text;\n$$ language sql;\n\ncreate function test.single_inout_param(INOUT num int) AS $$\n  select num + 1;\n$$ language sql;\n\ncreate function test.many_inout_params(INOUT num int, INOUT str text, INOUT b bool DEFAULT true) AS $$\n  select num, str, b;\n$$ language sql;\n\ncreate function test.single_column_table_return () returns table (a text) AS $$\n  select 'A'::text;\n$$ language sql;\n\ncreate function test.multi_column_table_return () returns table (a text, b text) AS $$\n  select 'A'::text, 'B'::text;\n$$ language sql;\n\nCREATE FUNCTION test.variadic_param(VARIADIC v TEXT[] DEFAULT '{}') RETURNS text[]\nIMMUTABLE\nLANGUAGE SQL AS $$\n  SELECT v\n$$;\n\nCREATE FUNCTION test.sayhello_variadic(name TEXT, VARIADIC v TEXT[]) RETURNS text\nIMMUTABLE\nLANGUAGE SQL AS $$\n  SELECT 'Hello, ' || name\n$$;\n\ncreate or replace function test.raise_pt402() returns void as $$\nbegin\n  raise sqlstate 'PT402' using message = 'Payment Required',\n                               detail = 'Quota exceeded',\n                               hint = 'Upgrade your plan';\nend;\n$$ language plpgsql;\n\ncreate or replace function test.raise_bad_pt() returns void as $$\nbegin\n  raise sqlstate 'PT40A' using message = 'Wrong';\nend;\n$$ language plpgsql;\n\ncreate or replace function test.send_body_status_403() returns json as $$\nbegin\n  perform set_config('response.status', '403', true);\n  return json_build_object('message', 'invalid user or password');\nend;\n$$ language plpgsql;\n\ncreate or replace function test.send_bad_status() returns json as $$\nbegin\n  perform set_config('response.status', 'bad', true);\n  return null;\nend;\n$$ language plpgsql;\n\ncreate or replace function test.get_projects_and_guc_headers() returns setof test.projects as $$\n  set local \"response.headers\" = '[{\"X-Test\": \"key1=val1; someValue; key2=val2\"}, {\"X-Test-2\": \"key1=val1\"}]';\n  select * from test.projects;\n$$ language sql;\n\ncreate or replace function test.get_int_and_guc_headers(num int) returns integer as $$\n  set local \"response.headers\" = '[{\"X-Test\":\"key1=val1; someValue; key2=val2\"},{\"X-Test-2\":\"key1=val1\"}]';\n  select num;\n$$ language sql;\n\ncreate or replace function test.bad_guc_headers_1() returns void as $$\n  set local \"response.headers\" = '{\"X-Test\": \"invalid structure for headers\"}';\n$$ language sql;\n\ncreate or replace function test.bad_guc_headers_2() returns void as $$\n  set local \"response.headers\" = '[\"invalid\", \"structure\", \"for\", \"headers\"]';\n$$ language sql;\n\ncreate or replace function test.bad_guc_headers_3() returns void as $$\n  set local \"response.headers\" = '{\"X-Test\": \"invalid\", \"X-Test-2\": \"structure\", \"X-Test-3\": \"for headers\"}';\n$$ language sql;\n\ncreate or replace function test.set_cookie_twice() returns void as $$\n  set local \"response.headers\" = '[{\"Set-Cookie\": \"sessionid=38afes7a8; HttpOnly; Path=/\"}, {\"Set-Cookie\": \"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly\"}]';\n$$ language sql;\n\ncreate or replace function test.three_defaults(a int default 1, b int default 2, c int default 3) returns int as $$\nselect a + b + c\n$$ language sql;\n\ncreate or replace function test.overloaded() returns setof int as $$\n  values (1), (2), (3);\n$$ language sql;\n\ncreate or replace function test.overloaded(json) returns table(x int, y text) as $$\n  select * from json_to_recordset($1) as r(x int, y text);\n$$ language sql;\n\ncreate or replace function test.overloaded(a int, b int) returns int as $$\n  select a + b\n$$ language sql;\n\ncreate or replace function test.overloaded(a text, b text, c text) returns text as $$\n  select a || b || c\n$$ language sql;\n\ncreate or replace function test.overloaded_default(opt_param text default 'Code w7') returns setof test.tasks as $$\nselect * from test.tasks where name like opt_param;\n$$ language sql;\n\ncreate or replace function test.overloaded_default(must_param int) returns jsonb as $$\nselect row_to_json(r)::jsonb from (select must_param as val) as r;\n$$ language sql;\n\ncreate or replace function test.overloaded_default(a int, opt_param text default 'Design IOS') returns setof test.tasks as $$\nselect * from test.tasks where name like opt_param and id > a;\n$$ language sql;\n\ncreate or replace function test.overloaded_default(a int, must_param int) returns jsonb as $$\nselect row_to_json(r)::jsonb from (select a, must_param as val) as r;\n$$ language sql;\n\ncreate or replace function test.overloaded_html_form() returns setof int as $$\nvalues (1), (2), (3);\n$$ language sql;\n\ncreate or replace function test.overloaded_html_form(single_param json) returns json as $$\nselect single_param;\n$$ language sql;\n\ncreate or replace function test.overloaded_html_form(a int, b int) returns int as $$\nselect a + b\n$$ language sql;\n\ncreate or replace function test.overloaded_html_form(a text, b text, c text) returns text as $$\nselect a || b || c\n$$ language sql;\n\ncreate or replace function test.overloaded_same_args(arg integer) returns json as $$\nselect json_build_object(\n    'type', pg_typeof(arg),\n    'value', arg\n  );\n$$ language sql;\n\ncreate or replace function test.overloaded_same_args(arg xml) returns json as $$\nselect json_build_object(\n    'type', pg_typeof(arg),\n    'value', arg\n  );\n$$ language sql;\n\ncreate or replace function test.overloaded_same_args(arg text, num integer default 0) returns json as $$\nselect json_build_object(\n    'type', pg_typeof(arg),\n    'value', arg\n  );\n$$ language sql;\n\ncreate table test.leak(\n  id serial primary key,\n  blob bytea\n);\n\ncreate function test.leak(blob bytea) returns void as $$ begin end; $$ language plpgsql;\n\ncreate table test.perf_articles(\n  id integer not null,\n  body text not null\n);\n\ncreate table test.employees(\n  first_name text,\n  last_name text,\n  salary money,\n  company text,\n  occupation text,\n  primary key(first_name, last_name)\n);\n\ncreate table test.tiobe_pls(\n  name text primary key,\n  rank smallint\n);\n\ncreate table test.single_unique(\n  unique_key integer unique not null,\n  value text\n);\n\ncreate table test.compound_unique(\n  key1 integer not null,\n  key2 integer not null,\n  value text,\n  unique(key1, key2)\n);\n\ncreate table test.family_tree (\n  id text not null primary key,\n  name text not null,\n  parent text\n);\nalter table only test.family_tree add constraint pptr foreign key (parent) references test.family_tree(id);\n\ncreate table test.managers (\n  id integer primary key,\n  name text\n);\n\ncreate table test.organizations (\n  id integer primary key,\n  name text,\n  referee integer references organizations(id),\n  auditor integer references organizations(id),\n  manager_id integer references managers(id)\n);\nalter table only test.organizations rename constraint organizations_manager_id_fkey to manager;\n\ncreate table private.authors(\n  id integer primary key,\n  name text\n);\n\ncreate table private.publishers(\n  id integer primary key,\n  name text\n);\n\ncreate table private.books(\n  id integer primary key,\n  title text,\n  publication_year smallint,\n  author_id integer references private.authors(id),\n  first_publisher_id integer references private.publishers(id)\n);\n\ncreate view test.authors as select id, name from private.authors;\ncreate view test.publishers as select id, name from private.publishers;\n\ncreate view test.books as select id, title, publication_year, author_id, first_publisher_id from private.books;\ncreate view test.forties_books as select id, title, publication_year, author_id from private.books where publication_year >= 1940 and publication_year < 1950;\ncreate view test.fifties_books as select id, title, publication_year, author_id from private.books where publication_year >= 1950 and publication_year < 1960;\ncreate view test.sixties_books as select id, title, publication_year, author_id from private.books where publication_year >= 1960 and publication_year < 1970;\n\ncreate table person (\n  id integer primary key,\n  name character varying not null);\n\ncreate table message (\n  id integer primary key,\n  body text not null default '',\n  sender bigint not null references person(id),\n  recipient bigint not null references person(id));\n\ncreate view person_detail as\n  select p.id, p.name, s.count as sent, r.count as received\n  from person p\n  join lateral (select message.sender, count(message.id) as count from message group by message.sender) s on s.sender = p.id\n  join lateral (select message.recipient, count(message.id) as count from message group by message.recipient) r on r.recipient = p.id;\n\ncreate table space(\n  id integer primary key,\n  name text);\n\ncreate table zone(\n  id integer primary key,\n  name text,\n  zone_type_id integer,\n  space_id integer references space(id));\n\n-- foreign table tests\ncreate extension file_fdw;\n\ncreate server import_csv foreign data wrapper file_fdw;\n\ncreate foreign table projects_dump (\n  id integer,\n  name text,\n  client_id integer\n) server import_csv options ( filename '/tmp/projects_dump.csv', format 'csv');\n\ncomment on foreign table projects_dump is\n$$A temporary projects dump\n\nJust a test for foreign tables$$;\n\ncreate table \"UnitTest\"(\n  \"idUnitTest\" integer primary key,\n  \"nameUnitTest\" text\n);\n\ncreate table json_arr(\n  id integer primary key,\n  data json\n);\n\ncreate table jsonb_test(\n  id integer primary key,\n  data jsonb\n);\n\ncreate view test.authors_books_number as\nselect\n  id,\n  name,\n  (\n    select\n      count(*)\n    from forties_books where author_id = authors.id\n  ) as num_in_forties,\n  (\n    select\n      count(*)\n    from fifties_books where author_id = authors.id\n  ) as num_in_fifties,\n  (\n    select\n      count(*)\n    from sixties_books where author_id = authors.id\n  ) as num_in_sixties,\n  (\n    select\n      count(*)\n    from (\n      select id\n      from forties_books where author_id = authors.id\n      union\n      select id\n      from fifties_books where author_id = authors.id\n      union\n      select id\n      from sixties_books where author_id = authors.id\n    ) _\n  ) as num_in_all_decades\nfrom private.authors;\n\ncreate view test.authors_have_book_in_decade as\nselect\n  id,\n  name,\n  case\n    when (x.id in (select author_id from test.forties_books))\n    then true\n    else false\n  end as has_book_in_forties,\n  case\n    when (x.id in (select author_id from test.fifties_books))\n    then true\n    else false\n  end as has_book_in_fifties,\n  case\n    when (x.id in (select author_id from test.sixties_books))\n    then true\n    else false\n  end as has_book_in_sixties\nfrom private.authors x;\n\ncreate view test.authors_have_book_in_decade2 as\nselect\n  id,\n  name,\n  coalesce(\n    (select true from test.forties_books where author_id=x.id limit 1),\n    false\n  ) as has_book_in_forties,\n  coalesce(\n    (select true from test.fifties_books where author_id=x.id limit 1),\n    false\n  ) as has_book_in_fifties,\n  coalesce(\n    (select true from test.sixties_books where author_id=x.id limit 1),\n    false\n  ) as has_book_in_sixties\nfrom private.authors x;\n\ncreate view test.forties_and_fifties_books as\nselect x.id, x.title, x.publication_year, y.name as first_publisher, x.author_id\nfrom (\n  select id, title, publication_year, author_id, first_publisher_id from private.books\n  where publication_year >= 1940 and publication_year < 1960) x\njoin private.publishers y on y.id = x.first_publisher_id;\n\ncreate view test.odd_years_publications as\nwith\nodd_years_books as(\n  select id, title, publication_year, author_id, first_publisher_id\n  from private.books\n  where publication_year % 2 <> 0\n)\nselect\n  x.id, x.title, x.publication_year,\n  y.name as first_publisher, x.author_id\nfrom odd_years_books x\njoin private.publishers y on y.id = x.first_publisher_id;\n\ncreate view test.projects_count_grouped_by as\nselect\n  client_id,\n  count(id) as number_of_projects\nfrom projects\ngroup by client_id;\n\ncreate view test.authors_w_entities as\nselect\n  id,\n  name,\n  (\n    select json_agg(id)\n    from test.entities\n    where id not in (\n      select parent_id from test.child_entities\n    )\n  ) as entities\nfrom private.authors;\n\nCREATE TABLE test.\"Foo\"(\n  id int primary key,\n  name text\n);\n\nCREATE TABLE test.bar(\n  id int primary key,\n  name text,\n  \"fooId\" int references \"Foo\"(id)\n);\n\nCREATE VIEW test.foos as select id,name from \"Foo\";\nCREATE VIEW test.bars as select id, \"fooId\", name from bar;\n\ncreate materialized view materialized_projects as\nselect id, name, client_id from projects;\n\ncomment on materialized view materialized_projects is\n$$A materialized view for projects\n\nJust a test for materialized views$$;\n\n-- Tests for updatable, insertable and deletable views\ncreate view test.projects_auto_updatable_view_with_pk as\nselect id, name, client_id from test.projects;\n\ncreate view test.projects_auto_updatable_view_without_pk as\nselect name, client_id from test.projects;\n\ncreate view test.projects_view_without_triggers as\nselect distinct id, name, client_id from test.projects;\n\ncreate or replace function test.test_for_views_with_triggers() returns trigger as $$\nbegin\n    return null;\nend;\n$$ language plpgsql;\n\ncreate view test.projects_view_with_all_triggers_with_pk as\nselect distinct id, name, client_id from test.projects;\n\ncreate trigger projects_view_with_all_triggers_with_pk_insert\n    instead of insert on test.projects_view_with_all_triggers_with_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate trigger projects_view_with_all_triggers_with_pk_update\n    instead of update on test.projects_view_with_all_triggers_with_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate trigger projects_view_with_all_triggers_with_pk_delete\n    instead of delete on test.projects_view_with_all_triggers_with_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate view test.projects_view_with_all_triggers_without_pk as\nselect distinct name, client_id from test.projects;\n\ncreate trigger projects_view_with_all_triggers_without_pk_insert\n    instead of insert on test.projects_view_with_all_triggers_without_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate trigger projects_view_with_all_triggers_without_pk_update\n    instead of update on test.projects_view_with_all_triggers_without_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate trigger projects_view_with_all_triggers_without_pk_delete\n    instead of delete on test.projects_view_with_all_triggers_without_pk\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate view test.projects_view_with_insert_trigger as\nselect distinct id, name, client_id from test.projects;\n\ncreate trigger projects_view_with_insert_trigger_insert\n    instead of insert on test.projects_view_with_insert_trigger\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate view test.projects_view_with_update_trigger as\nselect distinct id, name, client_id from test.projects;\n\ncreate trigger projects_view_with_update_trigger_update\n    instead of update on test.projects_view_with_update_trigger\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate view test.projects_view_with_delete_trigger as\nselect distinct id, name, client_id from test.projects;\n\ncreate trigger projects_view_with_delete_trigger_delete\n    instead of delete on test.projects_view_with_delete_trigger\n    for each row execute procedure test_for_views_with_triggers();\n\ncreate or replace function test.\"quotedFunction\"(\"user\" text, \"fullName\" text, \"SSN\" text)\nreturns jsonb AS $$\n  select format('{\"user\": \"%s\", \"fullName\": \"%s\", \"SSN\": \"%s\"}', \"user\", \"fullName\", \"SSN\")::jsonb;\n$$ language sql;\n\ncreate table private.player (\n  id integer not null,\n  first_name text not null,\n  last_name text not null,\n  birth_date date,\n  primary key (last_name, id, first_name, birth_date) -- just for testing a long compound pk\n);\n\ncreate table test.contract (\n  tournament text not null,\n  time tsrange not null,\n  purchase_price int not null,\n  id integer not null,\n  first_name text not null,\n  last_name text not null,\n  birth_date date,\n  foreign key (last_name, id, first_name, birth_date) references private.player\n);\n\ncreate view test.player_view as select * from private.player;\n\ncreate view test.contract_view as select * from test.contract;\n\ncreate type public.my_type AS enum ('something');\n\ncreate function test.test_arg(my_arg public.my_type) returns text as $$\n  select 'foobar'::text;\n$$ language sql;\n\ncreate extension if not exists ltree with schema public;\n\ncreate table test.ltree_sample (\n  path public.ltree\n);\n\nCREATE FUNCTION test.number_of_labels(test.ltree_sample) RETURNS integer AS $$\n  SELECT nlevel($1.path)\n$$ language sql;\n\ncreate extension if not exists isn with schema extensions;\n\ncreate table test.isn_sample (\n  id extensions.isbn,\n  name text\n);\n\ncreate function test.is_valid_isbn(input text) returns boolean as $$\n  select is_valid(input::isbn);\n$$ language sql;\n\ncreate table \"Server Today\"(\n  \"cHostname\" text,\n  \"Just A Server Model\" text\n);\n\ncreate table test.pgrst_reserved_chars (\n  \"*id*\" integer,\n  \":arr->ow::cast\" text,\n  \"(inside,parens)\" text,\n  \"a.dotted.column\" text,\n  \"  col  w  space  \" text\n);\n\nCREATE TABLE test.openapi_types(\n  \"a_character_varying\" character varying,\n  \"a_character\" character(1),\n  \"a_text\" text,\n  \"a_boolean\" boolean,\n  \"a_smallint\" smallint,\n  \"a_integer\" integer,\n  \"a_bigint\" bigint,\n  \"a_numeric\" numeric,\n  \"a_real\" real,\n  \"a_double_precision\" double precision,\n  \"a_json\" json,\n  \"a_jsonb\" jsonb,\n  \"a_text_arr\" text[],\n  \"a_int_arr\" int[],\n  \"a_bool_arr\" boolean[],\n  \"a_char_arr\" char[],\n  \"a_varchar_arr\" varchar[],\n  \"a_bigint_arr\" bigint[],\n  \"a_numeric_arr\" numeric[],\n  \"a_json_arr\" json[],\n  \"a_jsonb_arr\" jsonb[]\n);\n\nCREATE TABLE test.openapi_defaults(\n  \"text\" text default 'default',\n  \"boolean\" boolean default false,\n  \"integer\" integer default 42,\n  \"numeric\" numeric default 42.2,\n  \"date\" date default '1900-01-01'::date,\n  \"time\" time default '13:00:00'::time without time zone\n);\n\ncreate function add_them(a integer, b integer)\nreturns integer as $$\n  select a + b;\n$$ language sql;\n\ncreate or replace function root() returns \"application/openapi+json\" as $_$\ndeclare\nopenapi json = $$\n  {\n    \"swagger\": \"2.0\",\n    \"info\":{\n      \"title\":\"PostgREST API\",\n      \"description\":\"This is a dynamic API generated by PostgREST\"\n    }\n  }\n$$;\nbegin\n  return openapi;\nend\n$_$ language plpgsql;\n\ncreate or replace function welcome() returns \"text/plain\" as $$\nselect 'Welcome to PostgREST'::\"text/plain\";\n$$ language sql;\n\ncreate or replace function welcome_twice() returns setof \"text/plain\" as $$\nselect 'Welcome to PostgREST'\nunion all\nselect 'Welcome to PostgREST';\n$$ language sql;\n\ncreate or replace function \"welcome.html\"() returns \"text/html\" as $_$\nselect $$\n<html>\n  <head>\n    <title>PostgREST</title>\n  </head>\n  <body>\n    <h1>Welcome to PostgREST</h1>\n  </body>\n</html>\n$$::\"text/html\";\n$_$ language sql;\n\ncreate view getallprojects_view as\nselect * from getallprojects();\n\ncreate view get_projects_above_view as\nselect * from get_projects_above(1);\n\nCREATE TABLE web_content (\n  id integer,\n  name text,\n  p_web_id integer references web_content(id),\n  primary key (id)\n);\n\nCREATE FUNCTION getallusers() RETURNS SETOF users AS $$\n  SELECT * FROM test.users;\n$$ LANGUAGE sql STABLE;\n\ncreate table app_users (\n  id       integer    primary key,\n  email    text       unique not null,\n  password text       not null\n);\n\ncreate table private.pages (\n  link int not null unique\n, url text\n);\n\ncreate table private.referrals (\n  site text\n, link int references private.pages(link) not null\n);\n\ncreate view test.pages as select * from private.pages;\n\ncreate view test.referrals as select * from private.referrals;\n\ncreate table big_projects (\n  big_project_id  serial  primary key,\n  name text\n);\n\ncreate table sites (\n  site_id         serial  primary key\n, name text\n, main_project_id int     null references big_projects (big_project_id)\n);\nalter table sites rename constraint sites_main_project_id_fkey to main_project;\n\ncreate table jobs (\n  job_id          uuid\n, name text\n, site_id         int     not null references sites (site_id)\n, big_project_id  int     not null references big_projects (big_project_id)\n, primary key(job_id, site_id, big_project_id)\n);\n\ncreate view main_jobs as\nselect * from jobs\nwhere site_id in (select site_id from sites where main_project_id is not null);\n\n-- junction in a private schema, just to make sure we don't leak it on resource embedding\n-- if it leaks it would show on the disambiguation error tests\ncreate view private.priv_jobs as\n  select * from jobs;\n\n-- tables to show our limitation when trying to do an m2m embed\n-- with a junction table that has more than two foreign keys\ncreate table whatev_projects (\n  id  serial  primary key,\n  name text\n);\n\ncreate table whatev_sites (\n  id              serial  primary key\n, name text\n);\n\ncreate table whatev_jobs (\n  job_id        uuid\n, name text\n, site_id_1     int not null references whatev_sites (id)\n, project_id_1  int not null references whatev_projects (id)\n, site_id_2     int not null references whatev_sites (id)\n, project_id_2  int not null references whatev_projects (id)\n, primary key(job_id, site_id_1, project_id_1, site_id_2, project_id_2)\n);\n\n-- circular reference\ncreate table agents (\n  id int primary key\n, name text\n, department_id int\n);\n\ncreate table departments (\n  id int primary key\n, name text\n, head_id int references agents(id)\n);\n\nALTER TABLE agents\n    ADD CONSTRAINT agents_department_id_fkey foreign key (department_id) REFERENCES departments(id);\n\n-- composite key disambiguation\ncreate table schedules (\n  id        int primary key\n, name      text\n, start_at  timetz\n, end_at    timetz\n);\n\ncreate table activities (\n  id             int\n, schedule_id    int\n, car_id         text\n, camera_id      text\n, primary key (id, schedule_id)\n);\nalter table activities\nadd constraint schedule foreign key          (schedule_id)\n                        references schedules (id);\n\ncreate table unit_workdays (\n  unit_id int\n, day date\n, fst_shift_activity_id int\n, fst_shift_schedule_id int\n, snd_shift_activity_id int\n, snd_shift_schedule_id int\n, primary key (unit_id, day)\n);\nalter table unit_workdays\nadd constraint fst_shift foreign key           (fst_shift_activity_id, fst_shift_schedule_id)\n                         references activities (id, schedule_id),\nadd constraint snd_shift foreign key           (snd_shift_activity_id, snd_shift_schedule_id)\n                         references activities (id, schedule_id);\n\ncreate view unit_workdays_fst_shift as\nselect unit_id, day, fst_shift_activity_id, fst_shift_schedule_id\nfrom unit_workdays\nwhere fst_shift_activity_id is not null\n  and fst_shift_schedule_id is not null;\n\n-- for a pre-request function\ncreate or replace function custom_headers() returns void as $$\ndeclare\n  user_agent text := current_setting('request.headers', true)::json->>'user-agent';\n  req_path   text := current_setting('request.path', true);\n  req_accept text := current_setting('request.headers', true)::json->>'accept';\n  req_method text := current_setting('request.method', true);\nbegin\n  if user_agent similar to 'MSIE (6.0|7.0)' then\n    perform set_config('response.headers',\n      '[{\"Cache-Control\": \"no-cache, no-store, must-revalidate\"}]', true);\n  elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then\n    perform set_config('response.headers',\n      format('[{\"Content-Disposition\": \"attachment; filename=%s.csv\"}]', trim('/' from req_path)), true);\n  elsif req_path similar to '/(clients|rpc/getallprojects)' then\n    perform set_config('response.headers',\n      '[{\"Content-Type\": \"application/custom+json\"}]', true);\n  elsif req_path = '/items' and\n        req_method similar to 'POST|PATCH|PUT|DELETE' then\n    perform set_config('response.headers',\n      '[{\"X-Custom-Header\": \"mykey=myval\"}]', true);\n  end if;\nend; $$ language plpgsql;\n\ncreate table private.stuff(\n  id integer primary key\n, name text\n);\n\ncreate view test.stuff as select * from private.stuff;\n\ncreate or replace function location_for_stuff() returns trigger as $$\nbegin\n    insert into private.stuff values (new.id, new.name);\n    if new.id is not null\n    then\n      perform set_config(\n        'response.headers'\n      , format('[{\"Location\": \"/%s?id=eq.%s&overriden=true\"}]', tg_table_name, new.id)\n      , true\n      );\n    end if;\n    return new;\nend\n$$ language plpgsql security definer;\ncreate trigger location_for_stuff instead of insert on test.stuff for each row execute procedure test.location_for_stuff();\n\ncreate or replace function status_205_for_updated_stuff() returns trigger as $$\nbegin\n    update private.stuff set id = new.id, name = new.name;\n    perform set_config('response.status' , '205' , true);\n    return new;\nend\n$$ language plpgsql security definer;\ncreate trigger status_205_for_updated_stuff instead of update on test.stuff for each row execute procedure test.status_205_for_updated_stuff();\n\ncreate table loc_test (\n  id int primary key\n, c text\n);\n\n-- tables to test multi schema access in one instance\ncreate table v1.parents (\n  id    int primary key\n, name text\n);\n\ncreate table v1.children (\n  id int primary key\n, name text\n, parent_id int\n, constraint parent foreign key(parent_id)\n  references v1.parents(id)\n);\n\ncreate function v1.get_parents_below(id int)\nreturns setof v1.parents as $$\n  select * from v1.parents where id < $1;\n$$ language sql;\n\ncreate table v2.parents (\n  id    int primary key\n, name text\n);\n\ncreate table v2.children (\n  id int primary key\n, name text\n, parent_id int\n, constraint parent foreign key(parent_id)\n  references v2.parents(id)\n);\n\ncreate table v2.another_table (\n  id            int primary key\n, another_value text\n);\n\ncreate function v2.get_parents_below(id int)\nreturns setof v2.parents as $$\n  select * from v2.parents where id < $1;\n$$ language sql;\n\ncreate function v2.get_plain_text()\nreturns test.\"text/plain\" as $$\n  select 'plain'::test.\"text/plain\";\n$$ language sql;\n\ncreate domain v2.\"text/special\" as text;\n\ncreate function v2.get_special_text()\nreturns v2.\"text/special\" as $$\n  select 'special'::v2.\"text/special\";\n$$ language sql;\n\ncreate or replace function v2.special_trans (state v2.\"text/special\", next v2.another_table)\nreturns v2.\"text/special\" as $$\n  select 'special'::v2.\"text/special\";\n$$ language sql;\n\ndrop aggregate if exists v2.special_agg(v2.another_table);\ncreate aggregate v2.special_agg (v2.another_table) (\n  initcond = ''\n, stype = v2.\"text/special\"\n, sfunc = v2.special_trans\n);\n\ncreate or replace function v2.plain_trans (state test.\"text/plain\", next v2.another_table)\nreturns test.\"text/plain\" as $$\n  select 'plain'::test.\"text/plain\";\n$$ language sql;\n\ndrop aggregate if exists v2.plain_agg(v2.another_table);\ncreate aggregate v2.plain_agg (v2.another_table) (\n  initcond = ''\n, stype = test.\"text/plain\"\n, sfunc = v2.plain_trans\n);\n\ncreate table private.screens (\n  id serial primary key,\n  name text not null default 'new screen'\n);\n\ncreate table private.labels (\n  id serial primary key,\n  name text not null default 'new label'\n);\n\ncreate table private.label_screen (\n  label_id int not null references private.labels(id) on update cascade on delete cascade,\n  screen_id int not null references private.screens(id) on update cascade on delete cascade,\n  constraint label_screen_pkey primary key (label_id, screen_id)\n);\n\ncreate view test.labels as\nselect * from private.labels;\n\ncreate view test.screens as\nselect * from private.screens;\n\ncreate view test.label_screen as\nselect * from private.label_screen;\n\ncreate table private.actors (\n  id int,\n  name text,\n  constraint actors_id primary key (id)\n);\n\ncreate table private.films (\n  id integer,\n  title text,\n  constraint films_id primary key (id)\n);\n\ncreate table private.personnages (\n  film_id int not null,\n  role_id int not null,\n  character text not null,\n  constraint personnages_film_id_role_id primary key (film_id, role_id),\n  constraint personnages_film_id_fkey foreign key (film_id) references private.films(id) not deferrable,\n  constraint personnages_role_id_fkey foreign key (role_id) references private.actors(id) not deferrable\n);\n\ncreate view test.actors as\nselect * from private.actors;\n\ncreate view test.films as\nselect * from private.films;\n\ncreate view test.personnages as\nselect * from private.personnages;\n\ncreate table test.end_1(\n  id int primary key,\n  name text\n);\n\ncreate table test.end_2(\n  id int primary key,\n  name text\n);\n\ncreate table private.junction(\n  end_1_id int not null references test.end_1(id) on update cascade on delete cascade,\n  end_2_id int not null references test.end_2(id) on update cascade on delete cascade,\n  primary key (end_1_id, end_2_id)\n);\n\ncreate table test.schauspieler (\n  id int primary key,\n  name text\n);\n\ncreate table test.filme (\n  id int primary key,\n  titel text\n);\n\ncreate table test.rollen ();\n\ncreate table private.rollen (\n  film_id int not null,\n  rolle_id int not null,\n  charakter text not null,\n  primary key (film_id, rolle_id),\n  foreign key (film_id) references test.filme(id),\n  foreign key (rolle_id) references test.schauspieler(id)\n);\n\n-- Tables used for testing embedding between partitioned tables\ncreate table test.car_models(\n  name varchar(64) not null,\n  year int not null\n) partition by list (year);\n\ncomment on table test.car_models is\n$$A partitioned table\n\nA test for partitioned tables$$;\n\ncreate table test.car_models_2021 partition of test.car_models\n  for values in (2021);\ncreate table test.car_models_default partition of test.car_models\n  for values in (1981,1997,2001,2013);\n\ncreate table test.car_brands (\n  name varchar(64) primary key\n);\n\nalter table test.car_models add primary key (name, year);\nalter table test.car_models add column car_brand_name varchar(64) references test.car_brands(name);\n\ncreate table test.car_model_sales(\n  date varchar(64) not null,\n  quantity int not null,\n  car_model_name varchar(64),\n  car_model_year int,\n  primary key (date, car_model_name, car_model_year),\n  foreign key (car_model_name, car_model_year) references test.car_models (name, year)\n) partition by range (date);\n\ncreate table test.car_model_sales_202101 partition of test.car_model_sales\n  for values from ('2021-01-01') to ('2021-01-31');\n\ncreate table test.car_model_sales_default partition of test.car_model_sales\n  default;\n\ncreate table test.car_racers (\n  name varchar(64) not null primary key,\n  car_model_name varchar(64),\n  car_model_year int,\n  foreign key (car_model_name, car_model_year) references test.car_models (name, year)\n);\n\ncreate table test.car_dealers (\n  name varchar(64) not null,\n  city varchar(64) not null,\n  primary key (name, city)\n) partition by list (city);\n\ncreate table test.car_dealers_springfield partition of test.car_dealers\n  for values in ('Springfield');\n\ncreate table test.car_dealers_default partition of test.car_dealers\n  default;\n\ncreate table test.car_models_car_dealers (\n  car_model_name varchar(64) not null,\n  car_model_year int not null,\n  car_dealer_name varchar(64) not null,\n  car_dealer_city varchar(64) not null,\n  quantity int not null,\n  foreign key (car_model_name, car_model_year) references test.car_models (name, year),\n  foreign key (car_dealer_name, car_dealer_city) references test.car_dealers (name, city),\n  primary key (car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity)\n) partition by range (quantity);\n\ncreate table test.car_models_car_dealers_10to20 partition of test.car_models_car_dealers\n  for values from (10) to (20);\n\ncreate table test.car_models_car_dealers_default partition of test.car_models_car_dealers\n  default;\n\ncreate or replace function test.unnamed_json_param(json) returns json as $$\n  select $1;\n$$ language sql;\n\n-- Function with a NAMED json parameter (for testing single param fallback behavior)\ncreate or replace function test.named_json_param(data json) returns json as $$\n  select data;\n$$ language sql;\n\ncreate or replace function test.unnamed_text_param(text) returns \"text/plain\" as $$\n  select $1::\"text/plain\";\n$$ language sql;\n\ncreate or replace function test.unnamed_xml_param(pg_catalog.xml) returns \"text/xml\" as $$\n  select $1::\"text/xml\";\n$$ language sql;\n\ncreate or replace function test.unnamed_bytea_param(bytea) returns \"application/octet-stream\" as $$\n  select $1::\"application/octet-stream\";\n$$ language sql;\n\ncreate or replace function test.unnamed_int_param(int) returns int as $$\n  select $1;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_param(json) returns json as $$\n  select $1;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_param(bytea) returns \"application/octet-stream\" as $$\nselect $1::\"application/octet-stream\";\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_param(text) returns \"text/plain\" as $$\nselect $1::\"text/plain\";\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_param() returns int as $$\nselect 1;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$\n  select x + y;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_json_jsonb_param(json) returns json as $$\nselect $1;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_json_jsonb_param(jsonb) returns jsonb as $$\nselect $1;\n$$ language sql;\n\ncreate or replace function test.overloaded_unnamed_json_jsonb_param(x int, y int) returns int as $$\nselect x + y;\n$$ language sql;\n\ncreate table products(\n  id int primary key\n, name text\n);\n\ncreate table suppliers(\n  id int primary key\n, name text\n);\n\ncreate table products_suppliers(\n  product_id int references products(id),\n  supplier_id int references suppliers(id),\n  primary key (product_id, supplier_id)\n);\n\ncreate table trade_unions(\n  id int primary key\n, name text\n);\n\ncreate table suppliers_trade_unions(\n  supplier_id int references suppliers(id),\n  trade_union_id int references trade_unions(id),\n  primary key (supplier_id, trade_union_id)\n);\n\n\nCREATE TABLE client (\n  id int primary key\n, name text\n);\n\nCREATE TABLE contact (\n  id int primary key\n, name text\n, clientid int references client(id)\n);\n\nCREATE TABLE clientinfo (\n  id serial primary key\n, clientid int references client(id)\n, other text\n);\n\nCREATE TABLE chores (\n  id int primary key\n, name text\n, done bool\n);\n\nCREATE TABLE deferrable_unique_constraint (\n  col INT UNIQUE DEFERRABLE INITIALLY IMMEDIATE\n);\n\nCREATE FUNCTION raise_constraint(deferred BOOL DEFAULT FALSE) RETURNS void\nLANGUAGE plpgsql AS $$\nBEGIN\n  IF deferred THEN\n    SET CONSTRAINTS ALL DEFERRED;\n  END IF;\n\n  INSERT INTO deferrable_unique_constraint VALUES (1), (1);\nEND$$;\n\n-- This view is not used in any requests but just parsed by the pfkSourceColumns query.\nCREATE VIEW test.xml AS\nSELECT *\n  FROM (SELECT ''::xml AS data) _,\n       XMLTABLE(\n         ''\n         PASSING data\n         COLUMNS id int PATH '@id',\n                 premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'\n       );\n\n-- https://github.com/PostgREST/postgrest/issues/1543\nCREATE TYPE complex AS (\n r double precision,\n i double precision\n);\n\nCREATE TABLE test.fav_numbers (\n num    complex,\n person text\n);\n\n-- https://github.com/PostgREST/postgrest/issues/2075\ncreate table test.arrays (\n  id int primary key,\n  numbers int[],\n  numbers_mult int[][]\n);\n\n-- This procedure is to confirm that procedures don't show up in the OpenAPI output right now.\n-- Procedures are not supported, yet.\nCREATE PROCEDURE test.unsupported_proc ()\nLANGUAGE SQL AS '';\n\nCREATE FUNCTION public.dummy(int) RETURNS int\nLANGUAGE SQL AS $$ SELECT 1 $$;\n\n-- This aggregate is to confirm that aggregates don't show up in the OpenAPI output.\nCREATE AGGREGATE test.unsupported_agg (*) (\n  SFUNC = public.dummy,\n  STYPE = int\n);\n\ncreate function reset_table(tbl_name text default '', tbl_data json default '[]') returns void as $_$ begin\n  execute format(\n  $$\n    delete from %I where true; -- WHERE is required for pg-safeupdate tests\n    insert into %I\n    select * from json_populate_recordset(null::%I, $1);\n  $$::text,\n  tbl_name, tbl_name, tbl_name)\n  using tbl_data;\nend; $_$ language plpgsql volatile;\n\n-- tables for ensuring we generate real junctions for many-to-many relationships\ncreate table plate (\n    plate_id int primary key\n);\n\ncreate table well (\n    well_id int primary key,\n    plate_id int not null,\n    parent_well_id int,\n   CONSTRAINT well_parent_well_id_fkey\n      FOREIGN KEY(parent_well_id)\n\t  REFERENCES well(well_id),\n   CONSTRAINT well_plate_id_fkey\n      FOREIGN KEY(plate_id)\n\t  REFERENCES plate(plate_id)\n);\n\ncreate table plate_plan_step (\n    plate_plan_step_id int primary key,\n    from_well_id int,\n    to_well_id int,\n    to_plate_id int,\n   CONSTRAINT plate_plan_step_from_well_id_fkey\n      FOREIGN KEY(from_well_id)\n\t  REFERENCES well(well_id),\n   CONSTRAINT plate_plan_step_to_plate_id_fkey\n      FOREIGN KEY(to_plate_id)\n\t  REFERENCES plate(plate_id),\n   CONSTRAINT plate_plan_step_to_well_id_fkey\n      FOREIGN KEY(to_well_id)\n\t  REFERENCES well(well_id)\n);\n\nCREATE FUNCTION test.return_scalar_xml() RETURNS \"text/xml\"\nLANGUAGE sql AS $$\n  SELECT '<my-xml-tag/>'::\"text/xml\"\n$$;\n\nCREATE OR REPLACE FUNCTION \"welcome.xml\"() RETURNS \"text/xml\"\nLANGUAGE sql AS $_$\nselect $$\n<html>\n  <head>\n    <title>PostgREST</title>\n  </head>\n  <body>\n    <h1>Welcome to PostgREST</h1>\n  </body>\n</html>$$::\"text/xml\";\n$_$;\n\nCREATE TABLE test.xmltest (\n    id integer primary key,\n    xml pg_catalog.xml NOT NULL\n);\n\ncreate or replace function test.xml_handler_transition (state \"text/xml\", next test.xmltest)\nreturns \"text/xml\" as $$\n  select xmlconcat2(state, next.xml)::\"text/xml\";\n$$ language sql;\n\ncreate or replace function test.xml_handler_final (data \"text/xml\")\nreturns \"text/xml\" as $$\n  select data;\n$$ language sql;\n\ndrop aggregate if exists test.text_xml_agg(test.xmltest);\ncreate aggregate test.text_xml_agg (test.xmltest) (\n  stype = \"text/xml\"\n, sfunc = test.xml_handler_transition\n, finalfunc = test.xml_handler_final\n);\n\nCREATE TABLE oid_test(\n  id int,\n  oid_col oid,\n  oid_array_col oid[]);\n\nCREATE TABLE private.internal_job\n(\n    id integer NOT NULL,\n    parent_id integer,\n    CONSTRAINT internal_job_pkey PRIMARY KEY (id),\n    CONSTRAINT parent_fk FOREIGN KEY (parent_id) REFERENCES private.internal_job (id)\n);\n\nCREATE VIEW test.job AS\nSELECT j.id, j.parent_id\nFROM private.internal_job j;\n\n-- https://github.com/PostgREST/postgrest/issues/2238\nCREATE TABLE series (\n    id bigint PRIMARY KEY,\n    title text NOT NULL\n);\n\nCREATE TABLE adaptation_notifications (\n    id bigint PRIMARY KEY,\n    series bigint REFERENCES series(id),\n    status text\n);\n\nCREATE VIEW series_popularity AS\nSELECT id, random() AS popularity_score\nFROM series;\n\n-- https://github.com/PostgREST/postgrest/issues/1643\nCREATE TABLE test.test (\n  id BIGINT NOT NULL PRIMARY KEY,\n  parent_id BIGINT CONSTRAINT parent_test REFERENCES test(id)\n);\n\nCREATE OR REPLACE VIEW test.view_test AS\n  SELECT id FROM test.test;\n\ncreate extension if not exists postgis with schema extensions;\n\ncreate table shops (\n  id        int primary key\n, address   text\n, shop_geom extensions.geometry(POINT, 4326)\n);\n\ncreate table shop_bles (\n  id         int primary key\n, name       text\n, coords     extensions.geometry(POINT, 4326)\n, range_area extensions.geometry(POLYGON, 4326)\n, shop_id    int references shops(id)\n);\n\ncreate function get_shop(id int) returns shops as $$\n  select * from shops where id = $1;\n$$ language sql;\n\nCREATE TABLE \"SPECIAL \"\"@/\\#~_-\".languages(\n  id INT PRIMARY KEY,\n  name TEXT\n);\n\nCREATE TABLE \"SPECIAL \"\"@/\\#~_-\".names(\n  id INT PRIMARY KEY,\n  name TEXT,\n  language_id INT REFERENCES \"SPECIAL \"\"@/\\#~_-\".languages(id)\n);\n\nCREATE FUNCTION \"SPECIAL \"\"@/\\#~_-\".computed_languages(\"SPECIAL \"\"@/\\#~_-\".names) RETURNS SETOF \"SPECIAL \"\"@/\\#~_-\".languages ROWS 1 AS $$\n  SELECT * FROM \"SPECIAL \"\"@/\\#~_-\".languages where id = $1.language_id;\n$$ LANGUAGE sql;\n\nCREATE FUNCTION \"EXTRA \"\"@/\\#~_-\".get_val_special(val text) RETURNS text AS $$\n  SELECT val;\n$$ LANGUAGE sql;\n\nCREATE FUNCTION test.special_extended_schema(val text) RETURNS text AS $$\n  SELECT get_val_special(val);\n$$ LANGUAGE sql;\n\nCREATE TABLE do$llar$s (\n  a$num$ numeric\n);\n\n-- Tables and functions to test the pg-safeupdate library\n\nCREATE TABLE test.safe_update_items(\n  id INT PRIMARY KEY,\n  name TEXT,\n  observation TEXT\n);\n\nCREATE TABLE test.safe_delete_items(\n  id INT PRIMARY KEY,\n  name TEXT,\n  observation TEXT\n);\n\nCREATE TABLE test.unsafe_update_items(\n  id INT PRIMARY KEY,\n  name TEXT,\n  observation TEXT\n);\n\nCREATE TABLE test.unsafe_delete_items(\n  id INT PRIMARY KEY,\n  name TEXT,\n  observation TEXT\n);\n\nCREATE OR REPLACE FUNCTION test.load_safeupdate() RETURNS VOID AS $$\nBEGIN\n  LOAD 'safeupdate';\nEND; $$ LANGUAGE plpgsql SECURITY DEFINER;\n\n-- This tests data representations over computed joins: even a lower case title should come back title cased.\nDROP DOMAIN IF EXISTS public.titlecasetext CASCADE;\nCREATE DOMAIN public.titlecasetext AS text;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.titlecasetext) RETURNS json AS $$\n  SELECT to_json(INITCAP($1::text));\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE CAST (public.titlecasetext AS json) WITH FUNCTION \"json\"(public.titlecasetext) AS IMPLICIT;\n-- End of data representations specific stuff except for where the domain is used in the table.\n\nCREATE TABLE designers (\n  id int primary key\n, name public.titlecasetext\n);\n\nCREATE TABLE videogames (\n  id int primary key\n, name text\n, designer_id int references designers(id)\n);\n\n-- computed relationships\nCREATE FUNCTION test.computed_designers(test.videogames) RETURNS SETOF test.designers AS $$\n  SELECT * FROM test.designers WHERE id = $1.designer_id;\n$$ LANGUAGE sql STABLE ROWS 1;\n\nCREATE FUNCTION test.computed_designers_noset(test.videogames) RETURNS test.designers AS $$\n  SELECT * FROM test.designers WHERE id = $1.designer_id;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.computed_videogames(test.designers) RETURNS SETOF test.videogames AS $$\n  SELECT * FROM test.videogames WHERE designer_id = $1.id;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.getallvideogames() RETURNS SETOF test.videogames AS $$\n  SELECT * FROM test.videogames;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.getalldesigners() RETURNS SETOF test.designers AS $$\n  SELECT * FROM test.designers;\n$$ LANGUAGE sql STABLE;\n\n-- self join for computed relationships\nCREATE FUNCTION test.child_web_content(test.web_content) RETURNS SETOF test.web_content AS $$\n  SELECT * FROM test.web_content WHERE $1.id = p_web_id;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.parent_web_content(test.web_content) RETURNS SETOF test.web_content AS $$\n  SELECT * FROM test.web_content WHERE $1.p_web_id = id;\n$$ LANGUAGE sql STABLE ROWS 1;\n\n-- overriding computed rels that empty the results\nCREATE FUNCTION test.designers(test.videogames) RETURNS SETOF test.designers AS $$\n  SELECT * FROM test.designers WHERE FALSE;\n$$ LANGUAGE sql STABLE ROWS 1;\n\nCREATE FUNCTION test.videogames(test.designers) RETURNS SETOF test.videogames AS $$\n  SELECT * FROM test.videogames WHERE FALSE;\n$$ LANGUAGE sql STABLE;\n\nCREATE TABLE test.students(\n  id int\n, code text\n, name text\n, primary key(id, code)\n);\n\nCREATE TABLE test.students_info(\n  id int\n, code text\n, address text\n, primary key(id, code)\n, foreign key (code, id) references test.students(code, id) on delete cascade\n);\n\nCREATE TABLE test.country(\n  id int primary key\n, name text\n);\n\nCREATE TABLE test.capital(\n  id int primary key\n, name text\n, country_id int unique\n, foreign key (country_id) references test.country(id)\n);\n\nCREATE FUNCTION test.allcountries() RETURNS SETOF test.country AS $$\n  SELECT * FROM test.country;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.allcapitals() RETURNS SETOF test.capital AS $$\n  SELECT * FROM test.capital;\n$$ LANGUAGE sql STABLE;\n\ncreate view students_view as\nselect * from students;\n\ncreate view students_info_view as\nselect * from students_info;\n\ncreate table test.second (\n  id int primary key,\n  name text\n);\n\ncreate table test.first (\n  id int primary key,\n  name text,\n  second_id_1 int references test.second unique,\n  second_id_2 int references test.second unique\n);\n\ncreate table test.second_1 (\n  id int primary key,\n  name text\n);\n\ncreate table test.first_1 (\n  id int primary key,\n  name text,\n  second_id_1 int references test.second unique,\n  second_id_2 int references test.second unique\n);\n\nCREATE FUNCTION test.second_1(test.first_1) RETURNS SETOF test.second_1 AS $$\n  SELECT * FROM test.second_1 WHERE id = $1.second_id_1;\n$$ LANGUAGE sql STABLE ROWS 1;\n\nCREATE FUNCTION test.first_1(test.second_1) RETURNS SETOF test.first_1 AS $$\n  SELECT * FROM test.first_1 WHERE second_id_1 = $1.id;\n$$ LANGUAGE sql STABLE ROWS 1;\n\n\ncreate table fee (\n  fee_id int primary key\n);\ncreate table baz (\n  baz_id int primary key\n);\n\ncreate table janedoe (\n  janedoe_id int primary key,\n  baz_id int references baz(baz_id)\n);\n\ncreate table johnsmith (\n  johnsmith_id int primary key,\n  fee_id int references fee(fee_id),\n  baz_id int references baz(baz_id)\n);\n\ncreate or replace function jsbaz(fee) returns setof baz as $$\n    select b.*\n    from baz b\n    join johnsmith js on js.baz_id = b.baz_id\n    where js.fee_id = $1.fee_id\n$$ stable language sql;\n\n\n-- issue https://github.com/PostgREST/postgrest/issues/2518\ncreate table a (\n  primary key (c1, c2),\n  c1 int,\n  c2 bool\n);\n\ncreate table b (\n  c2 bool,\n  c1 int,\n  foreign key (c1, c2) references a\n);\n\ncreate view test.v1 as table test.a;\n\ncreate view test.v2 as table test.b;\n\n-- issue https://github.com/PostgREST/postgrest/issues/2458\ncreate table with_pk1 (pk1 int primary key);\ncreate table with_pk2 (pk2 int primary key);\n\ncreate view with_multiple_pks as\n  select * from with_pk1, with_pk2;\n\ncreate function with_multiple_pks_insert() returns trigger\nlanguage plpgsql as $$\nbegin\n  insert into with_pk1 values (new.pk1) on conflict do nothing;\n  insert into with_pk2 values (new.pk2) on conflict do nothing;\n  return new;\nend\n$$;\n\ncreate trigger ins instead of insert on with_multiple_pks\n  for each row execute procedure with_multiple_pks_insert();\n\n-- issue https://github.com/PostgREST/postgrest/issues/2283\ncreate view self_recursive_view as table projects;\ncreate or replace view self_recursive_view as table self_recursive_view;\n\nCREATE FUNCTION test.computed_clients(test.projects) RETURNS SETOF test.clients ROWS 1 AS $$\n  SELECT * FROM test.clients WHERE id = $1.client_id;\n$$ LANGUAGE sql STABLE;\n\nCREATE FUNCTION test.computed_projects(test.clients) RETURNS SETOF test.projects ROWS 1 AS $$\n  SELECT * FROM test.projects WHERE client_id = $1.id;\n$$ LANGUAGE sql STABLE;\n\n-- issue https://github.com/PostgREST/postgrest/issues/2459\ncreate table public.i2459_simple_t1 (\n  id int primary key\n);\n\ncreate table public.i2459_simple_t2 (\n  t1_id int references public.i2459_simple_t1\n);\n\ncreate view i2459_simple_v1 as table public.i2459_simple_t1;\n\ncreate view i2459_simple_v2 as\n  select t1_id as t1_id1, t1_id as t1_id2 from public.i2459_simple_t2;\n\n\ncreate table public.i2459_composite_t1 (\n  primary key (a,b),\n  a int,\n  b int\n);\n\ncreate table public.i2459_composite_t2 (\n  t1_a int,\n  t1_b int,\n  constraint i2459_composite_t2_t1_a_t1_b_fkey foreign key (t1_a, t1_b) references public.i2459_composite_t1\n);\n\ncreate view i2459_composite_v1 as table public.i2459_composite_t1;\n\ncreate view i2459_composite_v2 as\n  select t1_a as t1_a1,\n         t1_b as t1_b1,\n         t1_a as t1_a2,\n         t1_b as t1_b2\n    from public.i2459_composite_t2;\n\n\ncreate table public.i2459_self_t (\n  id int primary key,\n  parent int references public.i2459_self_t,\n  type text\n);\n\n\ncreate view i2459_self_v1 as\nselect parent.parent as grandparent,\n       child.parent,\n       child.id\n  from public.i2459_self_t as parent\n       join public.i2459_self_t as child\n         on child.parent = parent.id\n where child.type = 'A';\n\ncreate view i2459_self_v2 as\nselect parent.parent as grandparent,\n       child.parent,\n       child.id\n  from public.i2459_self_t as parent\n       join public.i2459_self_t as child\n         on child.parent = parent.id\n where child.type = 'B';\n\n-- issue https://github.com/PostgREST/postgrest/issues/2548\nCREATE TABLE public.ta (\n  a1 INT PRIMARY KEY,\n  a2 INT,\n  UNIQUE (a1, a2)\n);\n\nCREATE TABLE public.tb (\n  b1 INT REFERENCES public.ta (a1),\n  b2 INT,\n  FOREIGN KEY (b1, b2) REFERENCES public.ta (a1, a2)\n);\n\nCREATE VIEW test.va AS SELECT a1 FROM public.ta;\nCREATE VIEW test.vb AS SELECT b1 FROM public.tb;\n\ncreate table test.trash(\n  id int primary key\n);\n\ncreate table test.trash_details(\n  id int primary key references test.trash(id),\n  jsonb_col jsonb\n);\n\nCREATE TABLE test.groups (\n    name text PRIMARY KEY\n);\n\nCREATE TABLE test.yards (\n    id bigint PRIMARY KEY\n);\n\nCREATE TABLE test.group_yard (\n    id bigint NOT NULL,\n    group_id text NOT NULL REFERENCES test.groups(name),\n    yard_id bigint NOT NULL REFERENCES test.yards(id),\n    PRIMARY KEY (id, group_id, yard_id)\n);\n\nCREATE FUNCTION test.get_yards() RETURNS SETOF test.yards\nLANGUAGE sql\nAS $$\n  select * from test.yards;\n$$;\n\ncreate table test.posters(\n  id int primary key,\n  name text\n);\n\ncreate table test.subscriptions(\n  subscriber int references test.posters(id),\n  subscribed int references test.posters(id),\n  primary key(subscriber, subscribed)\n);\n\n-- Data representations feature\nDROP DOMAIN IF EXISTS public.color CASCADE;\nCREATE DOMAIN public.color AS INTEGER CHECK (VALUE >= 0 AND VALUE <= 16777215);\n\nCREATE OR REPLACE FUNCTION color(json) RETURNS public.color AS $$\n  SELECT color($1 #>> '{}');\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION color(text) RETURNS public.color AS $$\n  SELECT (('x' || lpad((CASE WHEN SUBSTRING($1::text, 1, 1) = '#' THEN SUBSTRING($1::text, 2) ELSE $1::text END), 8, '0'))::bit(32)::int)::public.color;\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.color) RETURNS json AS $$\n  SELECT\n    CASE WHEN $1 IS NULL THEN to_json(''::text)\n    ELSE to_json('#' || lpad(upper(to_hex($1)), 6, '0'))\n  END;\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE CAST (public.color AS json) WITH FUNCTION \"json\"(public.color) AS IMPLICIT;\nCREATE CAST (json AS public.color) WITH FUNCTION color(json) AS IMPLICIT;\nCREATE CAST (text AS public.color) WITH FUNCTION color(text) AS IMPLICIT;\n\nDROP DOMAIN IF EXISTS public.isodate CASCADE;\nCREATE DOMAIN public.isodate AS timestamp with time zone;\n\nCREATE OR REPLACE FUNCTION isodate(json) RETURNS public.isodate AS $$\n  SELECT isodate($1 #>> '{}');\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION isodate(text) RETURNS public.isodate AS $$\n  SELECT (replace($1, 'Z', '+00:00')::timestamp with time zone)::public.isodate;\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.isodate) RETURNS json AS $$\n  SELECT to_json(replace(to_json($1)#>>'{}', '+00:00', 'Z'));\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE CAST (public.isodate AS json) WITH FUNCTION \"json\"(public.isodate) AS IMPLICIT;\nCREATE CAST (json AS public.isodate) WITH FUNCTION isodate(json) AS IMPLICIT;\n-- We intentionally don't have this in order to test query string parsing doesn't try to fall back on JSON parsing.\n-- CREATE CAST (text AS public.isodate) WITH FUNCTION isodate(text) AS IMPLICIT;\n\n-- bytea_b64 is a base64-encoded binary string\nDROP DOMAIN IF EXISTS public.bytea_b64 CASCADE;\nCREATE DOMAIN public.bytea_b64 AS bytea;\n\nCREATE OR REPLACE FUNCTION bytea_b64(json) RETURNS public.bytea_b64 AS $$\n  SELECT bytea_b64($1 #>> '{}');\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION bytea_b64(text) RETURNS public.bytea_b64 AS $$\n  -- allow unpadded base64\n  SELECT decode($1 || repeat('=', 4 - (length($1) % 4)), 'base64')::public.bytea_b64;\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.bytea_b64) RETURNS json AS $$\n  SELECT to_json(translate(encode($1, 'base64'), E'\\n', ''));\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE CAST (public.bytea_b64 AS json) WITH FUNCTION \"json\"(public.bytea_b64) AS IMPLICIT;\nCREATE CAST (json AS public.bytea_b64) WITH FUNCTION bytea_b64(json) AS IMPLICIT;\nCREATE CAST (text AS public.bytea_b64) WITH FUNCTION bytea_b64(text) AS IMPLICIT;\n\n-- unixtz is a timestamptz represented as an integer number of seconds since the Unix epoch\nDROP DOMAIN IF EXISTS public.unixtz CASCADE;\nCREATE DOMAIN public.unixtz AS timestamp with time zone;\n\nCREATE OR REPLACE FUNCTION unixtz(json) RETURNS public.unixtz AS $$\n  SELECT unixtz($1 #>> '{}');\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION unixtz(text) RETURNS public.unixtz AS $$\n  SELECT (to_timestamp($1::numeric)::public.unixtz);\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.unixtz) RETURNS json AS $$\n  SELECT to_json(extract(epoch from $1)::bigint);\n$$ LANGUAGE SQL IMMUTABLE;\n\n\nCREATE CAST (public.unixtz AS json) WITH FUNCTION \"json\"(public.unixtz) AS IMPLICIT;\nCREATE CAST (json AS public.unixtz) WITH FUNCTION unixtz(json) AS IMPLICIT;\nCREATE CAST (text AS public.unixtz) WITH FUNCTION unixtz(text) AS IMPLICIT;\n\nDROP DOMAIN IF EXISTS public.monetary CASCADE;\nCREATE DOMAIN public.monetary AS numeric(17,2);\n\nCREATE OR REPLACE FUNCTION monetary(json) RETURNS public.monetary AS $$\n  SELECT monetary($1 #>> '{}');\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION monetary(text) RETURNS public.monetary AS $$\n  SELECT ($1::numeric)::public.monetary;\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE OR REPLACE FUNCTION \"json\"(public.monetary) RETURNS json AS $$\n  SELECT to_json($1::text);\n$$ LANGUAGE SQL IMMUTABLE;\n\nCREATE CAST (public.monetary AS json) WITH FUNCTION \"json\"(public.monetary) AS IMPLICIT;\nCREATE CAST (json AS public.monetary) WITH FUNCTION monetary(json) AS IMPLICIT;\nCREATE CAST (text AS public.monetary) WITH FUNCTION monetary(text) AS IMPLICIT;\n\nCREATE TABLE datarep_todos (\n  id bigint primary key,\n  name text,\n  label_color public.color default 0,\n  due_at public.isodate default '2018-01-01'::date,\n  icon_image public.bytea_b64,\n  created_at public.unixtz default '2017-12-14 01:02:30'::timestamptz,\n  budget public.monetary default 0\n);\n\nCREATE TABLE datarep_next_two_todos (\n  id bigint primary key,\n  first_item_id bigint references datarep_todos(id),\n  second_item_id bigint references datarep_todos(id),\n  name text\n);\n\nCREATE VIEW datarep_todos_computed as (\n  SELECT id,\n    name,\n    label_color,\n    due_at,\n    (label_color / 2)::public.color as dark_color\n  FROM datarep_todos\n);\n\n-- view's name is alphabetically before projects\ncreate view test.alpha_projects as\n  select c.id, p.name as pro_name, c.name as cli_name\n  from projects p join clients c on p.client_id = c.id;\n\n-- view's name is alphabetically after projects\ncreate view test.zeta_projects as\n  select c.id, p.name as pro_name, c.name as cli_name\n  from projects p join clients c on p.client_id = c.id;\n\nCREATE VIEW test.complex_items_view AS\nSELECT * FROM test.complex_items;\n\nALTER VIEW test.complex_items_view ALTER COLUMN name SET DEFAULT 'Default';\n\ncreate table test.tbl_w_json(\n  id int,\n  data json\n);\n\nCREATE TABLE test.channels (\n  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n  data jsonb DEFAULT '{\"foo\": \"bar\"}',\n  slug text\n);\n\nCREATE FUNCTION test.is_superuser() RETURNS boolean\nLANGUAGE sql\nAS $$\n  select current_setting('is_superuser')::boolean;\n$$;\n\nCREATE TABLE test.foo (\n  a text,\n  b text GENERATED ALWAYS AS (\n      case WHEN a = 'telegram' THEN 'im'\n           WHEN a = 'proton' THEN 'email'\n           WHEN a = 'infinity' THEN 'idea'\n           ELSE 'bad idea'\n      end) stored\n);\n\ncreate domain devil_int as int\n  default 666;\n\ncreate table evil_friends(\n  id   devil_int\n, name text\n);\n\ncreate table evil_friends_with_column_default(\n  id   devil_int default 420\n, name text\n);\n\ncreate table bets (\n  id int\n, data_json  json\n, data_jsonb jsonb\n);\n\ncreate index bets_data_json  on bets ((data_json  ->>'contractId'));\ncreate index bets_data_jsonb on bets ((data_jsonb ->>'contractId'));\n\n--- https://github.com/PostgREST/postgrest/issues/2862\ncreate table profiles (\n  id uuid primary key,\n  username text null\n);\n\ncreate table user_friend (\n  id bigint primary key,\n  user1 uuid references profiles (id),\n  user2 uuid references profiles (id)\n);\n\ncreate table status(\n  id bigint primary key\n);\n\ncreate table tournaments(\n  id bigint primary key,\n  status bigint references status(id)\n);\n\n-- https://github.com/PostgREST/postgrest/issues/2861\nCREATE TABLE bitchar_with_length (\n  bit bit(5),\n  char char(5),\n  bit_arr bit(5)[],\n  char_arr char(5)[]\n);\n\n-- https://github.com/PostgREST/postgrest/issues/1586\ncreate or replace function char_param_select(char_ char(4), char_arr char(4)[])\nreturns table(char_ char, char_arr char[]) as $$\n  select $1, $2;\n$$ language sql;\n\ncreate or replace function bit_param_select(bit_ char(4), bit_arr char(4)[])\nreturns table(bit_ char, bit_arr char[]) as $$\n  select $1, $2;\n$$ language sql;\n\ncreate or replace function char_param_insert(char_ char(4), char_arr char(4)[])\nreturns void as $$\n  insert into bitchar_with_length(char, char_arr) values($1, $2);\n$$ language sql;\n\ncreate or replace function bit_param_insert(bit_ bit(4), bit_arr bit(4)[])\nreturns void as $$\ninsert into bitchar_with_length(bit, bit_arr) values($1, $2);\n$$ language sql;\n\ncreate function returns_record() returns record as $$\nselect * from projects limit 1;\n$$ language sql;\n\ncreate function returns_record_params(id int, name text) returns record as $$\nselect * from projects p where p.id = $1 and p.name like $2;\n$$ language sql;\n\ncreate function returns_setof_record() returns setof record as $$\nselect * from projects limit 2;\n$$ language sql;\n\ncreate function returns_setof_record_params(id int, name text) returns setof record as $$\nselect * from projects p where p.id >= $1 and p.name like $2;\n$$ language sql;\n\ncreate function raise_sqlstate_test1() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\",\"details\":\"DEF\",\"hint\":\"XYZ\"}',\n      detail = '{\"status\":332,\"status_text\":\"My Custom Status\",\"headers\":{\"X-Header\":\"str\"}}';\nend\n$$;\n\ncreate function raise_sqlstate_test2() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\"}',\n      detail = '{\"status\":332,\"headers\":{\"X-Header\":\"str\"}}';\nend\n$$;\n\ncreate function raise_sqlstate_test3() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\"}',\n      detail = '{\"status\":404,\"headers\":{\"X-Header\":\"str\"}}';\nend\n$$;\n\ncreate function raise_sqlstate_test4() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\"}',\n      detail = '{\"status\":404,\"status_text\":\"My Not Found\",\"headers\":{\"X-Header\":\"str\"}}';\nend\n$$;\n\ncreate function raise_sqlstate_invalid_json_message() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = 'INVALID',\n      detail = '{\"status\":332,\"headers\":{\"X-Header\":\"str\"}}';\nend\n$$;\n\ncreate function raise_sqlstate_invalid_json_details() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\",\"details\":\"DEF\"}',\n      detail = 'INVALID';\nend\n$$;\n\ncreate function raise_sqlstate_missing_details() returns void\n  language plpgsql\n  as $$\nbegin\n    raise sqlstate 'PGRST' USING\n      message = '{\"code\":\"123\",\"message\":\"ABC\",\"details\":\"DEF\"}';\nend\n$$;\n\ncreate table table_a (\n  id int primary key,\n  name text\n);\n\ncreate table table_b (\n  table_a_id int references table_a(id),\n  name text\n);\n\ncreate or replace function test.returns_complex()\nreturns table(id int, val complex) as $$\n  select 1, row(0.1, 0.5)::complex as val\n  union\n  select 2, row(0.2, 0.6)::complex as val\n  union\n  select 3, row(0.3, 0.7)::complex as val;\n$$ language sql;\n\ncreate function computed_rel_overload(items) returns setof items2 as $$\n  select * from items2 limit 1\n$$ language sql;\n\ncreate function computed_rel_overload(items2) returns setof items2 as $$\n  select * from items2 limit 2\n$$ language sql;\n\ncreate function search2(id bigint) returns setof items2\nlanguage plpgsql\nstable\nas $$ begin\n  return query select items2.id from items2 where items2.id=search2.id;\nend$$;\n\ncreate table test.lines (\n  id   int primary key\n, name text\n, geom extensions.geometry(LINESTRING, 4326)\n);\n\ncreate or replace function test.get_lines ()\nreturns setof test.lines as $$\n  select * from lines;\n$$ language sql;\n\ncreate or replace function test.get_line (id int)\nreturns \"application/vnd.twkb\" as $$\n  select extensions.st_astwkb(geom)::\"application/vnd.twkb\" from lines where id = get_line.id;\n$$ language sql;\n\ncreate or replace function test.get_shop_bles ()\nreturns setof test.shop_bles as $$\n  select * from shop_bles;\n$$ language sql;\n\n-- it can work without a final function too if the stype is already the media type\ncreate or replace function test.twkb_handler_transition (state \"application/vnd.twkb\", next test.lines)\nreturns \"application/vnd.twkb\" as $$\n  select (state || extensions.st_astwkb(next.geom)) :: \"application/vnd.twkb\";\n$$ language sql;\n\ndrop aggregate if exists test.twkb_agg(test.lines);\ncreate aggregate test.twkb_agg (test.lines) (\n  initcond = ''\n, stype = \"application/vnd.twkb\"\n, sfunc = test.twkb_handler_transition\n);\n\ncreate or replace function test.geo2json_trans (state \"application/vnd.geo2+json\", next anyelement)\nreturns \"application/vnd.geo2+json\" as $$\n  select (state || extensions.ST_AsGeoJSON(next)::jsonb)::\"application/vnd.geo2+json\";\n$$ language sql;\n\ncreate or replace function test.geo2json_final (data \"application/vnd.geo2+json\")\nreturns \"application/vnd.geo2+json\" as $$\n  select (jsonb_build_object('type', 'FeatureCollection', 'hello', 'world'))::\"application/vnd.geo2+json\";\n$$ language sql;\n\ndrop aggregate if exists test.geo2json_agg_any(anyelement);\ncreate aggregate test.geo2json_agg_any(anyelement) (\n  initcond = '[]'\n, stype = \"application/vnd.geo2+json\"\n, sfunc = geo2json_trans\n, finalfunc = geo2json_final\n);\n\ncreate or replace function test.geo2json_trans (state \"application/vnd.geo2+json\", next test.shop_bles)\nreturns \"application/vnd.geo2+json\" as $$\n  select '\"anyelement overridden\"'::\"application/vnd.geo2+json\";\n$$ language sql;\n\ndrop aggregate if exists test.geo2json_agg(test.shop_bles);\ncreate aggregate test.geo2json_agg(test.shop_bles) (\n  initcond = '[]'\n, stype = \"application/vnd.geo2+json\"\n, sfunc = geo2json_trans\n);\n\ncreate table ov_json ();\n\n-- override application/json\ncreate or replace function test.ov_json_trans (state \"application/json\", next ov_json)\nreturns \"application/json\" as $$\n  select null;\n$$ language sql;\n\ndrop aggregate if exists test.ov_json_agg(ov_json);\ncreate aggregate test.ov_json_agg(ov_json) (\n  initcond = '{\"overridden\": \"true\"}'\n, stype = \"application/json\"\n, sfunc = ov_json_trans\n);\n\n-- override application/geo+json\ncreate or replace function test.lines_geojson_trans (state jsonb, next test.lines)\nreturns \"application/geo+json\" as $$\n  select (state || extensions.ST_AsGeoJSON(next)::jsonb)::\"application/geo+json\";\n$$ language sql;\n\ncreate or replace function test.lines_geojson_final (data jsonb)\nreturns \"application/geo+json\" as $$\n  select jsonb_build_object(\n    'type', 'FeatureCollection',\n    'crs',  json_build_object(\n        'type',      'name',\n        'properties', json_build_object(\n            'name', 'EPSG:4326'\n         )\n     ),\n    'features', data\n  )::\"application/geo+json\";\n$$ language sql;\n\ndrop aggregate if exists test.lines_geojson_agg(test.lines);\ncreate aggregate test.lines_geojson_agg (test.lines) (\n  initcond = '[]'\n, stype = \"application/geo+json\"\n, sfunc = lines_geojson_trans\n, finalfunc = lines_geojson_final\n);\n\n-- override application/vnd.pgrst.object\ncreate or replace function test.pgrst_obj_json_trans (state \"application/vnd.pgrst.object\", next anyelement)\nreturns \"application/vnd.pgrst.object\" as $$\n  select null;\n$$ language sql;\n\ndrop aggregate if exists test.pgrst_obj_agg(anyelement);\ncreate aggregate test.pgrst_obj_agg(anyelement) (\n  initcond = '{\"overridden\": \"true\"}'\n, stype = \"application/vnd.pgrst.object\"\n, sfunc = pgrst_obj_json_trans\n);\n\n-- create a \"text/tab-separated-values\" media type\ncreate or replace function test.tsv_trans (state text, next test.projects)\nreturns \"text/tab-separated-values\" as $$\n  select (state || next.id::text || E'\\t' || next.name || E'\\t' || coalesce(next.client_id::text, '') || E'\\n')::\"text/tab-separated-values\";\n$$ language sql;\n\n\ncreate or replace function test.tsv_final (data \"text/tab-separated-values\")\nreturns \"text/tab-separated-values\" as $$\n  select (E'id\\tname\\tclient_id\\n' || data)::\"text/tab-separated-values\";\n$$ language sql;\n\ndrop aggregate if exists test.tsv_agg(test.projects);\ncreate aggregate test.tsv_agg (test.projects) (\n  initcond = ''\n, stype = \"text/tab-separated-values\"\n, sfunc = tsv_trans\n, finalfunc = tsv_final\n);\n\n-- override CSV with BOM plus attachment\ncreate or replace function test.bom_csv_trans (state text, next test.lines)\nreturns \"text/csv\" as $$\n  select (state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\\n')::\"text/csv\";\n$$ language sql;\n\ncreate or replace function test.bom_csv_final (data \"text/csv\")\nreturns \"text/csv\" as $$\n  select set_config('response.headers', '[{\"Content-Disposition\": \"attachment; filename=\\\"lines.csv\\\"\"}]', true);\n  -- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8\n  select (convert_from (decode (E'EFBBBF', 'hex'),'UTF8') || (E'id,name,geom\\n' || data))::\"text/csv\";\n$$ language sql;\n\ndrop aggregate if exists test.bom_csv_agg(test.lines);\ncreate aggregate test.bom_csv_agg (test.lines) (\n  initcond = ''\n, stype = \"text/csv\"\n, sfunc = bom_csv_trans\n, finalfunc = bom_csv_final\n);\n\ncreate table empty_string as select 1 as id, ''::text as string;\n\ncreate table timestamps (\n  t timestamp with time zone\n);\n\ncreate table project_invoices (\n  id int primary key\n, invoice_total numeric\n, project_id integer references projects(id)\n);\n\ncreate table budget_categories (\n  id int primary key\n, category_name text\n, budget_owner text\n, budget_amount numeric\n);\n\ncreate table budget_expenses (\n  id int primary key\n, expense_amount numeric\n, budget_category_id integer references budget_categories(id)\n);\n\ncreate or replace function ret_any_mt ()\nreturns \"*/*\" as $$\n  select 'any'::\"*/*\";\n$$ language sql;\n\ncreate or replace function ret_some_mt ()\nreturns \"*/*\" as $$\ndeclare\n  req_accept text := current_setting('request.headers', true)::json->>'accept';\n  resp bytea;\nbegin\n  case req_accept\n    when 'app/chico'   then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);\n      resp := 'chico';\n    when 'app/harpo'   then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);\n      resp := 'harpo';\n    when '*/*'         then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'app/groucho'))::text, true);\n      resp := 'groucho';\n    else\n      raise sqlstate 'PT406' using message = 'Not Acceptable';\n  end case;\n  return resp;\nend; $$ language plpgsql;\n\ncreate table some_numbers as select x::int as val from generate_series(1,10) x;\n\ncreate or replace function some_trans (state \"*/*\", next some_numbers)\nreturns \"*/*\" as $$\n  select (state || E'\\n' || next.val::text::bytea)::\"*/*\";\n$$ language sql;\n\ncreate or replace function some_final (data \"*/*\")\nreturns \"*/*\" as $$\ndeclare\n  req_accept text := current_setting('request.headers', true)::json->>'accept';\n  prefix bytea;\nbegin\n  case req_accept\n    when 'magic/number' then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);\n      prefix := 'magic';\n    when 'crazy/bingo'  then\n      perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);\n      prefix := 'crazy';\n    else\n      prefix := 'anything';\n  end case;\n  return (prefix || data)::\"*/*\";\nend; $$ language plpgsql;\n\ndrop aggregate if exists some_agg (some_numbers);\ncreate aggregate test.some_agg (some_numbers) (\n  initcond = ''\n, stype = \"*/*\"\n, sfunc = some_trans\n, finalfunc = some_final\n);\n\ncreate view bad_subquery as\nselect * from projects where id = (select id from projects);\n\n-- custom generic mimetype\ncreate domain \"pg/outfunc\" as text;\ncreate function test.outfunc_trans (state text, next anyelement)\nreturns \"pg/outfunc\" as $$\n  select (state || next::text || E'\\n')::\"pg/outfunc\";\n$$ language sql;\n\ncreate aggregate test.outfunc_agg (anyelement) (\n  initcond = ''\n, stype = \"pg/outfunc\"\n, sfunc = outfunc_trans\n);\n\n-- used for manual testing\ncreate or replace function test.sleep(seconds double precision default 5) returns void as $$\n  select pg_sleep(seconds);\n$$ language sql;\n\n-- https://github.com/PostgREST/postgrest/issues/3256\ncreate view test.infinite_recursion as\nselect * from test.projects;\n\ncreate or replace view test.infinite_recursion as\nselect * from test.infinite_recursion;\n\n\ncreate or replace function temp_file_limit()\nreturns bigint as $$\n  select COUNT(*) FROM generate_series('-infinity'::TIMESTAMP, 'epoch'::TIMESTAMP, INTERVAL '1 DAY');\n$$ language sql security definer set temp_file_limit to '1kB';\n\n-- https://github.com/PostgREST/postgrest/issues/3255\ncreate table test.infinite_inserts(\n  id int\n, name text\n);\n\ncreate or replace function infinite_inserts()\nreturns trigger as $$ begin\n  insert into infinite_inserts values (NEW.id, NEW.name);\nend $$ language plpgsql;\n\ncreate trigger do_infinite_inserts\nafter insert\non infinite_inserts\nfor each row\nexecute procedure infinite_inserts();\n\ncreate table factories (\n  id int primary key,\n  name text\n);\n\ncreate table process_categories (\n  id int primary key,\n  name text\n);\n\ncreate table processes (\n  id int primary key,\n  name text,\n  factory_id int references factories(id),\n  category_id int references process_categories(id)\n);\n\ncreate table process_costs (\n  process_id int references processes(id) primary key,\n  cost numeric\n);\n\ncreate table supervisors (\n  id int primary key,\n  name text\n);\n\ncreate table process_supervisor (\n  process_id int references processes(id),\n  supervisor_id int references supervisors(id),\n  primary key (process_id, supervisor_id)\n);\n\ncreate table surr_serial_upsert (\n  id serial primary key,\n  name text,\n  extra text\n);\n\ncreate table surr_gen_default_upsert (\n  id int generated by default as identity primary key,\n  name text,\n  extra text\n);\n\n-- test for case-sensitive table name and sequence name, see #3712\ncreate table \"Surr_Gen_Default_Upsert\" (\n  id int generated by default as identity primary key,\n  name text,\n  extra text\n);\n\ncreate domain tsvector_not_null as tsvector\n  constraint \"tsvector is required\" check (value is not null);\n\ncreate domain tsvector_not_empty as tsvector_not_null\n  constraint \"tsvector is required and not empty\" check (value <> '');\n\ncreate table tsearch_to_tsvector (\n  text_search text,\n  jsonb_search jsonb,\n  text_search_domain tsvector_not_null default '',\n  text_search_rec_domain tsvector_not_empty default '.'\n);\n\ncreate function test.get_tsearch_to_tsvector() returns setof test.tsearch_to_tsvector AS $$\n  select * from test.tsearch_to_tsvector;\n$$ language sql;\n\ncreate function test.text_search_vector(test.tsearch_to_tsvector) returns tsvector AS $$\n  select to_tsvector('simple', $1.text_search)\n$$ language sql;\n\n-- to test ordering with mutations\nCREATE TABLE test.artists (\n  id int PRIMARY KEY,\n  name text\n);\n\nCREATE TABLE test.albums (\n  id int PRIMARY KEY,\n  title text,\n  artist_id int,\n\n  CONSTRAINT fk_artist\n    FOREIGN KEY (artist_id) REFERENCES artists (id)\n    ON UPDATE CASCADE\n    ON DELETE CASCADE\n);\n\ncreate table operators (\n  id int primary key,\n  name text,\n  status jsonb\n);\n\ncreate table process_operator (\n  process_id int references processes(id),\n  operator_id int references operators(id),\n  primary key (process_id, operator_id)\n);\n\ncreate table factory_buildings (\n  id int primary key,\n  code char(4),\n  size numeric,\n  \"type\" char(1),\n  factory_id int references factories(id),\n  inspections jsonb\n);\n\n-- collision test as occured in https://github.com/PostgREST/postgrest/issues/4052\ncreate table test.collision_test_table (id integer);\ncomment on table collision_test_table is 'foobarbaz';\n\ncreate function test.collision_test_func(id integer)\nreturns int language sql as $$\n  select 1;\n$$;\n\nupdate pg_proc\nset oid = 'test.collision_test_table'::regclass::oid\nwhere oid = 'test.collision_test_func'::regproc::oid;\n\ncomment on function test.collision_test_func(id integer) is 'fizzbuzz';\n\n\ncreate or replace function test.delete_items_returns_setof() returns setof items as $$\n  delete from items where id <= 15 returning *; -- deletes 15 items, then return them\n$$ language sql;\n\ncreate or replace function test.delete_items_returns_table() returns table(id bigint) as $$\n  delete from items where id <= 15 returning *;\n$$ language sql;\n\ncreate or replace function test.delete_items_returns_void() returns void as $$\n  delete from items;\n$$ language sql;\n\ncreate function do_nothing() returns void as $_$\n$_$ language sql;\n\ncreate or replace function test.get_tiobe_pls() returns setof test.tiobe_pls as $$\n  select * from test.tiobe_pls;\n$$ language sql;\n"
  },
  {
    "path": "test/weeder.toml",
    "content": "roots = [ \"^Main.main$\", \"^Paths_\" ]\ntype-class-roots = true\n"
  }
]