Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519615d5fb | ||
|
|
de5a8954b2 | ||
|
|
64b5a59410 | ||
|
|
417a96d137 | ||
|
|
53593824a8 | ||
|
|
e642086d27 | ||
|
|
e451700d5d | ||
|
|
13868983af | ||
|
|
db9da0b1ca | ||
|
|
f09655b7a6 | ||
|
|
8715e426c0 | ||
|
|
9a40bc266e | ||
|
|
e67461b991 | ||
|
|
420933ceba | ||
|
|
8f8dbd3425 | ||
|
|
8d7d7e4112 | ||
|
|
521acf61cb |
@@ -112,7 +112,7 @@ runs:
|
|||||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
||||||
- name: Save artifact to GitHub Actions
|
- name: Save artifact to GitHub Actions
|
||||||
if: steps.find-task.outputs.task_found
|
if: steps.find-task.outputs.task_found
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.upload }}
|
name: ${{ inputs.upload }}
|
||||||
path: ${{ steps.download.outputs.artifacts }}
|
path: ${{ steps.download.outputs.artifacts }}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Build static executable
|
- name: Build static executable
|
||||||
run: nix-build -A postgrestStatic
|
run: nix-build -A postgrestStatic
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: postgrest-linux-static-x64
|
name: postgrest-linux-static-x64
|
||||||
path: result/bin/postgrest
|
path: result/bin/postgrest
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
- name: Save built Docker image as artifact
|
- name: Save built Docker image as artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x64
|
name: postgrest-docker-x64
|
||||||
path: postgrest-docker.tar.gz
|
path: postgrest-docker.tar.gz
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
- uses: haskell-actions/setup@33585e1a16afa5875e124b0ebc89dd0c2f872c21 # v2.7.3
|
- uses: haskell-actions/setup@ec49483bfc012387b227434aba94f59a6ecd0900 # v2.7.5
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.6.5
|
ghc-version: 9.6.5
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
run: strip result/postgrest*
|
run: strip result/postgrest*
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
if: matrix.artifact
|
if: matrix.artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact }}
|
name: ${{ matrix.artifact }}
|
||||||
path: |
|
path: |
|
||||||
@@ -179,7 +179,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
- uses: haskell-actions/setup@33585e1a16afa5875e124b0ebc89dd0c2f872c21 # v2.7.3
|
- uses: haskell-actions/setup@ec49483bfc012387b227434aba94f59a6ecd0900 # v2.7.5
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
- name: Cache .cabal
|
- name: Cache .cabal
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
- name: Extract downloaded binaries
|
- name: Extract downloaded binaries
|
||||||
run: tar -xvf result.tar.xz && rm result.tar.xz
|
run: tar -xvf result.tar.xz && rm result.tar.xz
|
||||||
- name: Save aarch64 executable as artifact
|
- name: Save aarch64 executable as artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: postgrest-ubuntu-aarch64
|
name: postgrest-ubuntu-aarch64
|
||||||
path: result/postgrest
|
path: result/postgrest
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
echo "Relevant extract from CHANGELOG.md:"
|
echo "Relevant extract from CHANGELOG.md:"
|
||||||
cat CHANGES.md
|
cat CHANGES.md
|
||||||
- name: Save CHANGES.md as artifact
|
- name: Save CHANGES.md as artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: release-changes
|
name: release-changes
|
||||||
path: CHANGES.md
|
path: CHANGES.md
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Create release bundle with archives for all builds
|
- name: Create release bundle with archives for all builds
|
||||||
@@ -226,7 +226,7 @@ jobs:
|
|||||||
artifacts/postgrest-windows-x64/postgrest.exe
|
artifacts/postgrest-windows-x64/postgrest.exe
|
||||||
|
|
||||||
- name: Save release bundle
|
- name: Save release bundle
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: release-bundle
|
path: release-bundle
|
||||||
@@ -275,7 +275,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
- name: Download Docker image
|
- name: Download Docker image
|
||||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x64
|
name: postgrest-docker-x64
|
||||||
- name: Publish images on Docker Hub
|
- name: Publish images on Docker Hub
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure'
|
if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure'
|
||||||
steps:
|
steps:
|
||||||
- name: Download from Artifacts
|
- name: Download from Artifacts
|
||||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
|
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
|
||||||
postgrest-loadtest-report > loadtest/loadtest.md
|
postgrest-loadtest-report > loadtest/loadtest.md
|
||||||
- name: Upload report
|
- name: Upload report
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: loadtest.md
|
name: loadtest.md
|
||||||
path: loadtest/loadtest.md
|
path: loadtest/loadtest.md
|
||||||
|
|||||||
@@ -5,6 +5,25 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [12.2.3] - 2024-08-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #3091, Broken link in OpenAPI description `externalDocs` - @salim-b
|
||||||
|
- #3659, Embed One-to-One relationship with different column order properly - @wolfgangwalther
|
||||||
|
- #3504, Remove `format` from `rowFilter` parameters in OpenAPI - @dantheman2865
|
||||||
|
- #3660, Fix regression that loaded the schema cache before the in-database configuration - @steve-chavez, @laurenceisla
|
||||||
|
|
||||||
|
## [12.2.2] - 2024-07-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #3093, Nested empty embeds no longer show empty values and are correctly omitted - @laurenceisla
|
||||||
|
- #3644, Make --dump-schema work with in-database pgrst.db_schemas setting - @wolfgangwalther
|
||||||
|
- #3644, Show number of timezones in schema cache load report - @wolfgangwalther
|
||||||
|
- #3644, List correct enum options in OpenApi output when multiple types with same name are present - @wolfgangwalther
|
||||||
|
- #3523, Fix schema cache loading retry without backoff - @steve-chavez
|
||||||
|
|
||||||
## [12.2.1] - 2024-06-27
|
## [12.2.1] - 2024-06-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||

|

|
||||||
|
|
||||||
[](https://www.patreon.com/postgrest)
|
[](https://www.patreon.com/postgrest)
|
||||||
[](https://www.paypal.me/postgrest)
|
|
||||||
[](https://gitter.im/begriffs/postgrest)
|
|
||||||
[](http://postgrest.org)
|
[](http://postgrest.org)
|
||||||
[](https://hub.docker.com/r/postgrest/postgrest/)
|
[](https://hub.docker.com/r/postgrest/postgrest/)
|
||||||
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
@@ -145,11 +143,7 @@ and the [API guide](http://postgrest.org/en/stable/api.html).
|
|||||||
|
|
||||||
## Supporting development
|
## Supporting development
|
||||||
|
|
||||||
You can help PostgREST ongoing maintenance and development by:
|
You can help PostgREST ongoing maintenance and development by making a regular donation through Patreon https://www.patreon.com/postgrest
|
||||||
|
|
||||||
- Making a regular donation through Patreon https://www.patreon.com/postgrest
|
|
||||||
|
|
||||||
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
|
|
||||||
|
|
||||||
Every donation will be spent on making PostgREST better for the whole community.
|
Every donation will be spent on making PostgREST better for the whole community.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# PostgREST
|
# PostgREST
|
||||||
|
|
||||||
[](https://gitter.im/begriffs/postgrest)
|
|
||||||
[](https://www.patreon.com/postgrest)
|
[](https://www.patreon.com/postgrest)
|
||||||
[](https://www.paypal.me/postgrest)
|
|
||||||
[](http://postgrest.org)
|
[](http://postgrest.org)
|
||||||
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -16,15 +16,9 @@ PostgREST Documentation
|
|||||||
.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg
|
.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg
|
||||||
:target: https://hub.docker.com/r/postgrest/postgrest/
|
:target: https://hub.docker.com/r/postgrest/postgrest/
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg
|
|
||||||
:target: https://gitter.im/begriffs/postgrest
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854
|
.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854
|
||||||
:target: https://www.patreon.com/postgrest
|
:target: https://www.patreon.com/postgrest
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/Donate-PayPal-green.svg
|
|
||||||
:target: https://www.paypal.com/paypalme/postgrest
|
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|
||||||
PostgREST 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.
|
PostgREST 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.
|
||||||
@@ -113,7 +107,7 @@ PostgREST has a focused scope. It works well with other tools like Nginx. This f
|
|||||||
Getting Support
|
Getting Support
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_ or join our `chat room <https://gitter.im/begriffs/postgrest>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
|
The 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.
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ cs :code:`@>` contains e.g. :code:`?tags=cs.{example,
|
|||||||
cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}`
|
cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}`
|
||||||
ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` –
|
ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` –
|
||||||
also supports array types, use curly braces instead of square brackets e.g.
|
also supports array types, use curly braces instead of square brackets e.g.
|
||||||
:code: `?arr=ov.{1,3}`
|
:code:`?arr=ov.{1,3}`
|
||||||
sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)`
|
sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)`
|
||||||
sr :code:`>>` strictly right of
|
sr :code:`>>` strictly right of
|
||||||
nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)`
|
nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)`
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ To make an API we'll simply be building a database. All the endpoints and permis
|
|||||||
|
|
||||||
By the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.
|
By the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.
|
||||||
|
|
||||||
Step 1. Relax, we'll help
|
Step 1. Install PostgreSQL
|
||||||
-------------------------
|
|
||||||
|
|
||||||
As you begin the tutorial, pop open the project `chat room <https://gitter.im/begriffs/postgrest>`_ in another tab. There are a nice group of people active in the project and we'll help you out if you get stuck.
|
|
||||||
|
|
||||||
Step 2. Install PostgreSQL
|
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
If 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.
|
If 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.
|
||||||
@@ -48,7 +43,7 @@ This will run the Docker instance as a daemon and expose port 5432 to the host s
|
|||||||
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!
|
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!
|
||||||
|
|
||||||
|
|
||||||
Step 3. Install PostgREST
|
Step 2. Install PostgREST
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
Using a Package Manager
|
Using a Package Manager
|
||||||
@@ -122,7 +117,7 @@ If everything is working correctly it will print out its version and the availab
|
|||||||
</details>
|
</details>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Step 4. Create Database for API
|
Step 3. Create Database for API
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Connect to the SQL console (psql) inside the container. To do so, run this from your command line:
|
Connect to the SQL console (psql) inside the container. To do so, run this from your command line:
|
||||||
@@ -185,7 +180,7 @@ Now quit out of psql; it's time to start the API!
|
|||||||
|
|
||||||
\q
|
\q
|
||||||
|
|
||||||
Step 5. Run PostgREST
|
Step 4. Run PostgREST
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
PostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:
|
PostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 12.2.1
|
version: 12.2.3
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ run appState = do
|
|||||||
|
|
||||||
observer $ AppStartObs prettyVersion
|
observer $ AppStartObs prettyVersion
|
||||||
|
|
||||||
AppState.connectionWorker appState
|
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||||
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState)
|
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||||
|
|
||||||
Listener.runListener appState
|
Listener.runListener appState
|
||||||
|
|
||||||
Admin.runAdmin appState (serverSettings conf)
|
Admin.runAdmin appState (serverSettings conf)
|
||||||
|
|
||||||
let app = postgrest configLogLevel appState (AppState.connectionWorker appState)
|
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
|
||||||
|
|
||||||
case configServerUnixSocket of
|
case configServerUnixSocket of
|
||||||
Just path -> do
|
Just path -> do
|
||||||
|
|||||||
+105
-165
@@ -24,8 +24,8 @@ module PostgREST.AppState
|
|||||||
, putPgVersion
|
, putPgVersion
|
||||||
, putIsListenerOn
|
, putIsListenerOn
|
||||||
, usePool
|
, usePool
|
||||||
, reReadConfig
|
, readInDbConfig
|
||||||
, connectionWorker
|
, schemaCacheLoader
|
||||||
, getObserver
|
, getObserver
|
||||||
, isLoaded
|
, isLoaded
|
||||||
, isPending
|
, isPending
|
||||||
@@ -85,39 +85,37 @@ data AuthResult = AuthResult
|
|||||||
|
|
||||||
data AppState = AppState
|
data AppState = AppState
|
||||||
-- | Database connection pool
|
-- | Database connection pool
|
||||||
{ statePool :: SQL.Pool
|
{ statePool :: SQL.Pool
|
||||||
-- | Database server version, will be updated by the connectionWorker
|
-- | Database server version
|
||||||
, statePgVersion :: IORef PgVersion
|
, statePgVersion :: IORef PgVersion
|
||||||
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
-- | Schema cache
|
||||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||||
-- | The schema cache status
|
-- | The schema cache status
|
||||||
, stateSCacheStatus :: IORef SchemaCacheStatus
|
, stateSCacheStatus :: IORef SchemaCacheStatus
|
||||||
-- | The connection status
|
|
||||||
, stateConnStatus :: IORef ConnectionStatus
|
|
||||||
-- | State of the LISTEN channel
|
-- | State of the LISTEN channel
|
||||||
, stateIsListenerOn :: IORef Bool
|
, stateIsListenerOn :: IORef Bool
|
||||||
-- | starts the connection worker with a debounce
|
-- | starts the connection worker with a debounce
|
||||||
, debouncedConnectionWorker :: IO ()
|
, debouncedSCacheLoader :: IO ()
|
||||||
-- | Config that can change at runtime
|
-- | Config that can change at runtime
|
||||||
, stateConf :: IORef AppConfig
|
, stateConf :: IORef AppConfig
|
||||||
-- | Time used for verifying JWT expiration
|
-- | Time used for verifying JWT expiration
|
||||||
, stateGetTime :: IO UTCTime
|
, stateGetTime :: IO UTCTime
|
||||||
-- | Used for killing the main thread in case a subthread fails
|
-- | Used for killing the main thread in case a subthread fails
|
||||||
, stateMainThreadId :: ThreadId
|
, stateMainThreadId :: ThreadId
|
||||||
-- | Keeps track of the next delay for db connection retry
|
-- | Keeps track of the next delay for db connection retry
|
||||||
, stateNextDelay :: IORef Int
|
, stateNextDelay :: IORef Int
|
||||||
-- | Keeps track of the next delay for the listener
|
-- | Keeps track of the next delay for the listener
|
||||||
, stateNextListenerDelay :: IORef Int
|
, stateNextListenerDelay :: IORef Int
|
||||||
-- | JWT Cache
|
-- | JWT Cache
|
||||||
, jwtCache :: C.Cache ByteString AuthResult
|
, jwtCache :: C.Cache ByteString AuthResult
|
||||||
-- | Network socket for REST API
|
-- | Network socket for REST API
|
||||||
, stateSocketREST :: NS.Socket
|
, stateSocketREST :: NS.Socket
|
||||||
-- | Network socket for the admin UI
|
-- | Network socket for the admin UI
|
||||||
, stateSocketAdmin :: Maybe NS.Socket
|
, stateSocketAdmin :: Maybe NS.Socket
|
||||||
-- | Observation handler
|
-- | Observation handler
|
||||||
, stateObserver :: ObservationHandler
|
, stateObserver :: ObservationHandler
|
||||||
, stateLogger :: Logger.LoggerState
|
, stateLogger :: Logger.LoggerState
|
||||||
, stateMetrics :: Metrics.MetricsState
|
, stateMetrics :: Metrics.MetricsState
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Schema cache status
|
-- | Schema cache status
|
||||||
@@ -126,15 +124,8 @@ data SchemaCacheStatus
|
|||||||
| SCPending
|
| SCPending
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
-- | Current database connection status
|
|
||||||
data ConnectionStatus
|
|
||||||
= ConnEstablished
|
|
||||||
| ConnPending
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
type AppSockets = (NS.Socket, Maybe NS.Socket)
|
type AppSockets = (NS.Socket, Maybe NS.Socket)
|
||||||
|
|
||||||
|
|
||||||
init :: AppConfig -> IO AppState
|
init :: AppConfig -> IO AppState
|
||||||
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||||
loggerState <- Logger.init
|
loggerState <- Logger.init
|
||||||
@@ -153,7 +144,6 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
|||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
<*> newIORef Nothing
|
<*> newIORef Nothing
|
||||||
<*> newIORef SCPending
|
<*> newIORef SCPending
|
||||||
<*> newIORef ConnPending
|
|
||||||
<*> newIORef False
|
<*> newIORef False
|
||||||
<*> pure (pure ())
|
<*> pure (pure ())
|
||||||
<*> newIORef conf
|
<*> newIORef conf
|
||||||
@@ -168,15 +158,15 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
|||||||
<*> pure loggerState
|
<*> pure loggerState
|
||||||
<*> pure metricsState
|
<*> pure metricsState
|
||||||
|
|
||||||
debWorker <-
|
deb <-
|
||||||
let decisecond = 100000 in
|
let decisecond = 100000 in
|
||||||
mkDebounce defaultDebounceSettings
|
mkDebounce defaultDebounceSettings
|
||||||
{ debounceAction = internalConnectionWorker appState
|
{ debounceAction = retryingSchemaCacheLoad appState
|
||||||
, debounceFreq = decisecond
|
, debounceFreq = decisecond
|
||||||
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
||||||
}
|
}
|
||||||
|
|
||||||
return appState { debouncedConnectionWorker = debWorker}
|
return appState { debouncedSCacheLoader = deb}
|
||||||
|
|
||||||
destroy :: AppState -> IO ()
|
destroy :: AppState -> IO ()
|
||||||
destroy = destroyPool
|
destroy = destroyPool
|
||||||
@@ -302,15 +292,12 @@ getSchemaCache = readIORef . stateSchemaCache
|
|||||||
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
|
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
|
||||||
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
|
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
|
||||||
|
|
||||||
connectionWorker :: AppState -> IO ()
|
schemaCacheLoader :: AppState -> IO ()
|
||||||
connectionWorker = debouncedConnectionWorker
|
schemaCacheLoader = debouncedSCacheLoader
|
||||||
|
|
||||||
getNextDelay :: AppState -> IO Int
|
getNextDelay :: AppState -> IO Int
|
||||||
getNextDelay = readIORef . stateNextDelay
|
getNextDelay = readIORef . stateNextDelay
|
||||||
|
|
||||||
putNextDelay :: AppState -> Int -> IO ()
|
|
||||||
putNextDelay = atomicWriteIORef . stateNextDelay
|
|
||||||
|
|
||||||
getNextListenerDelay :: AppState -> IO Int
|
getNextListenerDelay :: AppState -> IO Int
|
||||||
getNextListenerDelay = readIORef . stateNextListenerDelay
|
getNextListenerDelay = readIORef . stateNextListenerDelay
|
||||||
|
|
||||||
@@ -338,167 +325,120 @@ getSocketAdmin = stateSocketAdmin
|
|||||||
getMainThreadId :: AppState -> ThreadId
|
getMainThreadId :: AppState -> ThreadId
|
||||||
getMainThreadId = stateMainThreadId
|
getMainThreadId = stateMainThreadId
|
||||||
|
|
||||||
getIsListenerOn :: AppState -> IO Bool
|
isConnEstablished :: AppState -> IO Bool
|
||||||
getIsListenerOn appState = do
|
isConnEstablished appState = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
if configDbChannelEnabled then
|
if configDbChannelEnabled then -- if the listener is enabled, we can be sure the connection is up
|
||||||
readIORef $ stateIsListenerOn appState
|
readIORef $ stateIsListenerOn appState
|
||||||
else
|
else -- otherwise the only way to check the connection is to make a query
|
||||||
pure True
|
isRight <$> usePool appState (SQL.sql "SELECT 1")
|
||||||
|
|
||||||
putIsListenerOn :: AppState -> Bool -> IO ()
|
putIsListenerOn :: AppState -> Bool -> IO ()
|
||||||
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
||||||
|
|
||||||
isConnEstablished :: AppState -> IO Bool
|
|
||||||
isConnEstablished x = do
|
|
||||||
conf <- getConfig x
|
|
||||||
if configDbChannelEnabled conf
|
|
||||||
then do -- if the listener is enabled, we can be sure the connection status is always up to date
|
|
||||||
st <- readIORef $ stateConnStatus x
|
|
||||||
return $ st == ConnEstablished
|
|
||||||
else -- otherwise the only way to check the connection is to make a query
|
|
||||||
isRight <$> usePool x (SQL.sql "SELECT 1")
|
|
||||||
|
|
||||||
isLoaded :: AppState -> IO Bool
|
isLoaded :: AppState -> IO Bool
|
||||||
isLoaded x = do
|
isLoaded x = do
|
||||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||||
connEstablished <- isConnEstablished x
|
connEstablished <- isConnEstablished x
|
||||||
listenerOn <- getIsListenerOn x
|
return $ scacheStatus == SCLoaded && connEstablished
|
||||||
return $ scacheStatus == SCLoaded && connEstablished && listenerOn
|
|
||||||
|
|
||||||
isPending :: AppState -> IO Bool
|
isPending :: AppState -> IO Bool
|
||||||
isPending x = do
|
isPending x = do
|
||||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||||
connStatus <- readIORef $ stateConnStatus x
|
connEstablished <- isConnEstablished x
|
||||||
listenerOn <- getIsListenerOn x
|
return $ scacheStatus == SCPending || not connEstablished
|
||||||
return $ scacheStatus == SCPending || connStatus == ConnPending || not listenerOn
|
|
||||||
|
|
||||||
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
|
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
|
||||||
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
|
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
|
||||||
|
|
||||||
putConnStatus :: AppState -> ConnectionStatus -> IO ()
|
|
||||||
putConnStatus = atomicWriteIORef . stateConnStatus
|
|
||||||
|
|
||||||
getObserver :: AppState -> ObservationHandler
|
getObserver :: AppState -> ObservationHandler
|
||||||
getObserver = stateObserver
|
getObserver = stateObserver
|
||||||
|
|
||||||
-- | Load the SchemaCache by using a connection from the pool.
|
-- | Try to load the schema cache and retry if it fails.
|
||||||
loadSchemaCache :: AppState -> IO SchemaCacheStatus
|
|
||||||
loadSchemaCache appState@AppState{stateObserver=observer} = do
|
|
||||||
conf@AppConfig{..} <- getConfig appState
|
|
||||||
(resultTime, result) <-
|
|
||||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
|
||||||
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
|
||||||
case result of
|
|
||||||
Left e -> do
|
|
||||||
putSCacheStatus appState SCPending
|
|
||||||
putSchemaCache appState Nothing
|
|
||||||
observer $ SchemaCacheErrorObs e
|
|
||||||
return SCPending
|
|
||||||
|
|
||||||
Right sCache -> do
|
|
||||||
-- 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
|
|
||||||
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
|
||||||
putSCacheStatus appState SCPending
|
|
||||||
putSchemaCache appState $ Just sCache
|
|
||||||
observer $ SchemaCacheQueriedObs resultTime
|
|
||||||
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
|
||||||
observer $ SchemaCacheLoadedObs t
|
|
||||||
putSCacheStatus appState SCLoaded
|
|
||||||
return SCLoaded
|
|
||||||
|
|
||||||
-- | The purpose of this worker is to obtain a healthy connection to pg and an
|
|
||||||
-- up-to-date schema cache(SchemaCache). This method is meant to be called
|
|
||||||
-- multiple times by the same thread, but does nothing if the previous
|
|
||||||
-- invocation has not terminated. In all cases this method does not halt the
|
|
||||||
-- calling thread, the work is performed in a separate thread.
|
|
||||||
--
|
--
|
||||||
-- Background thread that does the following :
|
-- 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.
|
||||||
-- 1. Tries to connect to pg server and will keep trying until success.
|
-- It's necessary to flush the pool:
|
||||||
-- 2. Checks if the pg version is supported and if it's not it kills the main
|
--
|
||||||
-- program.
|
-- + Because connections cache the pg catalog(see #2620)
|
||||||
-- 3. Obtains the sCache. If this fails, it goes back to 1.
|
-- + For rapid recovery. Otherwise, the pool idle or lifetime timeout would have to be reached for new healthy connections to be acquired.
|
||||||
internalConnectionWorker :: AppState -> IO ()
|
retryingSchemaCacheLoad :: AppState -> IO ()
|
||||||
internalConnectionWorker appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} = work
|
retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThreadId=mainThreadId} =
|
||||||
|
void $ retrying retryPolicy shouldRetry (\RetryStatus{rsIterNumber, rsPreviousDelay} -> do
|
||||||
|
when (rsIterNumber > 0) $ do
|
||||||
|
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
|
||||||
|
observer $ ConnectionRetryObs delay
|
||||||
|
putNextListenerDelay appState delay
|
||||||
|
|
||||||
|
flushPool appState
|
||||||
|
|
||||||
|
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||||
|
)
|
||||||
where
|
where
|
||||||
work = do
|
qPgVersion :: IO (Maybe PgVersion)
|
||||||
|
qPgVersion = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
observer DBConnectAttemptObs
|
|
||||||
connStatus <- establishConnection appState
|
|
||||||
case connStatus of
|
|
||||||
ConnPending ->
|
|
||||||
unless configDbPoolAutomaticRecovery $ do
|
|
||||||
observer ExitDBNoRecoveryObs
|
|
||||||
killThread mainThreadId
|
|
||||||
ConnEstablished -> do
|
|
||||||
actualPgVersion <- getPgVersion appState
|
|
||||||
when (actualPgVersion < minimumPgVersion) $ do
|
|
||||||
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
|
||||||
killThread mainThreadId
|
|
||||||
observer (DBConnectedObs $ pgvFullName actualPgVersion)
|
|
||||||
-- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again
|
|
||||||
-- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue.
|
|
||||||
when configDbConfig $ reReadConfig False appState
|
|
||||||
scStatus <- loadSchemaCache appState
|
|
||||||
case scStatus of
|
|
||||||
SCLoaded ->
|
|
||||||
-- do nothing and proceed if the load was successful
|
|
||||||
return ()
|
|
||||||
SCPending ->
|
|
||||||
-- retry reloading the schema cache
|
|
||||||
work
|
|
||||||
|
|
||||||
-- | Repeatedly flush the pool, and check if a connection from the
|
|
||||||
-- pool allows access to the PostgreSQL database.
|
|
||||||
--
|
|
||||||
-- Releasing the pool is key for rapid recovery. Otherwise, the pool
|
|
||||||
-- timeout would have to be reached for new healthy connections to be acquired.
|
|
||||||
-- Which might not happen if the server is busy with requests. No idle
|
|
||||||
-- connection, no pool timeout.
|
|
||||||
--
|
|
||||||
-- It's also necessary to release the pool connections because they cache the pg catalog(see #2620)
|
|
||||||
--
|
|
||||||
-- The connection tries are capped, but if the connection times out no error is
|
|
||||||
-- thrown, just 'False' is returned.
|
|
||||||
establishConnection :: AppState -> IO ConnectionStatus
|
|
||||||
establishConnection appState@AppState{stateObserver=observer} =
|
|
||||||
retrying retryPolicy shouldRetry $
|
|
||||||
const $ flushPool appState >> getConnectionStatus
|
|
||||||
where
|
|
||||||
getConnectionStatus :: IO ConnectionStatus
|
|
||||||
getConnectionStatus = do
|
|
||||||
pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
|
pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
|
||||||
case pgVersion of
|
case pgVersion of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
observer $ ConnectionPgVersionErrorObs e
|
observer $ QueryPgVersionError e
|
||||||
putConnStatus appState ConnPending
|
unless configDbPoolAutomaticRecovery $ do
|
||||||
return ConnPending
|
observer ExitDBNoRecoveryObs
|
||||||
Right version -> do
|
killThread mainThreadId
|
||||||
putConnStatus appState ConnEstablished
|
return Nothing
|
||||||
putPgVersion appState version
|
Right actualPgVersion -> do
|
||||||
return ConnEstablished
|
when (actualPgVersion < minimumPgVersion) $ do
|
||||||
|
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
||||||
|
killThread mainThreadId
|
||||||
|
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
||||||
|
putPgVersion appState actualPgVersion
|
||||||
|
return $ Just actualPgVersion
|
||||||
|
|
||||||
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
|
qInDbConfig :: IO ()
|
||||||
shouldRetry rs isConnSucc = do
|
qInDbConfig = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
let
|
when configDbConfig $ readInDbConfig False appState
|
||||||
delay = fromMaybe 0 (rsPreviousDelay rs) `div` oneSecondInUs
|
|
||||||
itShould = ConnPending == isConnSucc && configDbPoolAutomaticRecovery
|
qSchemaCache :: IO (Maybe SchemaCache)
|
||||||
when itShould $ observer $ ConnectionRetryObs delay
|
qSchemaCache = do
|
||||||
when itShould $ putNextDelay appState delay
|
conf@AppConfig{..} <- getConfig appState
|
||||||
|
(resultTime, result) <-
|
||||||
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
|
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||||
|
case result of
|
||||||
|
Left e -> do
|
||||||
|
putSCacheStatus appState SCPending
|
||||||
|
putSchemaCache appState Nothing
|
||||||
|
observer $ SchemaCacheErrorObs e
|
||||||
|
return Nothing
|
||||||
|
|
||||||
|
Right sCache -> do
|
||||||
|
-- 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
|
||||||
|
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||||
|
putSCacheStatus appState SCPending
|
||||||
|
putSchemaCache appState $ Just sCache
|
||||||
|
observer $ SchemaCacheQueriedObs resultTime
|
||||||
|
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||||
|
observer $ SchemaCacheLoadedObs t
|
||||||
|
putSCacheStatus appState SCLoaded
|
||||||
|
return $ Just sCache
|
||||||
|
|
||||||
|
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
|
||||||
|
shouldRetry _ (pgVer, sCache) = do
|
||||||
|
AppConfig{..} <- getConfig appState
|
||||||
|
let itShould = configDbPoolAutomaticRecovery && (isNothing pgVer || isNothing sCache)
|
||||||
return itShould
|
return itShould
|
||||||
|
|
||||||
retryPolicy :: RetryPolicy
|
retryPolicy :: RetryPolicy
|
||||||
retryPolicy =
|
retryPolicy =
|
||||||
let
|
let delayMicroseconds = 32*oneSecondInUs {-32 seconds-} in
|
||||||
delayMicroseconds = 32000000 -- 32 seconds
|
|
||||||
in
|
|
||||||
capDelay delayMicroseconds $ exponentialBackoff oneSecondInUs
|
capDelay delayMicroseconds $ exponentialBackoff oneSecondInUs
|
||||||
oneSecondInUs = 1000000 -- | One second in microseconds
|
|
||||||
|
|
||||||
-- | Re-reads the config plus config options from the db
|
oneSecondInUs = 1000000 -- one second in microseconds
|
||||||
reReadConfig :: Bool -> AppState -> IO ()
|
|
||||||
reReadConfig startingUp appState@AppState{stateObserver=observer} = do
|
-- | Reads the in-db config and reads the config file again
|
||||||
|
-- | 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.
|
||||||
|
readInDbConfig :: Bool -> AppState -> IO ()
|
||||||
|
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
pgVer <- getPgVersion appState
|
pgVer <- getPgVersion appState
|
||||||
dbSettings <-
|
dbSettings <-
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ main CLI{cliCommand, cliPath} = do
|
|||||||
AppState.destroy
|
AppState.destroy
|
||||||
(\appState -> case cliCommand of
|
(\appState -> case cliCommand of
|
||||||
CmdDumpConfig -> do
|
CmdDumpConfig -> do
|
||||||
when configDbConfig $ AppState.reReadConfig True appState
|
when configDbConfig $ AppState.readInDbConfig True appState
|
||||||
putStr . Config.toText =<< AppState.getConfig appState
|
putStr . Config.toText =<< AppState.getConfig appState
|
||||||
CmdDumpSchema -> putStrLn =<< dumpSchema appState
|
CmdDumpSchema -> do
|
||||||
|
when configDbConfig $ AppState.readInDbConfig True appState
|
||||||
|
putStrLn =<< dumpSchema appState
|
||||||
CmdRun -> App.run appState)
|
CmdRun -> App.run appState)
|
||||||
|
|
||||||
-- | Dump SchemaCache schema to JSON
|
-- | Dump SchemaCache schema to JSON
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ retryingListen appState = do
|
|||||||
|
|
||||||
delay <- AppState.getNextListenerDelay appState
|
delay <- AppState.getNextListenerDelay appState
|
||||||
when (delay > 1) $ do -- if we did a retry
|
when (delay > 1) $ do -- if we did a retry
|
||||||
-- assume we lost notifications, call the connection worker which will also reload the schema cache
|
-- assume we lost notifications, refresh the schema cache
|
||||||
AppState.connectionWorker appState
|
AppState.schemaCacheLoader appState
|
||||||
-- reset the delay
|
-- reset the delay
|
||||||
AppState.putNextListenerDelay appState 1
|
AppState.putNextListenerDelay appState 1
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ retryingListen appState = do
|
|||||||
handleNotification channel msg =
|
handleNotification channel msg =
|
||||||
if | BS.null msg -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
|
if | BS.null msg -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
|
||||||
| msg == "reload schema" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
|
| msg == "reload schema" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
|
||||||
| msg == "reload config" -> observer (DBListenerGotConfigMsg channel) >> AppState.reReadConfig False appState
|
| msg == "reload config" -> observer (DBListenerGotConfigMsg channel) >> AppState.readInDbConfig False appState
|
||||||
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
|
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
|
||||||
|
|
||||||
cacheReloader =
|
cacheReloader =
|
||||||
AppState.connectionWorker appState
|
AppState.schemaCacheLoader appState
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ data Observation
|
|||||||
| AppStartObs ByteString
|
| AppStartObs ByteString
|
||||||
| AppServerPortObs NS.PortNumber
|
| AppServerPortObs NS.PortNumber
|
||||||
| AppServerUnixObs FilePath
|
| AppServerUnixObs FilePath
|
||||||
| DBConnectAttemptObs
|
|
||||||
| ExitUnsupportedPgVersion PgVersion PgVersion
|
| ExitUnsupportedPgVersion PgVersion PgVersion
|
||||||
| ExitDBNoRecoveryObs
|
| ExitDBNoRecoveryObs
|
||||||
| ExitDBFatalError ObsFatalError SQL.UsageError
|
| ExitDBFatalError ObsFatalError SQL.UsageError
|
||||||
@@ -39,7 +38,6 @@ data Observation
|
|||||||
| SchemaCacheSummaryObs Text
|
| SchemaCacheSummaryObs Text
|
||||||
| SchemaCacheLoadedObs Double
|
| SchemaCacheLoadedObs Double
|
||||||
| ConnectionRetryObs Int
|
| ConnectionRetryObs Int
|
||||||
| ConnectionPgVersionErrorObs SQL.UsageError
|
|
||||||
| DBListenStart Text
|
| DBListenStart Text
|
||||||
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
|
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
|
||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
@@ -50,6 +48,7 @@ data Observation
|
|||||||
| ConfigSucceededObs
|
| ConfigSucceededObs
|
||||||
| QueryRoleSettingsErrorObs SQL.UsageError
|
| QueryRoleSettingsErrorObs SQL.UsageError
|
||||||
| QueryErrorCodeHighObs SQL.UsageError
|
| QueryErrorCodeHighObs SQL.UsageError
|
||||||
|
| QueryPgVersionError SQL.UsageError
|
||||||
| PoolAcqTimeoutObs SQL.UsageError
|
| PoolAcqTimeoutObs SQL.UsageError
|
||||||
| HasqlPoolObs SQL.Observation
|
| HasqlPoolObs SQL.Observation
|
||||||
| PoolRequest
|
| PoolRequest
|
||||||
@@ -69,8 +68,6 @@ observationMessage = \case
|
|||||||
"Listening on port " <> show port
|
"Listening on port " <> show port
|
||||||
AppServerUnixObs sock ->
|
AppServerUnixObs sock ->
|
||||||
"Listening on unix socket " <> show sock
|
"Listening on unix socket " <> show sock
|
||||||
DBConnectAttemptObs ->
|
|
||||||
"Attempting to connect to the database..."
|
|
||||||
DBConnectedObs ver ->
|
DBConnectedObs ver ->
|
||||||
"Successfully connected to " <> ver
|
"Successfully connected to " <> ver
|
||||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||||
@@ -78,7 +75,7 @@ observationMessage = \case
|
|||||||
ExitDBNoRecoveryObs ->
|
ExitDBNoRecoveryObs ->
|
||||||
"Automatic recovery disabled, exiting."
|
"Automatic recovery disabled, exiting."
|
||||||
ExitDBFatalError ServerAuthError usageErr ->
|
ExitDBFatalError ServerAuthError usageErr ->
|
||||||
jsonMessage usageErr
|
"Failed to establish a connection. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||||
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerError42P05 usageErr ->
|
ExitDBFatalError ServerError42P05 usageErr ->
|
||||||
@@ -95,8 +92,8 @@ observationMessage = \case
|
|||||||
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||||
ConnectionRetryObs delay ->
|
ConnectionRetryObs delay ->
|
||||||
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||||
ConnectionPgVersionErrorObs usageErr ->
|
QueryPgVersionError usageErr ->
|
||||||
jsonMessage usageErr
|
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||||
DBListenStart channel -> do
|
DBListenStart channel -> do
|
||||||
"Listening for notifications on the " <> show channel <> " channel"
|
"Listening for notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
|
|||||||
@@ -622,7 +622,11 @@ generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, rel
|
|||||||
where
|
where
|
||||||
rsSelName = fromMaybe relName relAlias
|
rsSelName = fromMaybe relName relAlias
|
||||||
rsEmbedMode = if relIsToOne rel then JsonObject else JsonArray
|
rsEmbedMode = if relIsToOne rel then JsonObject else JsonArray
|
||||||
rsEmptyEmbed = null select && null forest
|
rsEmptyEmbed = hasOnlyNullEmbed (null select) forest
|
||||||
|
hasOnlyNullEmbed = foldr checkIfNullEmbed
|
||||||
|
checkIfNullEmbed :: ReadPlanTree -> Bool -> Bool
|
||||||
|
checkIfNullEmbed (Node ReadPlan{select=s} f) isNullEmbed =
|
||||||
|
isNullEmbed && hasOnlyNullEmbed (null s) f
|
||||||
generateRelSelectField _ = Nothing
|
generateRelSelectField _ = Nothing
|
||||||
|
|
||||||
generateSpreadSelectFields :: ReadPlan -> [SpreadSelectField]
|
generateSpreadSelectFields :: ReadPlan -> [SpreadSelectField]
|
||||||
|
|||||||
@@ -296,8 +296,7 @@ makeRowFilter tn c =
|
|||||||
& required ?~ False
|
& required ?~ False
|
||||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
& in_ .~ ParamQuery
|
& in_ .~ ParamQuery
|
||||||
& type_ ?~ SwaggerString
|
& type_ ?~ SwaggerString))
|
||||||
& format ?~ colType c))
|
|
||||||
|
|
||||||
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
||||||
makeRowFilters tn = fmap (makeRowFilter tn)
|
makeRowFilters tn = fmap (makeRowFilter tn)
|
||||||
@@ -402,7 +401,7 @@ postgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecu
|
|||||||
& description ?~ fromMaybe "This is a dynamic API generated by PostgREST" dDesc)
|
& description ?~ fromMaybe "This is a dynamic API generated by PostgREST" dDesc)
|
||||||
& externalDocs ?~ ((mempty :: ExternalDocs)
|
& externalDocs ?~ ((mempty :: ExternalDocs)
|
||||||
& description ?~ "PostgREST Documentation"
|
& description ?~ "PostgREST Documentation"
|
||||||
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
|
||||||
& host .~ h'
|
& host .~ h'
|
||||||
& definitions .~ fromList (makeTableDef rels <$> ti)
|
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs ti)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
|
|||||||
@@ -95,13 +95,14 @@ instance JSON.ToJSON SchemaCache where
|
|||||||
]
|
]
|
||||||
|
|
||||||
showSummary :: SchemaCache -> Text
|
showSummary :: SchemaCache -> Text
|
||||||
showSummary (SchemaCache tbls rels routs reps mediaHdlrs _) =
|
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs) =
|
||||||
T.intercalate ", "
|
T.intercalate ", "
|
||||||
[ show (HM.size tbls) <> " Relations"
|
[ show (HM.size tbls) <> " Relations"
|
||||||
, show (HM.size rels) <> " Relationships"
|
, show (HM.size rels) <> " Relationships"
|
||||||
, show (HM.size routs) <> " Functions"
|
, show (HM.size routs) <> " Functions"
|
||||||
, show (HM.size reps) <> " Domain Representations"
|
, show (HM.size reps) <> " Domain Representations"
|
||||||
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
|
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
|
||||||
|
, show (S.size tzs) <> " Timezones"
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | A view foreign key or primary key dependency detected on its source table
|
-- | A view foreign key or primary key dependency detected on its source table
|
||||||
@@ -646,7 +647,7 @@ tablesSqlQuery pgVer =
|
|||||||
information_schema._pg_truetypid(a.*, t.*),
|
information_schema._pg_truetypid(a.*, t.*),
|
||||||
information_schema._pg_truetypmod(a.*, t.*)
|
information_schema._pg_truetypmod(a.*, t.*)
|
||||||
)::integer AS character_maximum_length,
|
)::integer AS character_maximum_length,
|
||||||
COALESCE(bt.typname, t.typname)::name AS udt_name,
|
COALESCE(bt.oid, t.oid) AS base_type,
|
||||||
a.attnum::integer AS position
|
a.attnum::integer AS position
|
||||||
FROM pg_attribute a
|
FROM pg_attribute a
|
||||||
LEFT JOIN pg_description AS d
|
LEFT JOIN pg_description AS d
|
||||||
@@ -690,14 +691,13 @@ tablesSqlQuery pgVer =
|
|||||||
FROM columns info
|
FROM columns info
|
||||||
LEFT OUTER JOIN (
|
LEFT OUTER JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
n.nspname AS s,
|
e.enumtypid,
|
||||||
t.typname AS n,
|
|
||||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS vals
|
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS vals
|
||||||
FROM pg_type t
|
FROM pg_type t
|
||||||
JOIN pg_enum e ON t.oid = e.enumtypid
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
GROUP BY s,n
|
GROUP BY enumtypid
|
||||||
) AS enum_info ON info.udt_name = enum_info.n
|
) AS enum_info ON info.base_type = enum_info.enumtypid
|
||||||
WHERE info.table_schema NOT IN ('pg_catalog', 'information_schema')
|
WHERE info.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
GROUP BY info.table_schema, info.table_name
|
GROUP BY info.table_schema, info.table_name
|
||||||
),
|
),
|
||||||
@@ -872,7 +872,7 @@ allM2OandO2ORels pgVer =
|
|||||||
JOIN LATERAL (
|
JOIN LATERAL (
|
||||||
SELECT
|
SELECT
|
||||||
array_agg(row(cols.attname, refs.attname) order by ord) AS cols_and_fcols,
|
array_agg(row(cols.attname, refs.attname) order by ord) AS cols_and_fcols,
|
||||||
jsonb_agg(cols.attname order by ord) AS cols
|
jsonb_agg(cols.attname order by cols.attnum) AS cols
|
||||||
FROM unnest(traint.conkey, traint.confkey) WITH ORDINALITY AS _(col, ref, ord)
|
FROM unnest(traint.conkey, traint.confkey) WITH ORDINALITY AS _(col, ref, ord)
|
||||||
JOIN pg_attribute cols ON cols.attrelid = traint.conrelid AND cols.attnum = col
|
JOIN pg_attribute cols ON cols.attrelid = traint.conrelid AND cols.attnum = col
|
||||||
JOIN pg_attribute refs ON refs.attrelid = traint.confrelid AND refs.attnum = ref
|
JOIN pg_attribute refs ON refs.attrelid = traint.confrelid AND refs.attnum = ref
|
||||||
|
|||||||
@@ -218,6 +218,40 @@
|
|||||||
pdSchema: public
|
pdSchema: public
|
||||||
pdVolatility: Volatile
|
pdVolatility: Volatile
|
||||||
|
|
||||||
|
- - qiName: reset_db_schemas_config
|
||||||
|
qiSchema: public
|
||||||
|
- - pdDescription: null
|
||||||
|
pdFuncSettings: []
|
||||||
|
pdHasVariadic: false
|
||||||
|
pdName: reset_db_schemas_config
|
||||||
|
pdParams: []
|
||||||
|
pdReturnType:
|
||||||
|
contents:
|
||||||
|
contents:
|
||||||
|
qiName: void
|
||||||
|
qiSchema: pg_catalog
|
||||||
|
tag: Scalar
|
||||||
|
tag: Single
|
||||||
|
pdSchema: public
|
||||||
|
pdVolatility: Volatile
|
||||||
|
|
||||||
|
- - qiName: change_db_schemas_config
|
||||||
|
qiSchema: public
|
||||||
|
- - pdDescription: null
|
||||||
|
pdFuncSettings: []
|
||||||
|
pdHasVariadic: false
|
||||||
|
pdName: change_db_schemas_config
|
||||||
|
pdParams: []
|
||||||
|
pdReturnType:
|
||||||
|
contents:
|
||||||
|
contents:
|
||||||
|
qiName: void
|
||||||
|
qiSchema: pg_catalog
|
||||||
|
tag: Scalar
|
||||||
|
tag: Single
|
||||||
|
pdSchema: public
|
||||||
|
pdVolatility: Volatile
|
||||||
|
|
||||||
- - qiName: change_db_schema_and_full_reload
|
- - qiName: change_db_schema_and_full_reload
|
||||||
qiSchema: public
|
qiSchema: public
|
||||||
- - pdDescription: null
|
- - pdDescription: null
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ GRANT
|
|||||||
CREATE SCHEMA v1;
|
CREATE SCHEMA v1;
|
||||||
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
||||||
|
|
||||||
|
CREATE SCHEMA test;
|
||||||
|
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
|
||||||
|
|
||||||
CREATE TABLE authors_only ();
|
CREATE TABLE authors_only ();
|
||||||
GRANT SELECT ON authors_only TO postgrest_test_author;
|
GRANT SELECT ON authors_only TO postgrest_test_author;
|
||||||
|
|
||||||
@@ -226,3 +229,17 @@ $$ language sql;
|
|||||||
create function get_statement_timeout(items) returns text as $$
|
create function get_statement_timeout(items) returns text as $$
|
||||||
select current_setting('statement_timeout', true) as statement_timeout
|
select current_setting('statement_timeout', true) as statement_timeout
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
|
create function change_db_schemas_config() returns void as $_$
|
||||||
|
begin
|
||||||
|
alter role postgrest_test_authenticator set pgrst.db_schemas = 'test';
|
||||||
|
end $_$ volatile security definer language plpgsql;
|
||||||
|
|
||||||
|
create function reset_db_schemas_config() returns void as $_$
|
||||||
|
begin
|
||||||
|
alter role postgrest_test_authenticator reset pgrst.db_schemas;
|
||||||
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
|
create function test.get_current_schema() returns text as $$
|
||||||
|
select current_schema()::text;
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
+18
-1
@@ -778,7 +778,7 @@ def test_metrics_include_schema_cache_fails(defaultenv, metapostgrest):
|
|||||||
r'pgrst_schema_cache_loads_total{status="FAIL"} (\d+)', response.text
|
r'pgrst_schema_cache_loads_total{status="FAIL"} (\d+)', response.text
|
||||||
).group(1)
|
).group(1)
|
||||||
)
|
)
|
||||||
assert metrics > 3.0
|
assert metrics == 1.0
|
||||||
|
|
||||||
reset_statement_timeout(metapostgrest, role)
|
reset_statement_timeout(metapostgrest, role)
|
||||||
|
|
||||||
@@ -1579,3 +1579,20 @@ def test_admin_metrics(defaultenv):
|
|||||||
assert "pgrst_db_pool_waiting" in response.text
|
assert "pgrst_db_pool_waiting" in response.text
|
||||||
assert "pgrst_db_pool_available" in response.text
|
assert "pgrst_db_pool_available" in response.text
|
||||||
assert "pgrst_db_pool_timeouts_total" in response.text
|
assert "pgrst_db_pool_timeouts_total" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):
|
||||||
|
"verify that the Schema Cache loads correctly at startup, using the in-db `pgrst.db_schemas` config"
|
||||||
|
|
||||||
|
response = metapostgrest.session.post("/rpc/change_db_schemas_config")
|
||||||
|
assert response.text == ""
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
with run(env=defaultenv) as postgrest:
|
||||||
|
response = postgrest.session.get("/rpc/get_current_schema")
|
||||||
|
assert response.text == '"test"'
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = metapostgrest.session.post("/rpc/reset_db_schemas_config")
|
||||||
|
assert response.text == ""
|
||||||
|
assert response.status_code == 204
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
|
|
||||||
let docsUrl = r ^? key "externalDocs" . key "url"
|
let docsUrl = r ^? key "externalDocs" . key "url"
|
||||||
|
|
||||||
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
|
||||||
|
|
||||||
describe "schema" $ do
|
describe "schema" $ do
|
||||||
|
|
||||||
|
|||||||
@@ -1218,6 +1218,20 @@ spec actualPgVersion = do
|
|||||||
[json|[{"id":1,"name":"Angela Martin"}]|]
|
[json|[{"id":1,"name":"Angela Martin"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works on nested relationships" $ do
|
||||||
|
get "/users?select=*,users_tasks(tasks(projects()))" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Angela Martin"}, {"id":2,"name":"Michael Scott"}, {"id":3,"name":"Dwight Schrute"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/users?select=*,users_tasks!inner(tasks!inner(projects()))&users_tasks.tasks.id=eq.3" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Angela Martin"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/users?select=*,tasks(projects(clients()),users_tasks())" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Angela Martin"}, {"id":2,"name":"Michael Scott"}, {"id":3,"name":"Dwight Schrute"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/users?select=*,tasks!inner(projects(clients()),users_tasks(),name)&tasks.id=eq.3" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Angela Martin","tasks":[{"name": "Design w10"}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "empty root select" $
|
context "empty root select" $
|
||||||
it "gives all columns" $ do
|
it "gives all columns" $ do
|
||||||
get "/projects?select=" `shouldRespondWith`
|
get "/projects?select=" `shouldRespondWith`
|
||||||
@@ -1413,4 +1427,3 @@ spec actualPgVersion = do
|
|||||||
get "/infinite_recursion?select=*" `shouldRespondWith`
|
get "/infinite_recursion?select=*" `shouldRespondWith`
|
||||||
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2872,7 +2872,7 @@ CREATE TABLE test.students_info(
|
|||||||
, code text
|
, code text
|
||||||
, address text
|
, address text
|
||||||
, primary key(id, code)
|
, primary key(id, code)
|
||||||
, foreign key (id, code) references test.students(id, code) on delete cascade
|
, foreign key (code, id) references test.students(code, id) on delete cascade
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE test.country(
|
CREATE TABLE test.country(
|
||||||
|
|||||||
Reference in New Issue
Block a user